Skip to content

Commit 9e74c0e

Browse files
authored
Fix race in cache eviction (#18528)
1 parent 230df0c commit 9e74c0e

File tree

1 file changed

+38
-50
lines changed

1 file changed

+38
-50
lines changed

src/Compiler/Utilities/Caches.fs

Lines changed: 38 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -29,29 +29,27 @@ type CacheOptions =
2929
[<Sealed; NoComparison; NoEquality>]
3030
[<DebuggerDisplay("{ToString()}")>]
3131
type CachedEntity<'Key, 'Value> =
32-
val mutable Key: 'Key
33-
val mutable Value: 'Value
34-
val mutable AccessCount: int64
35-
val mutable Node: LinkedListNode<CachedEntity<'Key, 'Value>> voption
32+
val mutable private key: 'Key
33+
val mutable private value: 'Value
3634

37-
new(key, value) =
38-
{
39-
Key = key
40-
Value = value
41-
AccessCount = 0L
42-
Node = ValueNone
43-
}
35+
[<DefaultValue(false)>]
36+
val mutable private node: LinkedListNode<CachedEntity<'Key, 'Value>>
4437

45-
// This is one time initialization, outside of the constructor because of circular reference.
46-
// The contract is that each CachedEntity that the EntityPool produces, has Node assigned.
47-
member this.WithNode() =
48-
this.Node <- ValueSome(LinkedListNode this)
49-
this
38+
private new(key, value) = { key = key; value = value }
39+
40+
member this.Node = this.node
41+
member this.Key = this.key
42+
member this.Value = this.value
43+
44+
static member Create(key: 'Key, value: 'Value) =
45+
let entity = CachedEntity(key, value)
46+
// The contract is that each CachedEntity produced by the EntityPool always has Node referencing itself.
47+
entity.node <- LinkedListNode(entity)
48+
entity
5049

5150
member this.ReUse(key, value) =
52-
this.Key <- key
53-
this.Value <- value
54-
this.AccessCount <- 0L
51+
this.key <- key
52+
this.value <- value
5553
this
5654

5755
override this.ToString() = $"{this.Key}"
@@ -142,21 +140,15 @@ type CacheMetrics(cacheId) =
142140
// More than totalCapacity can be created, but it will hold for reuse at most totalCapacity.
143141
type EntityPool<'Key, 'Value>(totalCapacity, cacheId) =
144142
let pool = ConcurrentBag<CachedEntity<'Key, 'Value>>()
145-
let mutable created = 0
146143

147-
let overCapacity =
148-
CacheMetrics.Meter.CreateCounter<int64>("over-capacity", "count", cacheId)
144+
let created = CacheMetrics.Meter.CreateCounter<int64>("created", "count", cacheId)
149145

150146
member _.Acquire(key, value) =
151147
match pool.TryTake() with
152148
| true, entity -> entity.ReUse(key, value)
153149
| _ ->
154-
if Interlocked.Increment &created > totalCapacity then
155-
overCapacity.Add 1L
156-
157-
// Associate a LinkedListNode with freshly created entity.
158-
// This is a one time initialization.
159-
CachedEntity(key, value).WithNode()
150+
created.Add 1L
151+
CachedEntity.Create(key, value)
160152

161153
member _.Reclaim(entity: CachedEntity<'Key, 'Value>) =
162154
if pool.Count < totalCapacity then
@@ -223,13 +215,10 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC
223215
async {
224216
match! mb.Receive() with
225217
| EvictionQueueMessage.Add entity ->
226-
227-
assert entity.Node.IsSome
228-
229-
evictionQueue.AddLast(entity.Node.Value)
218+
evictionQueue.AddLast(entity.Node)
230219

231220
// Evict one immediately if necessary.
232-
while evictionQueue.Count > capacity do
221+
if evictionQueue.Count > capacity then
233222
let first = nonNull evictionQueue.First
234223

235224
match store.TryRemove(first.Value.Key) with
@@ -240,16 +229,13 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC
240229
evicted.Trigger()
241230
| _ -> evictionFails.Add 1L
242231

243-
| EvictionQueueMessage.Update entity ->
244-
entity.AccessCount <- entity.AccessCount + 1L
245-
246-
assert entity.Node.IsSome
232+
// Store updates are not synchronized. It is possible the entity is no longer in the queue.
233+
| EvictionQueueMessage.Update entity when isNull entity.Node.List -> ()
247234

248-
let node = entity.Node.Value
249-
assert (node.List = evictionQueue)
235+
| EvictionQueueMessage.Update entity ->
250236
// Just move this node to the end of the list.
251-
evictionQueue.Remove(node)
252-
evictionQueue.AddLast(node)
237+
evictionQueue.Remove(entity.Node)
238+
evictionQueue.AddLast(entity.Node)
253239

254240
return! processNext ()
255241
}
@@ -264,25 +250,27 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality> internal (totalC
264250

265251
member _.TryGetValue(key: 'Key, value: outref<'Value>) =
266252
match store.TryGetValue(key) with
267-
| true, cachedEntity ->
253+
| true, entity ->
268254
hits.Add 1L
269-
evictionProcessor.Post(EvictionQueueMessage.Update cachedEntity)
270-
value <- cachedEntity.Value
255+
evictionProcessor.Post(EvictionQueueMessage.Update entity)
256+
value <- entity.Value
271257
true
272258
| _ ->
273259
misses.Add 1L
274260
value <- Unchecked.defaultof<'Value>
275261
false
276262

277263
member _.TryAdd(key: 'Key, value: 'Value) =
278-
let cachedEntity = pool.Acquire(key, value)
264+
let entity = pool.Acquire(key, value)
279265

280-
if store.TryAdd(key, cachedEntity) then
281-
evictionProcessor.Post(EvictionQueueMessage.Add cachedEntity)
282-
true
266+
let added = store.TryAdd(key, entity)
267+
268+
if added then
269+
evictionProcessor.Post(EvictionQueueMessage.Add entity)
283270
else
284-
pool.Reclaim(cachedEntity)
285-
false
271+
pool.Reclaim(entity)
272+
273+
added
286274

287275
interface IDisposable with
288276
member this.Dispose() =

0 commit comments

Comments
 (0)