Narrow dedup cache fix to generation correctness

This commit is contained in:
vekovius 2026-08-14 13:17:02 -05:00
parent 75223bf266
commit 4816a13244
2 changed files with 8 additions and 57 deletions

View File

@ -28,7 +28,6 @@ final class LRUDeduplicationCache<Value> {
private var map: [String: StoredEntry] = [:]
private var order: [OrderEntry] = []
private var head: Int = 0
private var staleNodeCount: Int = 0
private var nextGeneration: UInt64 = 0
private let capacity: Int
@ -44,11 +43,6 @@ final class LRUDeduplicationCache<Value> {
map.count
}
#if DEBUG
/// Order-node storage exposed only for bounded-storage regression tests.
var _orderStorageCountForTesting: Int { order.count }
#endif
/// Checks if a key exists in the cache
func contains(_ key: String) -> Bool {
map[key] != nil
@ -71,14 +65,11 @@ final class LRUDeduplicationCache<Value> {
}
trimIfNeeded()
compactIfNeeded()
}
/// Removes a specific key from the cache
func remove(_ key: String) {
guard map.removeValue(forKey: key) != nil else { return }
staleNodeCount += 1
compactIfNeeded()
map.removeValue(forKey: key)
}
/// Clears all entries from the cache
@ -86,7 +77,6 @@ final class LRUDeduplicationCache<Value> {
map.removeAll()
order.removeAll()
head = 0
staleNodeCount = 0
nextGeneration = 0
}
@ -103,11 +93,15 @@ final class LRUDeduplicationCache<Value> {
let candidate = order[head]
head += 1
// Periodically compact the consumed prefix, matching the previous
// cache behavior without scanning unconsumed order nodes.
if head >= 32 && head * 2 >= order.count {
order.removeFirst(head)
head = 0
}
guard let liveEntry = map[candidate.key],
liveEntry.generation == candidate.generation else {
if staleNodeCount > 0 {
staleNodeCount -= 1
}
continue
}
@ -116,24 +110,6 @@ final class LRUDeduplicationCache<Value> {
}
return false
}
private func compactIfNeeded() {
let unconsumedCount = order.count - head
let shouldDropConsumedPrefix = head >= 32 && head * 2 >= order.count
let shouldRemoveStaleNodes = staleNodeCount >= 32 && staleNodeCount * 2 >= unconsumedCount
guard shouldDropConsumedPrefix || shouldRemoveStaleNodes else { return }
if shouldRemoveStaleNodes {
order = order[head...].filter { candidate in
map[candidate.key]?.generation == candidate.generation
}
staleNodeCount = 0
} else {
order.removeFirst(head)
}
head = 0
}
}
// MARK: - Content Normalizer

View File

@ -166,31 +166,6 @@ struct LRUDeduplicationCacheTests {
#expect(cache.contains("c"))
}
@Test func repeatedRemovalAndReinsertionCompactsStaleNodes() {
let cache = LRUDeduplicationCache<Int>(capacity: 3)
cache.record("a", value: 0)
for value in 1...1_001 {
cache.remove("a")
cache.record("a", value: value)
}
#expect(cache.count == 1)
#expect(cache.value(for: "a") == 1_001)
#if DEBUG
#expect(cache._orderStorageCountForTesting <= 32)
#endif
cache.record("b", value: 1)
cache.record("c", value: 2)
cache.record("d", value: 3)
#expect(!cache.contains("a"))
#expect(cache.contains("b"))
#expect(cache.contains("c"))
#expect(cache.contains("d"))
}
// MARK: - Edge Cases
@Test func emptyKey_works() {