Skip to content

Commit 85a14e4

Browse files
authored
LFU after access fails to update expiry when read buffer is not full (#603)
* quick fix * proper test * update time on read * rem delayable ---------
1 parent 07f8a1c commit 85a14e4

File tree

3 files changed

+62
-26
lines changed

3 files changed

+62
-26
lines changed

BitFaster.Caching.UnitTests/Lfu/ConcurrentTLfuTests.cs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Runtime.InteropServices;
3+
using System.Threading;
34
using BitFaster.Caching.Lfu;
45
using BitFaster.Caching.Scheduler;
56
using BitFaster.Caching.UnitTests.Retry;
@@ -25,6 +26,36 @@ public ConcurrentTLfuTests()
2526
lfu = new ConcurrentTLfu<int, string>(capacity, new ExpireAfterWrite<int, string>(timeToLive));
2627
}
2728

29+
// This is a scenario test to verify maintenance is run promptly after read.
30+
[RetryFact]
31+
public void WhenItemIsAccessedTimeToExpireIsUpdated()
32+
{
33+
var cache = new ConcurrentLfuBuilder<int, int>()
34+
.WithCapacity(10)
35+
.WithExpireAfterAccess(TimeSpan.FromSeconds(5))
36+
.Build();
37+
38+
Timed.Execute(
39+
cache,
40+
cache =>
41+
{
42+
cache.AddOrUpdate(1, 1);
43+
return cache;
44+
},
45+
TimeSpan.FromSeconds(4),
46+
cache =>
47+
{
48+
cache.TryGet(1, out var value);
49+
},
50+
TimeSpan.FromSeconds(2),
51+
cache =>
52+
{
53+
cache.TryGet(1, out var value).Should().BeTrue();
54+
cache.TryGet(1, out value).Should().BeTrue();
55+
}
56+
);
57+
}
58+
2859
[Fact]
2960
public void ConstructAddAndRetrieveWithCustomComparerReturnsValue()
3061
{

BitFaster.Caching/Lfu/ConcurrentLfuCore.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ private bool TryGetImpl(K key, [MaybeNullWhen(false)] out V value)
276276
{
277277
TryScheduleDrain();
278278
}
279+
this.policy.OnRead(node);
279280
value = node.Value;
280281
return true;
281282
}
@@ -366,6 +367,7 @@ public bool TryUpdate(K key, V value)
366367
// and we will just lose ordering/hit count, but not orphan the node.
367368
this.writeBuffer.TryAdd(node);
368369
TryScheduleDrain();
370+
this.policy.OnWrite(node);
369371
return true;
370372
}
371373

@@ -525,8 +527,6 @@ private bool Maintenance(N? droppedWrite = null)
525527
{
526528
this.drainStatus.VolatileWrite(DrainStatus.ProcessingToIdle);
527529

528-
policy.AdvanceTime();
529-
530530
// Note: this is only Span on .NET Core 3.1+, else this is no-op and it is still an array
531531
var buffer = this.drainBuffer.AsSpanOrArray();
532532

@@ -601,7 +601,7 @@ private void OnAccess(N node)
601601
break;
602602
}
603603

604-
policy.OnRead(node);
604+
policy.AfterRead(node);
605605
}
606606

607607
private void OnWrite(N node)
@@ -650,7 +650,7 @@ private void OnWrite(N node)
650650
break;
651651
}
652652

653-
policy.OnWrite(node);
653+
policy.AfterWrite(node);
654654
}
655655

656656
private void PromoteProbation(LfuNode<K, V> node)

BitFaster.Caching/Lfu/NodePolicy.cs

Lines changed: 27 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ internal interface INodePolicy<K, V, N>
1010
{
1111
N Create(K key, V value);
1212
bool IsExpired(N node);
13-
void AdvanceTime();
1413
void OnRead(N node);
1514
void OnWrite(N node);
15+
void AfterRead(N node);
16+
void AfterWrite(N node);
1617
void OnEvict(N node);
1718
void ExpireEntries<P>(ref ConcurrentLfuCore<K, V, N, P> cache) where P : struct, INodePolicy<K, V, N>;
1819
}
@@ -33,17 +34,22 @@ public bool IsExpired(AccessOrderNode<K, V> node)
3334
}
3435

3536
[MethodImpl(MethodImplOptions.AggressiveInlining)]
36-
public void AdvanceTime()
37+
public void OnRead(AccessOrderNode<K, V> node)
3738
{
3839
}
3940

4041
[MethodImpl(MethodImplOptions.AggressiveInlining)]
41-
public void OnRead(AccessOrderNode<K, V> node)
42+
public void OnWrite(AccessOrderNode<K, V> node)
4243
{
4344
}
4445

4546
[MethodImpl(MethodImplOptions.AggressiveInlining)]
46-
public void OnWrite(AccessOrderNode<K, V> node)
47+
public void AfterRead(AccessOrderNode<K, V> node)
48+
{
49+
}
50+
51+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
52+
public void AfterWrite(AccessOrderNode<K, V> node)
4753
{
4854
}
4955

@@ -84,29 +90,33 @@ public TimeOrderNode<K, V> Create(K key, V value)
8490

8591
[MethodImpl(MethodImplOptions.AggressiveInlining)]
8692
public bool IsExpired(TimeOrderNode<K, V> node)
87-
{
88-
return node.TimeToExpire < Duration.SinceEpoch();
89-
}
90-
91-
[MethodImpl(MethodImplOptions.AggressiveInlining)]
92-
public void AdvanceTime()
9393
{
9494
current = Duration.SinceEpoch();
95+
return node.TimeToExpire < current;
9596
}
9697

9798
[MethodImpl(MethodImplOptions.AggressiveInlining)]
9899
public void OnRead(TimeOrderNode<K, V> node)
99100
{
100-
var oldTte = node.TimeToExpire;
101+
// we know IsExpired is always called immediate before OnRead, so piggyback on the current time
101102
node.TimeToExpire = current + expiryCalculator.GetExpireAfterRead(node.Key, node.Value, node.TimeToExpire - current);
102-
if (oldTte.raw != node.TimeToExpire.raw)
103-
{
104-
wheel.Reschedule(node);
105-
}
106103
}
107104

108105
[MethodImpl(MethodImplOptions.AggressiveInlining)]
109106
public void OnWrite(TimeOrderNode<K, V> node)
107+
{
108+
var current = Duration.SinceEpoch();
109+
node.TimeToExpire = current + expiryCalculator.GetExpireAfterUpdate(node.Key, node.Value, node.TimeToExpire - current);
110+
}
111+
112+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
113+
public void AfterRead(TimeOrderNode<K, V> node)
114+
{
115+
wheel.Reschedule(node);
116+
}
117+
118+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
119+
public void AfterWrite(TimeOrderNode<K, V> node)
110120
{
111121
// if the node is not yet scheduled, it is being created
112122
// the time is set on create in case it is read before the buffer is processed
@@ -116,12 +126,7 @@ public void OnWrite(TimeOrderNode<K, V> node)
116126
}
117127
else
118128
{
119-
var oldTte = node.TimeToExpire;
120-
node.TimeToExpire = current + expiryCalculator.GetExpireAfterUpdate(node.Key, node.Value, node.TimeToExpire - current);
121-
if (oldTte.raw != node.TimeToExpire.raw)
122-
{
123-
wheel.Reschedule(node);
124-
}
129+
wheel.Reschedule(node);
125130
}
126131
}
127132

@@ -134,7 +139,7 @@ public void OnEvict(TimeOrderNode<K, V> node)
134139
[MethodImpl(MethodImplOptions.AggressiveInlining)]
135140
public void ExpireEntries<P>(ref ConcurrentLfuCore<K, V, TimeOrderNode<K, V>, P> cache) where P : struct, INodePolicy<K, V, TimeOrderNode<K, V>>
136141
{
137-
wheel.Advance(ref cache, current);
142+
wheel.Advance(ref cache, Duration.SinceEpoch());
138143
}
139144
}
140145
}

0 commit comments

Comments
 (0)