88
99namespace Lightweight . Caching . Lru
1010{
11+ /// <summary>
12+ /// LRU implementation where LRU list is composed of 3 segments: hot, warm and cold. Cost of maintaining
13+ /// segments is amortized across requests. Items are only cycled when capacity is exceeded. Pure read does
14+ /// not cycle items if all segments are within capacity constraints.
15+ /// There are no global locks. On cache miss, a new item is added. Tail items in each segment are dequeued,
16+ /// examined, and are either enqueued or discarded.
17+ /// </summary>
18+ /// <remarks>
19+ /// Each segment has a capacity. When segment capacity is exceeded, items are moved as follows:
20+ /// 1. New items are added to hot, WasAccessed = false
21+ /// 2. When items are accessed, update WasAccessed = true
22+ /// 3. When items are moved WasAccessed is set to false.
23+ /// 4. When hot is full, hot tail is moved to either Warm or Cold depending on WasAccessed.
24+ /// 5. When warm is full, warm tail is moved to warm head or cold depending on WasAccessed.
25+ /// 6. When cold is full, cold tail is moved to warm head or removed from dictionary on depending on WasAccessed.
26+ /// </remarks>
1127 public class TemplateConcurrentLru < K , V , I , P , H > : ICache < K , V >
1228 where I : LruItem < K , V >
1329 where P : struct , IPolicy < K , V , I >
@@ -56,7 +72,7 @@ public TemplateConcurrentLru(
5672 this . hitCounter = hitCounter ;
5773 }
5874
59- public int Count => this . hotCount + this . warmCount + this . coldCount ;
75+ public int Count => this . dictionary . Count ;
6076
6177 public int HotCount => this . hotCount ;
6278
@@ -103,7 +119,7 @@ public V GetOrAdd(K key, Func<K, V> valueFactory)
103119 {
104120 this . hotQueue . Enqueue ( newItem ) ;
105121 Interlocked . Increment ( ref hotCount ) ;
106- BumpItems ( ) ;
122+ Cycle ( ) ;
107123 return newItem . Value ;
108124 }
109125
@@ -125,30 +141,51 @@ public async Task<V> GetOrAddAsync(K key, Func<K, Task<V>> valueFactory)
125141 {
126142 this . hotQueue . Enqueue ( newItem ) ;
127143 Interlocked . Increment ( ref hotCount ) ;
128- BumpItems ( ) ;
144+ Cycle ( ) ;
129145 return newItem . Value ;
130146 }
131147
132148 return await this . GetOrAddAsync ( key , valueFactory ) . ConfigureAwait ( false ) ;
133149 }
134150
135- private void BumpItems ( )
151+ public bool TryRemove ( K key )
152+ {
153+ if ( this . dictionary . TryRemove ( key , out var removedItem ) )
154+ {
155+ // Mark as not accessed, it will later be cycled out of the queues because it can never be fetched
156+ // from the dictionary. Note: Hot/Warm/Cold count will reflect the removed item until it is cycled
157+ // from the queue.
158+ removedItem . WasAccessed = false ;
159+
160+ if ( removedItem . Value is IDisposable d )
161+ {
162+ d . Dispose ( ) ;
163+ }
164+
165+ return true ;
166+ }
167+
168+ return false ;
169+ }
170+
171+ private void Cycle ( )
136172 {
137173 // There will be races when queue count == queue capacity. Two threads may each dequeue items.
138- // This will prematurely free slots for the next caller. Each thread will still only bump at most 5 items.
174+ // This will prematurely free slots for the next caller. Each thread will still only cycle at most 5 items.
139175 // Since TryDequeue is thread safe, only 1 thread can dequeue each item. Thus counts and queue state will always
140176 // converge on correct over time.
141- BumpHot ( ) ;
177+ CycleHot ( ) ;
178+
142179 // Multi-threaded stress tests show that due to races, the warm and cold count can increase beyond capacity when
143- // hit rate is very high. Double bump results in stable count under all conditions. When contention is low,
144- // secondary bumps have no effect.
145- BumpWarm ( ) ;
146- BumpWarm ( ) ;
147- BumpCold ( ) ;
148- BumpCold ( ) ;
180+ // hit rate is very high. Double cycle results in stable count under all conditions. When contention is low,
181+ // secondary cycles have no effect.
182+ CycleWarm ( ) ;
183+ CycleWarm ( ) ;
184+ CycleCold ( ) ;
185+ CycleCold ( ) ;
149186 }
150187
151- private void BumpHot ( )
188+ private void CycleHot ( )
152189 {
153190 if ( this . hotCount > this . hotCapacity )
154191 {
@@ -166,7 +203,7 @@ private void BumpHot()
166203 }
167204 }
168205
169- private void BumpWarm ( )
206+ private void CycleWarm ( )
170207 {
171208 if ( this . warmCount > this . warmCapacity )
172209 {
@@ -195,7 +232,7 @@ private void BumpWarm()
195232 }
196233 }
197234
198- private void BumpCold ( )
235+ private void CycleCold ( )
199236 {
200237 if ( this . coldCount > this . coldCapacity )
201238 {
0 commit comments