@@ -24,24 +24,28 @@ type CacheOptions =
2424 HeadroomPercentage = 50
2525 }
2626
27+ // It is important that this is not a struct, because LinkedListNode holds a reference to it,
28+ // and it holds the reference to that Node, in a circular way.
2729[<Sealed; NoComparison; NoEquality>]
2830[<DebuggerDisplay( " {ToString()}" ) >]
2931type CachedEntity < 'Key , 'Value > =
3032 val mutable Key : 'Key
3133 val mutable Value : 'Value
3234 val mutable AccessCount : int64
33- val mutable Node : LinkedListNode < CachedEntity < 'Key , 'Value >>
35+ val mutable Node : LinkedListNode < CachedEntity < 'Key , 'Value >> voption
3436
3537 new ( key, value) =
3638 {
3739 Key = key
3840 Value = value
3941 AccessCount = 0 L
40- Node = Unchecked.defaultof <_>
42+ Node = ValueNone
4143 }
4244
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.
4347 member this.WithNode () =
44- this.Node <- LinkedListNode ( this)
48+ this.Node <- ValueSome ( LinkedListNode this)
4549 this
4650
4751 member this.ReUse ( key , value ) =
@@ -134,6 +138,8 @@ type CacheMetrics(cacheId) =
134138 observedCaches[ cacheId]. Dispose()
135139 observedCaches.TryRemove( cacheId) |> ignore
136140
141+ // Creates and after reclaiming holds entities for reuse.
142+ // More than totalCapacity can be created, but it will hold for reuse at most totalCapacity.
137143type EntityPool < 'Key , 'Value >( totalCapacity , cacheId ) =
138144 let pool = ConcurrentBag< CachedEntity< 'Key, 'Value>>()
139145 let mutable created = 0
@@ -148,6 +154,8 @@ type EntityPool<'Key, 'Value>(totalCapacity, cacheId) =
148154 if Interlocked.Increment & created > totalCapacity then
149155 overCapacity.Add 1 L
150156
157+ // Associate a LinkedListNode with freshly created entity.
158+ // This is a one time initialization.
151159 CachedEntity( key, value) .WithNode()
152160
153161 member _.Reclaim ( entity : CachedEntity < 'Key , 'Value >) =
@@ -169,10 +177,14 @@ module Cache =
169177 | NonNull _ when capacity > 1024 -> 1024
170178 | _ -> capacity
171179
180+ [<Struct>]
181+ type EvictionQueueMessage < 'Key , 'Value > =
182+ | Add of CachedEntity < 'Key , 'Value >
183+ | Update of CachedEntity < 'Key , 'Value >
184+
172185[<Sealed; NoComparison; NoEquality>]
173186[<DebuggerDisplay( " {GetStats()}" ) >]
174- type Cache < 'Key , 'Value when 'Key: not null and 'Key: equality >
175- internal ( totalCapacity, headroom, cts: CancellationTokenSource, ?name, ?observeMetrics) =
187+ type Cache < 'Key , 'Value when 'Key: not null and 'Key: equality > internal ( totalCapacity , headroom , ? name , ? observeMetrics ) =
176188
177189 let instanceId = defaultArg name ( Guid.NewGuid() .ToString())
178190
@@ -197,75 +209,61 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality>
197209
198210 let evictionQueue = LinkedList< CachedEntity< 'Key, 'Value>>()
199211
200- let addToEvictionQueue ( entity : CachedEntity < 'Key , 'Value >) =
201- lock evictionQueue <| fun () -> evictionQueue.AddLast( entity.Node)
202-
203- // Only LRU currrently. We can add other strategies when needed.
204- let updateEvictionQueue ( entity : CachedEntity < 'Key , 'Value >) =
205- lock evictionQueue
206- <| fun () ->
207-
208- let node = entity.Node
209-
210- // Sync between store and the eviction queue is not atomic. It might be already evicted or not yet added.
211- if node.List = evictionQueue then
212- // Just move this node to the end of the list.
213- evictionQueue.Remove( node)
214- evictionQueue.AddLast( node)
215-
216- let tryEvictOne () =
217- match evictionQueue.First with
218- | null -> evictionFails.Add 1 L
219- | first ->
220- match store.TryRemove( first.Value.Key) with
221- | true , removed ->
222- lock evictionQueue <| fun () -> evictionQueue.Remove( first)
223- pool.Reclaim( removed)
224- evictions.Add 1 L
225- | _ -> evictionFails.Add 1 L
226-
227212 // Non-evictable capacity.
228213 let capacity = totalCapacity - headroom
229214
230- let backgroundEvictionComplete = Event<_>()
215+ let evicted = Event<_>()
231216
232- let evictItems () =
233- while store.Count > capacity - headroom && evictionQueue.Count > 0 do
234- tryEvictOne ()
217+ let evictionProcessor =
218+ new MailboxProcessor< EvictionQueueMessage<_, _>>( fun mb ->
219+ let rec processNext () =
220+ async {
221+ match ! mb.Receive() with
222+ | EvictionQueueMessage.Add entity ->
235223
236- backgroundEvictionComplete.Trigger ()
224+ assert entity.Node.IsSome
237225
238- let rec backgroundEviction () =
239- async {
240- let utilization = ( float store.Count / float totalCapacity)
241- // So, based on utilization this will scale the delay between 0 and 1 seconds.
242- // Worst case scenario would be when 1 second delay happens,
243- // if the cache will grow rapidly (or in bursts), it will go beyond the maximum capacity.
244- // In this case underlying dictionary will resize, AND we will have to evict items, which will likely be slow.
245- // In this case, cache stats should be used to adjust MaximumCapacity and PercentageToEvict.
246- let delay = 1000.0 - ( 1000.0 * utilization)
226+ evictionQueue.AddLast( entity.Node.Value)
247227
248- if delay > 0.0 then
249- do ! Async.Sleep( int delay)
228+ // Evict one immediately if necessary.
229+ if evictionQueue.Count > capacity then
230+ let first = nonNull evictionQueue.First
250231
251- if store.Count > capacity then
252- evictItems ()
232+ match store.TryRemove( first.Value.Key) with
233+ | true , removed ->
234+ evictionQueue.Remove( first)
235+ pool.Reclaim( removed)
236+ evictions.Add 1 L
237+ evicted.Trigger()
238+ | _ -> evictionFails.Add 1 L
253239
254- return ! backgroundEviction ()
255- }
240+ | EvictionQueueMessage.Update entity ->
241+ entity.AccessCount <- entity.AccessCount + 1 L
242+
243+ assert entity.Node.IsSome
256244
257- do Async.Start( backgroundEviction (), cancellationToken = cts.Token)
245+ let node = entity.Node.Value
246+ assert ( node.List = evictionQueue)
247+ // Just move this node to the end of the list.
248+ evictionQueue.Remove( node)
249+ evictionQueue.AddLast( node)
258250
259- member val BackgroundEvictionComplete = backgroundEvictionComplete.Publish
251+ do ! processNext ()
252+ }
253+
254+ processNext ())
255+
256+ do evictionProcessor.Start()
257+
258+ member val Evicted = evicted.Publish
260259
261260 member val Name = instanceId
262261
263262 member _.TryGetValue ( key : 'Key , value : outref < 'Value >) =
264263 match store.TryGetValue( key) with
265264 | true , cachedEntity ->
266265 hits.Add 1 L
267- Interlocked.Increment(& cachedEntity.AccessCount) |> ignore
268- updateEvictionQueue cachedEntity
266+ evictionProcessor.Post( EvictionQueueMessage.Update cachedEntity)
269267 value <- cachedEntity.Value
270268 true
271269 | _ ->
@@ -277,16 +275,16 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality>
277275 let cachedEntity = pool.Acquire( key, value)
278276
279277 if store.TryAdd( key, cachedEntity) then
280- addToEvictionQueue cachedEntity
278+ evictionProcessor.Post ( EvictionQueueMessage.Add cachedEntity)
281279 true
282280 else
283281 pool.Reclaim( cachedEntity)
284282 false
285283
286284 interface IDisposable with
287285 member this.Dispose () =
286+ evictionProcessor.Dispose()
288287 store.Clear()
289- cts.Cancel()
290288
291289 if observeMetrics then
292290 CacheMetrics.RemoveInstrumentation instanceId
@@ -307,9 +305,7 @@ type Cache<'Key, 'Value when 'Key: not null and 'Key: equality>
307305 let headroom =
308306 int ( float options.TotalCapacity * float options.HeadroomPercentage / 100.0 )
309307
310- let cts = new CancellationTokenSource()
311-
312308 let cache =
313- new Cache<_, _>( totalCapacity, headroom, cts , ?name = name, ?observeMetrics = observeMetrics)
309+ new Cache<_, _>( totalCapacity, headroom, ?name = name, ?observeMetrics = observeMetrics)
314310
315311 cache
0 commit comments