Skip to content

Commit e84e929

Browse files
authored
Implement clear (#75)
* basic impl * fix * rem cmt
1 parent b4edcbf commit e84e929

File tree

5 files changed

+182
-49
lines changed

5 files changed

+182
-49
lines changed

BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,5 +327,38 @@ public void WhenAddOrUpdateExpiresItemsTheyAreDisposed()
327327
// all other items are not disposed
328328
items.Skip(1).All(i => i.IsDisposed == false).Should().BeTrue();
329329
}
330+
331+
[Fact]
332+
public void WhenCacheIsEmptyClearIsNoOp()
333+
{
334+
lru.Clear();
335+
lru.Count.Should().Be(0);
336+
}
337+
338+
[Fact]
339+
public void WhenItemsExistClearRemovesAllItems()
340+
{
341+
lru.AddOrUpdate(1, "1");
342+
lru.AddOrUpdate(2, "2");
343+
lru.Clear();
344+
lru.Count.Should().Be(0);
345+
}
346+
347+
[Fact]
348+
public void WhenItemsAreDisposableClearDisposesItemsOnRemove()
349+
{
350+
var lruOfDisposable = new ClassicLru<int, DisposableItem>(1, 3, EqualityComparer<int>.Default);
351+
352+
var items = Enumerable.Range(1, 4).Select(i => new DisposableItem()).ToList();
353+
354+
for (int i = 0; i < 4; i++)
355+
{
356+
lruOfDisposable.AddOrUpdate(i, items[i]);
357+
}
358+
359+
lruOfDisposable.Clear();
360+
361+
items.All(i => i.IsDisposed == true).Should().BeTrue();
362+
}
330363
}
331364
}

BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -477,5 +477,45 @@ public void WhenKeyDoesNotExistAddOrUpdateMaintainsLruOrder()
477477
lru.HotCount.Should().Be(3);
478478
lru.ColdCount.Should().Be(1); // items must have been enqueued and cycled for one of them to reach the cold queue
479479
}
480+
481+
[Fact]
482+
public void WhenCacheIsEmptyClearIsNoOp()
483+
{
484+
lru.Clear();
485+
lru.Count.Should().Be(0);
486+
}
487+
488+
[Fact]
489+
public void WhenItemsExistClearRemovesAllItems()
490+
{
491+
lru.AddOrUpdate(1, "1");
492+
lru.AddOrUpdate(2, "2");
493+
494+
lru.Clear();
495+
496+
lru.Count.Should().Be(0);
497+
498+
// verify queues are purged
499+
lru.HotCount.Should().Be(0);
500+
lru.WarmCount.Should().Be(0);
501+
lru.ColdCount.Should().Be(0);
502+
}
503+
504+
[Fact]
505+
public void WhenItemsAreDisposableClearDisposesItemsOnRemove()
506+
{
507+
var lruOfDisposable = new ConcurrentLru<int, DisposableItem>(1, 6, EqualityComparer<int>.Default);
508+
509+
var items = Enumerable.Range(1, 4).Select(i => new DisposableItem()).ToList();
510+
511+
for (int i = 0; i < 4; i++)
512+
{
513+
lruOfDisposable.AddOrUpdate(i, items[i]);
514+
}
515+
516+
lruOfDisposable.Clear();
517+
518+
items.All(i => i.IsDisposed == true).Should().BeTrue();
519+
}
480520
}
481521
}

BitFaster.Caching/ICache.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,5 +62,10 @@ public interface ICache<K, V>
6262
/// <param name="key">The key of the element to update.</param>
6363
/// <param name="value">The new value.</param>
6464
void AddOrUpdate(K key, V value);
65+
66+
/// <summary>
67+
/// Removes all keys and values from the cache.
68+
/// </summary>
69+
void Clear();
6570
}
6671
}

BitFaster.Caching/Lru/ClassicLru.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -258,6 +258,19 @@ public void AddOrUpdate(K key, V value)
258258
AddOrUpdate(key, value);
259259
}
260260

261+
///<inheritdoc/>
262+
public void Clear()
263+
{
264+
// take a key snapshot
265+
var keys = this.dictionary.Keys.ToList();
266+
267+
// remove all keys in the snapshot - this correctly handles disposable values
268+
foreach (var key in keys)
269+
{
270+
TryRemove(key);
271+
}
272+
}
273+
261274
// Thead A reads x from the dictionary. Thread B adds a new item. Thread A moves x to the end. Thread B now removes the new first Node (removal is atomic on both data structures).
262275
private void LockAndMoveToEnd(LinkedListNode<LruItem> node)
263276
{

BitFaster.Caching/Lru/TemplateConcurrentLru.cs

Lines changed: 91 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,30 @@ public void AddOrUpdate(K key, V value)
259259

260260
// if both update and add failed there was a race, try again
261261
AddOrUpdate(key, value);
262+
}
263+
264+
///<inheritdoc/>
265+
public void Clear()
266+
{
267+
// take a key snapshot
268+
var keys = this.dictionary.Keys.ToList();
269+
270+
// remove all keys in the snapshot - this correctly handles disposable values
271+
foreach (var key in keys)
272+
{
273+
TryRemove(key);
274+
}
275+
276+
// At this point, dictionary is empty but queues still hold references to all values.
277+
// Cycle the queues to purge all refs. If any items were added during this process,
278+
// it is possible they might be removed as part of CycleCold. However, the dictionary
279+
// and queues will remain in a consistent state.
280+
for (int i = 0; i < keys.Count; i++)
281+
{
282+
CycleHotUnchecked();
283+
CycleWarmUnchecked();
284+
CycleColdUnchecked();
285+
}
262286
}
263287

264288
private void Cycle()
@@ -282,72 +306,90 @@ private void CycleHot()
282306
{
283307
if (this.hotCount > this.hotCapacity)
284308
{
285-
Interlocked.Decrement(ref this.hotCount);
309+
CycleHotUnchecked();
310+
}
311+
}
286312

287-
if (this.hotQueue.TryDequeue(out var item))
288-
{
289-
var where = this.policy.RouteHot(item);
290-
this.Move(item, where);
291-
}
292-
else
293-
{
294-
Interlocked.Increment(ref this.hotCount);
295-
}
313+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
314+
private void CycleHotUnchecked()
315+
{
316+
Interlocked.Decrement(ref this.hotCount);
317+
318+
if (this.hotQueue.TryDequeue(out var item))
319+
{
320+
var where = this.policy.RouteHot(item);
321+
this.Move(item, where);
322+
}
323+
else
324+
{
325+
Interlocked.Increment(ref this.hotCount);
296326
}
297327
}
298328

299329
private void CycleWarm()
300330
{
301331
if (this.warmCount > this.warmCapacity)
302332
{
303-
Interlocked.Decrement(ref this.warmCount);
304-
305-
if (this.warmQueue.TryDequeue(out var item))
306-
{
307-
var where = this.policy.RouteWarm(item);
333+
CycleWarmUnchecked();
334+
}
335+
}
308336

309-
// When the warm queue is full, we allow an overflow of 1 item before redirecting warm items to cold.
310-
// This only happens when hit rate is high, in which case we can consider all items relatively equal in
311-
// terms of which was least recently used.
312-
if (where == ItemDestination.Warm && this.warmCount <= this.warmCapacity)
313-
{
314-
this.Move(item, where);
315-
}
316-
else
317-
{
318-
this.Move(item, ItemDestination.Cold);
319-
}
320-
}
321-
else
322-
{
323-
Interlocked.Increment(ref this.warmCount);
324-
}
337+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
338+
private void CycleWarmUnchecked()
339+
{
340+
Interlocked.Decrement(ref this.warmCount);
341+
342+
if (this.warmQueue.TryDequeue(out var item))
343+
{
344+
var where = this.policy.RouteWarm(item);
345+
346+
// When the warm queue is full, we allow an overflow of 1 item before redirecting warm items to cold.
347+
// This only happens when hit rate is high, in which case we can consider all items relatively equal in
348+
// terms of which was least recently used.
349+
if (where == ItemDestination.Warm && this.warmCount <= this.warmCapacity)
350+
{
351+
this.Move(item, where);
352+
}
353+
else
354+
{
355+
this.Move(item, ItemDestination.Cold);
356+
}
357+
}
358+
else
359+
{
360+
Interlocked.Increment(ref this.warmCount);
325361
}
326362
}
327363

328364
private void CycleCold()
329365
{
330366
if (this.coldCount > this.coldCapacity)
331367
{
332-
Interlocked.Decrement(ref this.coldCount);
333-
334-
if (this.coldQueue.TryDequeue(out var item))
335-
{
336-
var where = this.policy.RouteCold(item);
368+
CycleColdUnchecked();
369+
}
370+
}
337371

338-
if (where == ItemDestination.Warm && this.warmCount <= this.warmCapacity)
339-
{
340-
this.Move(item, where);
341-
}
342-
else
343-
{
344-
this.Move(item, ItemDestination.Remove);
345-
}
346-
}
347-
else
348-
{
349-
Interlocked.Increment(ref this.coldCount);
350-
}
372+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
373+
private void CycleColdUnchecked()
374+
{
375+
Interlocked.Decrement(ref this.coldCount);
376+
377+
if (this.coldQueue.TryDequeue(out var item))
378+
{
379+
var where = this.policy.RouteCold(item);
380+
381+
if (where == ItemDestination.Warm && this.warmCount <= this.warmCapacity)
382+
{
383+
this.Move(item, where);
384+
}
385+
else
386+
{
387+
this.Move(item, ItemDestination.Remove);
388+
}
389+
}
390+
else
391+
{
392+
Interlocked.Increment(ref this.coldCount);
351393
}
352394
}
353395

0 commit comments

Comments
 (0)