Skip to content

LFU after access fails to update expiry when read buffer is not full #603

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions BitFaster.Caching.UnitTests/Lfu/ConcurrentTLfuTests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Runtime.InteropServices;
using System.Threading;
using BitFaster.Caching.Lfu;
using BitFaster.Caching.Scheduler;
using BitFaster.Caching.UnitTests.Retry;
Expand All @@ -25,6 +26,36 @@ public ConcurrentTLfuTests()
lfu = new ConcurrentTLfu<int, string>(capacity, new ExpireAfterWrite<int, string>(timeToLive));
}

// This is a scenario test to verify maintenance is run promptly after read.
[RetryFact]
public void WhenItemIsAccessedTimeToExpireIsUpdated()
{
var cache = new ConcurrentLfuBuilder<int, int>()
.WithCapacity(10)
.WithExpireAfterAccess(TimeSpan.FromSeconds(5))
.Build();

Timed.Execute(
cache,
cache =>
{
cache.AddOrUpdate(1, 1);
return cache;
},
TimeSpan.FromSeconds(4),
cache =>
{
cache.TryGet(1, out var value);
},
TimeSpan.FromSeconds(2),
cache =>
{
cache.TryGet(1, out var value).Should().BeTrue();
cache.TryGet(1, out value).Should().BeTrue();
}
);
}

[Fact]
public void ConstructAddAndRetrieveWithCustomComparerReturnsValue()
{
Expand Down
8 changes: 4 additions & 4 deletions BitFaster.Caching/Lfu/ConcurrentLfuCore.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ private bool TryGetImpl(K key, [MaybeNullWhen(false)] out V value)
{
TryScheduleDrain();
}
this.policy.OnRead(node);
value = node.Value;
return true;
}
Expand Down Expand Up @@ -366,6 +367,7 @@ public bool TryUpdate(K key, V value)
// and we will just lose ordering/hit count, but not orphan the node.
this.writeBuffer.TryAdd(node);
TryScheduleDrain();
this.policy.OnWrite(node);
return true;
}

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

policy.AdvanceTime();

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

Expand Down Expand Up @@ -601,7 +601,7 @@ private void OnAccess(N node)
break;
}

policy.OnRead(node);
policy.AfterRead(node);
}

private void OnWrite(N node)
Expand Down Expand Up @@ -650,7 +650,7 @@ private void OnWrite(N node)
break;
}

policy.OnWrite(node);
policy.AfterWrite(node);
}

private void PromoteProbation(LfuNode<K, V> node)
Expand Down
49 changes: 27 additions & 22 deletions BitFaster.Caching/Lfu/NodePolicy.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,10 @@ internal interface INodePolicy<K, V, N>
{
N Create(K key, V value);
bool IsExpired(N node);
void AdvanceTime();
void OnRead(N node);
void OnWrite(N node);
void AfterRead(N node);
void AfterWrite(N node);
void OnEvict(N node);
void ExpireEntries<P>(ref ConcurrentLfuCore<K, V, N, P> cache) where P : struct, INodePolicy<K, V, N>;
}
Expand All @@ -33,17 +34,22 @@ public bool IsExpired(AccessOrderNode<K, V> node)
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AdvanceTime()
public void OnRead(AccessOrderNode<K, V> node)
{
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void OnRead(AccessOrderNode<K, V> node)
public void OnWrite(AccessOrderNode<K, V> node)
{
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void OnWrite(AccessOrderNode<K, V> node)
public void AfterRead(AccessOrderNode<K, V> node)
{
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AfterWrite(AccessOrderNode<K, V> node)
{
}

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

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public bool IsExpired(TimeOrderNode<K, V> node)
{
return node.TimeToExpire < Duration.SinceEpoch();
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AdvanceTime()
{
current = Duration.SinceEpoch();
return node.TimeToExpire < current;
}

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

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void OnWrite(TimeOrderNode<K, V> node)
{
var current = Duration.SinceEpoch();
node.TimeToExpire = current + expiryCalculator.GetExpireAfterUpdate(node.Key, node.Value, node.TimeToExpire - current);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AfterRead(TimeOrderNode<K, V> node)
{
wheel.Reschedule(node);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void AfterWrite(TimeOrderNode<K, V> node)
{
// if the node is not yet scheduled, it is being created
// the time is set on create in case it is read before the buffer is processed
Expand All @@ -116,12 +126,7 @@ public void OnWrite(TimeOrderNode<K, V> node)
}
else
{
var oldTte = node.TimeToExpire;
node.TimeToExpire = current + expiryCalculator.GetExpireAfterUpdate(node.Key, node.Value, node.TimeToExpire - current);
if (oldTte.raw != node.TimeToExpire.raw)
{
wheel.Reschedule(node);
}
wheel.Reschedule(node);
}
}

Expand All @@ -134,7 +139,7 @@ public void OnEvict(TimeOrderNode<K, V> node)
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void ExpireEntries<P>(ref ConcurrentLfuCore<K, V, TimeOrderNode<K, V>, P> cache) where P : struct, INodePolicy<K, V, TimeOrderNode<K, V>>
{
wheel.Advance(ref cache, current);
wheel.Advance(ref cache, Duration.SinceEpoch());
}
}
}
Loading