Skip to content

Commit 8995ef2

Browse files
authored
Trim (#120)
* trim poc * v2 * v3 * more tests * simplify * cleanup * rem return * clean * clean * safe trim * expire and trim * test TLRU * classic trim 1 * test policies * test partial expire * cleanup * fix cold trim
1 parent 768fd7f commit 8995ef2

19 files changed

+597
-40
lines changed

BitFaster.Caching.UnitTests/Lru/ClassicLruTests.cs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -390,5 +390,62 @@ public void WhenItemsAreDisposableClearDisposesItemsOnRemove()
390390

391391
items.All(i => i.IsDisposed == true).Should().BeTrue();
392392
}
393+
394+
[Fact]
395+
public void WhenTrimCountIsZeroThrows()
396+
{
397+
lru.Invoking(l => lru.Trim(0)).Should().Throw<ArgumentOutOfRangeException>();
398+
}
399+
400+
[Fact]
401+
public void WhenTrimCountIsMoreThanCapacityThrows()
402+
{
403+
lru.Invoking(l => lru.Trim(capacity + 1)).Should().Throw<ArgumentOutOfRangeException>();
404+
}
405+
406+
[Theory]
407+
[InlineData(1, new[] { 1, 3 })]
408+
[InlineData(2, new[] { 1 })]
409+
[InlineData(3, new int[] { })]
410+
public void WhenItemsExistTrimRemovesExpectedItemCount(int trimCount, int[] expected)
411+
{
412+
// initial state:
413+
// 1, 3, 2
414+
lru.AddOrUpdate(1, "1");
415+
lru.AddOrUpdate(2, "2");
416+
lru.AddOrUpdate(3, "3");
417+
418+
lru.GetOrAdd(1, i => i.ToString());
419+
420+
lru.Trim(trimCount);
421+
422+
lru.Keys.Should().BeEquivalentTo(expected);
423+
}
424+
425+
[Fact]
426+
public void WhenCacheIsEmptyTrimIsNoOp()
427+
{
428+
lru.Trim(2);
429+
}
430+
431+
[Fact]
432+
public void WhenItemsAreDisposableTrimDisposesItems()
433+
{
434+
var lruOfDisposable = new ClassicLru<int, DisposableItem>(1, 4, EqualityComparer<int>.Default);
435+
436+
var items = Enumerable.Range(1, 4).Select(i => new DisposableItem()).ToList();
437+
438+
for (int i = 0; i < 4; i++)
439+
{
440+
lruOfDisposable.AddOrUpdate(i, items[i]);
441+
}
442+
443+
lruOfDisposable.Trim(2);
444+
445+
items[0].IsDisposed.Should().BeTrue();
446+
items[1].IsDisposed.Should().BeTrue();
447+
items[2].IsDisposed.Should().BeFalse();
448+
items[3].IsDisposed.Should().BeFalse();
449+
}
393450
}
394451
}

BitFaster.Caching.UnitTests/Lru/ConcurrentLruTests.cs

Lines changed: 217 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -404,7 +404,7 @@ public void WhenValueEvictedItemRemovedEventIsFired()
404404

405405
for (int i = 0; i < 6; i++)
406406
{
407-
lruEvents.GetOrAdd(i+1, i => i + 1);
407+
lruEvents.GetOrAdd(i + 1, i => i + 1);
408408
}
409409

410410
removedItems.Count.Should().Be(2);
@@ -461,7 +461,7 @@ public void WhenItemIsRemovedRemovedEventIsFired()
461461
var lruEvents = new ConcurrentLru<int, int>(1, 6, EqualityComparer<int>.Default);
462462
lruEvents.ItemRemoved += OnLruItemRemoved;
463463

464-
lruEvents.GetOrAdd(1, i => i+2);
464+
lruEvents.GetOrAdd(1, i => i + 2);
465465

466466
lruEvents.TryRemove(1).Should().BeTrue();
467467

@@ -581,7 +581,7 @@ public void WhenItemsExistClearRemovesAllItems()
581581
{
582582
lru.AddOrUpdate(1, "1");
583583
lru.AddOrUpdate(2, "2");
584-
584+
585585
lru.Clear();
586586

587587
lru.Count.Should().Be(0);
@@ -608,5 +608,219 @@ public void WhenItemsAreDisposableClearDisposesItemsOnRemove()
608608

609609
items.All(i => i.IsDisposed == true).Should().BeTrue();
610610
}
611+
612+
[Fact]
613+
public void WhenItemsArClearedAnEventIsFired()
614+
{
615+
var lruEvents = new ConcurrentLru<int, int>(1, 9, EqualityComparer<int>.Default);
616+
lruEvents.ItemRemoved += OnLruItemRemoved;
617+
618+
for (int i = 0; i < 6; i++)
619+
{
620+
lruEvents.GetOrAdd(i + 1, i => i + 1);
621+
}
622+
623+
lruEvents.Clear();
624+
625+
removedItems.Count.Should().Be(6);
626+
627+
for (int i = 0; i < 6; i++)
628+
{
629+
removedItems[i].Reason.Should().Be(ItemRemovedReason.Cleared);
630+
}
631+
}
632+
633+
[Fact]
634+
public void WhenTrimCountIsZeroThrows()
635+
{
636+
lru.Invoking(l => lru.Trim(0)).Should().Throw<ArgumentOutOfRangeException>();
637+
}
638+
639+
[Fact]
640+
public void WhenTrimCountIsMoreThanCapacityThrows()
641+
{
642+
lru.Invoking(l => lru.Trim(hotCap + warmCap + coldCap + 1)).Should().Throw<ArgumentOutOfRangeException>();
643+
}
644+
645+
[Theory]
646+
[InlineData(1, new[] { 9, 8, 7, 3, 2, 1, 6, 5 })]
647+
[InlineData(2, new[] { 9, 8, 7, 3, 2, 1, 6 })]
648+
[InlineData(3, new[] { 9, 8, 7, 3, 2, 1 })]
649+
[InlineData(4, new[] { 9, 8, 7, 3, 2 })]
650+
[InlineData(5, new[] { 9, 8, 7, 3 })]
651+
[InlineData(6, new[] { 9, 8, 7 })]
652+
[InlineData(7, new[] { 9, 8 })]
653+
[InlineData(8, new[] { 9 })]
654+
[InlineData(9, new int[] { })]
655+
public void WhenColdItemsExistTrimRemovesExpectedItemCount(int trimCount, int[] expected)
656+
{
657+
// initial state:
658+
// Hot = 9, 8, 7
659+
// Warm = 3, 2, 1
660+
// Cold = 6, 5, 4
661+
lru.AddOrUpdate(1, "1");
662+
lru.AddOrUpdate(2, "2");
663+
lru.AddOrUpdate(3, "3");
664+
lru.GetOrAdd(1, i => i.ToString());
665+
lru.GetOrAdd(2, i => i.ToString());
666+
lru.GetOrAdd(3, i => i.ToString());
667+
668+
lru.AddOrUpdate(4, "4");
669+
lru.AddOrUpdate(5, "5");
670+
lru.AddOrUpdate(6, "6");
671+
672+
lru.AddOrUpdate(7, "7");
673+
lru.AddOrUpdate(8, "8");
674+
lru.AddOrUpdate(9, "9");
675+
676+
lru.Trim(trimCount);
677+
678+
lru.Keys.Should().BeEquivalentTo(expected);
679+
}
680+
681+
[Theory]
682+
[InlineData(1, new[] { 6, 5, 4, 3, 2 })]
683+
[InlineData(2, new[] { 6, 5, 4, 3 })]
684+
[InlineData(3, new[] { 6, 5, 4 })]
685+
[InlineData(4, new[] { 6, 5 })]
686+
[InlineData(5, new[] { 6 })]
687+
[InlineData(6, new int[] { })]
688+
[InlineData(7, new int[] { })]
689+
[InlineData(8, new int[] { })]
690+
[InlineData(9, new int[] { })]
691+
public void WhenHotAndWarmItemsExistTrimRemovesExpectedItemCount(int itemCount, int[] expected)
692+
{
693+
// initial state:
694+
// Hot = 6, 5, 4
695+
// Warm = 3, 2, 1
696+
// Cold = -
697+
lru.AddOrUpdate(1, "1");
698+
lru.AddOrUpdate(2, "2");
699+
lru.AddOrUpdate(3, "3");
700+
lru.GetOrAdd(1, i => i.ToString());
701+
lru.GetOrAdd(2, i => i.ToString());
702+
lru.GetOrAdd(3, i => i.ToString());
703+
704+
lru.AddOrUpdate(4, "4");
705+
lru.AddOrUpdate(5, "5");
706+
lru.AddOrUpdate(6, "6");
707+
708+
lru.Trim(itemCount);
709+
710+
lru.Keys.Should().BeEquivalentTo(expected);
711+
}
712+
713+
[Theory]
714+
[InlineData(1, new[] { 3, 2 })]
715+
[InlineData(2, new[] { 3 })]
716+
[InlineData(3, new int[] { })]
717+
[InlineData(4, new int[] { })]
718+
[InlineData(5, new int[] { })]
719+
[InlineData(6, new int[] { })]
720+
[InlineData(7, new int[] { })]
721+
[InlineData(8, new int[] { })]
722+
[InlineData(9, new int[] { })]
723+
public void WhenHotItemsExistTrimRemovesExpectedItemCount(int itemCount, int[] expected)
724+
{
725+
// initial state:
726+
// Hot = 3, 2, 1
727+
// Warm = -
728+
// Cold = -
729+
lru.AddOrUpdate(1, "1");
730+
lru.AddOrUpdate(2, "2");
731+
lru.AddOrUpdate(3, "3");
732+
733+
lru.Trim(itemCount);
734+
735+
lru.Keys.Should().BeEquivalentTo(expected);
736+
}
737+
738+
[Theory]
739+
[InlineData(1, new[] { 9, 8, 7, 6, 5, 4, 3, 2 })]
740+
[InlineData(2, new[] { 9, 8, 7, 6, 5, 4, 3 })]
741+
[InlineData(3, new[] { 9, 8, 7, 6, 5, 4 })]
742+
[InlineData(4, new[] { 9, 8, 7, 6, 5 })]
743+
[InlineData(5, new[] { 9, 8, 7, 6 })]
744+
[InlineData(6, new[] { 9, 8, 7 })]
745+
[InlineData(7, new[] { 9, 8 })]
746+
[InlineData(8, new[] { 9 })]
747+
[InlineData(9, new int[] { })]
748+
public void WhenColdItemsAreTouchedTrimRemovesExpectedItemCount(int trimCount, int[] expected)
749+
{
750+
// initial state:
751+
// Hot = 9, 8, 7
752+
// Warm = 3, 2, 1
753+
// Cold = 6*, 5*, 4*
754+
lru.AddOrUpdate(1, "1");
755+
lru.AddOrUpdate(2, "2");
756+
lru.AddOrUpdate(3, "3");
757+
lru.GetOrAdd(1, i => i.ToString());
758+
lru.GetOrAdd(2, i => i.ToString());
759+
lru.GetOrAdd(3, i => i.ToString());
760+
761+
lru.AddOrUpdate(4, "4");
762+
lru.AddOrUpdate(5, "5");
763+
lru.AddOrUpdate(6, "6");
764+
765+
lru.AddOrUpdate(7, "7");
766+
lru.AddOrUpdate(8, "8");
767+
lru.AddOrUpdate(9, "9");
768+
769+
// touch all items in the cold queue
770+
lru.GetOrAdd(4, i => i.ToString());
771+
lru.GetOrAdd(5, i => i.ToString());
772+
lru.GetOrAdd(6, i => i.ToString());
773+
774+
lru.Trim(trimCount);
775+
776+
this.testOutputHelper.WriteLine("LRU " + string.Join(" ", lru.Keys));
777+
this.testOutputHelper.WriteLine("exp " + string.Join(" ", expected));
778+
779+
lru.Keys.Should().BeEquivalentTo(expected);
780+
}
781+
782+
[Fact]
783+
public void WhenItemsAreDisposableTrimDisposesItems()
784+
{
785+
var lruOfDisposable = new ConcurrentLru<int, DisposableItem>(1, 6, EqualityComparer<int>.Default);
786+
787+
var items = Enumerable.Range(1, 4).Select(i => new DisposableItem()).ToList();
788+
789+
for (int i = 0; i < 4; i++)
790+
{
791+
lruOfDisposable.AddOrUpdate(i, items[i]);
792+
}
793+
794+
lruOfDisposable.Trim(2);
795+
796+
items[0].IsDisposed.Should().BeTrue();
797+
items[1].IsDisposed.Should().BeTrue();
798+
items[2].IsDisposed.Should().BeFalse();
799+
items[3].IsDisposed.Should().BeFalse();
800+
}
801+
802+
[Fact]
803+
public void WhenItemsAreTrimmedAnEventIsFired()
804+
{
805+
var lruEvents = new ConcurrentLru<int, int>(1, 9, EqualityComparer<int>.Default);
806+
lruEvents.ItemRemoved += OnLruItemRemoved;
807+
808+
for (int i = 0; i < 6; i++)
809+
{
810+
lruEvents.GetOrAdd(i + 1, i => i + 1);
811+
}
812+
813+
lruEvents.Trim(2);
814+
815+
removedItems.Count.Should().Be(2);
816+
817+
removedItems[0].Key.Should().Be(1);
818+
removedItems[0].Value.Should().Be(2);
819+
removedItems[0].Reason.Should().Be(ItemRemovedReason.Trimmed);
820+
821+
removedItems[1].Key.Should().Be(2);
822+
removedItems[1].Value.Should().Be(3);
823+
removedItems[1].Reason.Should().Be(ItemRemovedReason.Trimmed);
824+
}
611825
}
612826
}

BitFaster.Caching.UnitTests/Lru/ConcurrentTLruTests.cs

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace BitFaster.Caching.UnitTests.Lru
1010
{
1111
public class ConcurrentTLruTests
1212
{
13-
private readonly TimeSpan timeToLive = TimeSpan.FromSeconds(1);
13+
private readonly TimeSpan timeToLive = TimeSpan.FromMilliseconds(10);
1414
private const int capacity = 9;
1515
private ConcurrentTLru<int, string> lru;
1616

@@ -100,5 +100,66 @@ public void WhenItemIsAddedThenRetrievedHitRatioIsHalf()
100100

101101
lru.HitRatio.Should().Be(0.5);
102102
}
103+
104+
[Fact]
105+
public async Task WhenItemsAreExpiredExpireRemovesExpiredItems()
106+
{
107+
lru.AddOrUpdate(1, "1");
108+
lru.AddOrUpdate(2, "2");
109+
lru.AddOrUpdate(3, "3");
110+
lru.GetOrAdd(1, valueFactory.Create);
111+
lru.GetOrAdd(2, valueFactory.Create);
112+
lru.GetOrAdd(3, valueFactory.Create);
113+
114+
lru.AddOrUpdate(4, "4");
115+
lru.AddOrUpdate(5, "5");
116+
lru.AddOrUpdate(6, "6");
117+
118+
lru.AddOrUpdate(7, "7");
119+
lru.AddOrUpdate(8, "8");
120+
lru.AddOrUpdate(9, "9");
121+
122+
await Task.Delay(timeToLive * 2);
123+
124+
lru.TrimExpired();
125+
126+
lru.Count.Should().Be(0);
127+
}
128+
129+
[Fact]
130+
public async Task WhenCacheHasExpiredAndFreshItemsExpireRemovesOnlyExpiredItems()
131+
{
132+
lru.AddOrUpdate(1, "1");
133+
lru.AddOrUpdate(2, "2");
134+
lru.AddOrUpdate(3, "3");
135+
136+
lru.AddOrUpdate(4, "4");
137+
lru.AddOrUpdate(5, "5");
138+
lru.AddOrUpdate(6, "6");
139+
140+
await Task.Delay(timeToLive * 2);
141+
142+
lru.GetOrAdd(1, valueFactory.Create);
143+
lru.GetOrAdd(2, valueFactory.Create);
144+
lru.GetOrAdd(3, valueFactory.Create);
145+
146+
lru.TrimExpired();
147+
148+
lru.Count.Should().Be(3);
149+
}
150+
151+
[Fact]
152+
public async Task WhenItemsAreExpiredTrimRemovesExpiredItems()
153+
{
154+
lru.AddOrUpdate(1, "1");
155+
lru.AddOrUpdate(2, "2");
156+
lru.AddOrUpdate(3, "3");
157+
158+
await Task.Delay(timeToLive * 2);
159+
160+
lru.Trim(1);
161+
162+
lru.Count.Should().Be(0);
163+
}
103164
}
104165
}

0 commit comments

Comments
 (0)