Skip to content

Conversation

@0xLeif
Copy link
Owner

@0xLeif 0xLeif commented Dec 5, 2025

No description provided.

@0xLeif 0xLeif requested a review from Copilot December 5, 2025 00:56
@gemini-code-assist
Copy link

Summary of Changes

Hello @0xLeif, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical deadlock scenario within the PersistableCache class. Previously, if an error occurred during file saving or deletion, the associated lock might not be released, leading to subsequent operations hanging indefinitely. The changes implement defer statements to guarantee lock release, significantly improving the robustness and reliability of the caching mechanism. Furthermore, new comprehensive tests have been added to validate this fix and explore other potential concurrency issues within the caching system.

Highlights

  • Deadlock Fix: Resolved a potential deadlock in PersistableCache's save() and delete() methods by ensuring locks are always released using defer statements, even if an error occurs during file operations.
  • New Deadlock Test: Added a new test case, testPersistableCacheDeadlockAfterDeleteError, to specifically reproduce and verify the fix for the PersistableCache deadlock issue.
  • Additional LRUCache Tests: Introduced additional bug-exposing tests for LRUCache, including testLRUCacheContainsUpdatesRecency to validate LRU order updates and testLRUCacheDualLockRaceCondition to check for race conditions.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request correctly fixes a potential deadlock in PersistableCache by using defer to ensure the lock is always released, which is a great improvement for thread safety. The addition of BugExposingTests.swift is also very valuable, especially the testPersistableCacheDeadlockAfterDeleteError test which directly verifies the fix and prevents regressions. The new test file also includes tests for LRUCache, which improve overall test coverage. I've found one minor issue in the implementation of one of the new tests that could be improved for clarity.

Comment on lines +82 to +122
let expectation = XCTestExpectation(description: "Concurrent operations complete")

DispatchQueue.concurrentPerform(iterations: iterations) { i in
let key = i % 30

// Interleave different operations
switch i % 4 {
case 0:
cache[key] = i
case 1:
_ = cache[key]
case 2:
_ = cache.contains(key)
case 3:
cache.remove(key)
default:
break
}

// Periodically check consistency
if i % 1000 == 0 {
let count = cache.allValues.count
if count > 50 {
errorLock.lock()
errors.append("Cache exceeded capacity: \(count) > 50 at iteration \(i)")
errorLock.unlock()
}
}
}

expectation.fulfill()
wait(for: [expectation], timeout: 30.0)

// Report any consistency violations
if !errors.isEmpty {
XCTFail("Race condition detected! Errors:\n\(errors.joined(separator: "\n"))")
}

// Final consistency check
let finalCount = cache.allValues.count
XCTAssertLessThanOrEqual(finalCount, 50, "Final cache size \(finalCount) exceeds capacity of 50")

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The use of XCTestExpectation here is not correct. DispatchQueue.concurrentPerform is a synchronous call that blocks the current thread until all of its iterations are complete. This means the expectation is fulfilled immediately after the concurrent work is done, and wait(for:timeout:) returns right away without actually waiting for any asynchronous work. The test still works as intended for concurrency testing, but the expectation adds confusion. It's better to remove the expectation and the wait for clarity.

        DispatchQueue.concurrentPerform(iterations: iterations) { i in
            let key = i % 30

            // Interleave different operations
            switch i % 4 {
            case 0:
                cache[key] = i
            case 1:
                _ = cache[key]
            case 2:
                _ = cache.contains(key)
            case 3:
                cache.remove(key)
            default:
                break
            }

            // Periodically check consistency
            if i % 1000 == 0 {
                let count = cache.allValues.count
                if count > 50 {
                    errorLock.lock()
                    errors.append("Cache exceeded capacity: \(count) > 50 at iteration \(i)")
                    errorLock.unlock()
                }
            }
        }

        // Report any consistency violations
        if !errors.isEmpty {
            XCTFail("Race condition detected! Errors:\n\(errors.joined(separator: "\n"))")
        }

        // Final consistency check
        let finalCount = cache.allValues.count
        XCTAssertLessThanOrEqual(finalCount, 50, "Final cache size \(finalCount) exceeds capacity of 50")

@0xLeif 0xLeif merged commit 35221b7 into main Dec 5, 2025
7 checks passed
@0xLeif 0xLeif deleted the leif/updates branch December 5, 2025 01:03
Copilot finished reviewing on behalf of 0xLeif December 5, 2025 01:12
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes a critical deadlock issue in PersistableCache where the NSLock was not properly released when save() or delete() operations threw exceptions. The fix replaces manual lock/unlock calls with the safer defer pattern, ensuring locks are always released even when errors are thrown. The PR also introduces a comprehensive test suite to verify the fix and expose other potential concurrency issues in the cache implementations.

Key changes:

  • Replaced manual lock management with defer { lock.unlock() } pattern in PersistableCache.save() and PersistableCache.delete() methods
  • Added BugExposingTests.swift with three test cases covering the deadlock fix, LRU cache recency behavior, and potential race conditions from dual-lock architecture

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 6 comments.

File Description
Sources/Cache/Cache/PersistableCache.swift Fixes deadlock by using defer to guarantee lock release in save() and delete() methods, even when exceptions are thrown
Tests/CacheTests/BugExposingTests.swift Adds comprehensive test coverage including deadlock detection, LRU cache behavior validation, and concurrency stress testing

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

XCTAssertNotEqual(result, .timedOut, "DEADLOCK DETECTED: save() hung after delete() error - lock was never released!")
}

// MARK: - LRUCache contains() Updates Recency
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment header says "LRUCache contains() Updates Recency" but doesn't include a bug number like the other test sections (e.g., "Bug #1" and "Bug #3"). For consistency, consider either adding a bug number (e.g., "Bug #2:") or removing the "Bug #1" and "Bug #3" prefixes from the other sections to maintain a uniform documentation style.

Suggested change
// MARK: - LRUCache contains() Updates Recency
// MARK: - Bug #2: LRUCache contains() Updates Recency

Copilot uses AI. Check for mistakes.
// Periodically check consistency
if i % 1000 == 0 {
let count = cache.allValues.count
if count > 50 {
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The magic number 50 is used here and also on line 77 (capacity) and line 122 (final assertion). Consider extracting this to a constant at the test method level for better maintainability:

let capacity = 50
let cache = LRUCache<Int, Int>(capacity: capacity)
// ...
if count > capacity {
    // ...
}
// ...
XCTAssertLessThanOrEqual(finalCount, capacity, "Final cache size \(finalCount) exceeds capacity of \(capacity)")

Copilot uses AI. Check for mistakes.
Comment on lines +10 to +12
/// This test exposes a deadlock bug in PersistableCache.
/// When delete() throws an error, the NSLock is never released.
/// Any subsequent call to save() or delete() will hang forever.
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test documentation states "This test exposes a deadlock bug in PersistableCache" (present tense), but with the accompanying fix in PersistableCache.swift, this bug is now resolved. Consider updating the documentation to clarify that this test verified the deadlock bug that has now been fixed, or that it serves as a regression test. For example:

/// This test verifies that a deadlock bug in PersistableCache has been fixed.
/// Previously, when delete() threw an error, the NSLock was never released,
/// causing subsequent calls to save() or delete() to hang forever.
Suggested change
/// This test exposes a deadlock bug in PersistableCache.
/// When delete() throws an error, the NSLock is never released.
/// Any subsequent call to save() or delete() will hang forever.
/// This test verifies that a deadlock bug in PersistableCache has been fixed.
/// Previously, when delete() threw an error, the NSLock was never released,
/// causing subsequent calls to save() or delete() to hang forever.

Copilot uses AI. Check for mistakes.
Comment on lines +49 to +50
/// This test validates that contains() updates LRU order.
/// Calling contains() is considered a "use" of the key, promoting it to most recently used.
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[nitpick] The test comment states "This test validates that contains() updates LRU order" but the test name and context suggest this might be exposing a bug rather than validating correct behavior. Consider clarifying whether this is testing for expected behavior or exposing a bug. If contains() updating recency is the intended behavior, the comment is accurate. If not, it should indicate this is a bug being exposed.

Suggested change
/// This test validates that contains() updates LRU order.
/// Calling contains() is considered a "use" of the key, promoting it to most recently used.
/// This test exposes a bug: contains() updates LRU order.
/// Calling contains() is currently considered a "use" of the key, promoting it to most recently used.

Copilot uses AI. Check for mistakes.
Comment on lines +82 to +113
let expectation = XCTestExpectation(description: "Concurrent operations complete")

DispatchQueue.concurrentPerform(iterations: iterations) { i in
let key = i % 30

// Interleave different operations
switch i % 4 {
case 0:
cache[key] = i
case 1:
_ = cache[key]
case 2:
_ = cache.contains(key)
case 3:
cache.remove(key)
default:
break
}

// Periodically check consistency
if i % 1000 == 0 {
let count = cache.allValues.count
if count > 50 {
errorLock.lock()
errors.append("Cache exceeded capacity: \(count) > 50 at iteration \(i)")
errorLock.unlock()
}
}
}

expectation.fulfill()
wait(for: [expectation], timeout: 30.0)
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The expectation is fulfilled immediately after concurrentPerform completes (line 112), but concurrentPerform is a synchronous call that already blocks until all iterations finish. This makes the expectation and the wait on line 113 redundant. The expectation pattern is typically used for asynchronous operations, but here everything is already synchronous.

Consider removing the expectation entirely and directly asserting after concurrentPerform completes:

DispatchQueue.concurrentPerform(iterations: iterations) { i in
    // ... existing logic
}

// Report any consistency violations
if !errors.isEmpty {
    XCTFail("Race condition detected! Errors:\n\(errors.joined(separator: "\n"))")
}

// Final consistency check
let finalCount = cache.allValues.count
XCTAssertLessThanOrEqual(finalCount, 50, "Final cache size \(finalCount) exceeds capacity of 50")

Copilot uses AI. Check for mistakes.
/// its own lock separate from the parent Cache's lock.
func testLRUCacheDualLockRaceCondition() {
let cache = LRUCache<Int, Int>(capacity: 50)
let iterations = 100_000
Copy link

Copilot AI Dec 5, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses 100,000 iterations which may cause the test to be slow and potentially timeout on CI systems or slower machines. Consider reducing this to a more reasonable number (e.g., 10,000 or 1,000) that still effectively tests for race conditions while maintaining reasonable test execution time.

Suggested change
let iterations = 100_000
let iterations = 10_000

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants