Skip to content

Commit

Permalink
seqlock
Browse files Browse the repository at this point in the history
  • Loading branch information
Alex Peck committed Aug 31, 2024
1 parent 791242a commit c60366d
Show file tree
Hide file tree
Showing 6 changed files with 258 additions and 69 deletions.
45 changes: 45 additions & 0 deletions BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuSoakTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
using BitFaster.Caching.Buffers;
using BitFaster.Caching.Lfu;
Expand Down Expand Up @@ -316,6 +317,50 @@ public async Task WhenConcurrentUpdateAndRemoveKvp()
await removal;
}

// This test will run forever if there is a live lock.
// Since the cache bookkeeping has some overhead, it is harder to provoke
// spinning inside the reader thread compared to LruItemSoakTests.DetectTornStruct.
[Theory]
[Repeat(10)]
public async Task WhenValueIsBigStructNoLiveLock(int _)
{
using var source = new CancellationTokenSource();
var started = new TaskCompletionSource<bool>();
var cache = new ConcurrentLfu<int, Guid>(1, 20, new BackgroundThreadScheduler(), EqualityComparer<int>.Default);

var setTask = Task.Run(() => Setter(cache, source.Token, started));
await started.Task;
Checker(cache, source);

await setTask;
}

private void Setter(ICache<int, Guid> cache, CancellationToken cancelToken, TaskCompletionSource<bool> started)
{
started.SetResult(true);

while (true)
{
cache.AddOrUpdate(1, Guid.NewGuid());
cache.AddOrUpdate(1, Guid.NewGuid());

if (cancelToken.IsCancellationRequested)
{
return;
}
}
}

private void Checker(ICache<int, Guid> cache,CancellationTokenSource source)
{
for (int count = 0; count < 100_000; ++count)
{
cache.TryGet(1, out _);
}

source.Cancel();
}

private ConcurrentLfu<int, string> CreateWithBackgroundScheduler()
{
var scheduler = new BackgroundThreadScheduler();
Expand Down
13 changes: 13 additions & 0 deletions BitFaster.Caching.UnitTests/Lfu/ConcurrentLfuTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -600,6 +600,19 @@ public void WhenItemIsUpdatedItIsUpdated()
value.Should().Be(2);
}

[Fact]
public void WhenKeyExistsAddOrUpdateGuidUpdatesExistingItem()
{
var lfu2 = new ConcurrentLfu<int, Guid>(1, 40, new BackgroundThreadScheduler(), EqualityComparer<int>.Default);

var b = new byte[8];
lfu2.AddOrUpdate(1, new Guid(1, 0, 0, b));
lfu2.AddOrUpdate(1, new Guid(2, 0, 0, b));

lfu2.TryGet(1, out var value).Should().BeTrue();
value.Should().Be(new Guid(2, 0, 0, b));
}

[Fact]
public void WhenItemDoesNotExistUpdatedAddsItem()
{
Expand Down
65 changes: 65 additions & 0 deletions BitFaster.Caching.UnitTests/Lfu/LfuNodeSoakTest.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using BitFaster.Caching.Lfu;
using Xunit;
using static BitFaster.Caching.UnitTests.Lru.LruItemSoakTests;

namespace BitFaster.Caching.UnitTests.Lfu
{
[Collection("Soak")]
public class LfuNodeSoakTest
{
private const int soakIterations = 3;
private readonly LfuNode<int, MassiveStruct> item = new(1, MassiveStruct.A);

// Adapted from
// https://stackoverflow.com/questions/23262513/reproduce-torn-reads-of-decimal-in-c-sharp
[Theory]
[Repeat(soakIterations)]
public async Task DetectTornStruct(int _)
{
using var source = new CancellationTokenSource();
var started = new TaskCompletionSource<bool>();

var setTask = Task.Run(() => Setter(source.Token, started));
await started.Task;
Checker(source);

await setTask;
}

private void Setter(CancellationToken cancelToken, TaskCompletionSource<bool> started)
{
started.SetResult(true);

while (true)
{
item.SeqLockWrite(MassiveStruct.A);
item.SeqLockWrite(MassiveStruct.B);

if (cancelToken.IsCancellationRequested)
{
return;
}
}
}

private void Checker(CancellationTokenSource source)
{
// On my machine, without SeqLock, this consistently fails below 100 iterations
// on debug build, and below 1000 on release build
for (int count = 0; count < 10_000; ++count)
{
var t = item.SeqLockRead();

if (t != MassiveStruct.A && t != MassiveStruct.B)
{
throw new Exception($"Value is torn after {count} iterations");
}
}

source.Cancel();
}
}
}
128 changes: 64 additions & 64 deletions BitFaster.Caching.UnitTests/Lfu/NodeMemoryLayoutDumps.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,77 +15,77 @@ public NodeMemoryLayoutDumps(ITestOutputHelper testOutputHelper)
}

//Type layout for 'AccessOrderNode`2'
//Size: 48 bytes.Paddings: 2 bytes(%4 of empty space)
//|====================================================|
//| Object Header(8 bytes) |
//|----------------------------------------------------|
//| Method Table Ptr(8 bytes) |
//|====================================================|
//| 0-7: LfuNodeList`2 list(8 bytes) |
//|----------------------------------------------------|
//| 8-15: LfuNode`2 next(8 bytes) |
//|----------------------------------------------------|
//| 16-23: LfuNode`2 prev(8 bytes) |
//|----------------------------------------------------|
//| 24-31: Object Key(8 bytes) |
//|----------------------------------------------------|
//| 32-39: Object<Value> k__BackingField(8 bytes) |
//|----------------------------------------------------|
//| 40-43: Position<Position> k__BackingField(4 bytes) |
//| |===============================| |
//| | 0-3: Int32 value__(4 bytes) | |
//| |===============================| |
//|----------------------------------------------------|
//| 44: Boolean wasRemoved(1 byte) |
//|----------------------------------------------------|
//| 45: Boolean wasDeleted(1 byte) |
//|----------------------------------------------------|
//| 46-47: padding(2 bytes) |
//|====================================================|
//Size: 48 bytes. Paddings: 0 bytes (%0 of empty space)
//|=====================================================|
//| Object Header (8 bytes) |
//|-----------------------------------------------------|
//| Method Table Ptr (8 bytes) |
//|=====================================================|
//| 0-7: Object data (8 bytes) |
//|-----------------------------------------------------|
//| 8-15: LfuNodeList`2 list (8 bytes) |
//|-----------------------------------------------------|
//| 16-23: LfuNode`2 next (8 bytes) |
//|-----------------------------------------------------|
//| 24-31: LfuNode`2 prev (8 bytes) |
//|-----------------------------------------------------|
//| 32-39: Object Key (8 bytes) |
//|-----------------------------------------------------|
//| 40-43: Int32 sequence (4 bytes) |
//|-----------------------------------------------------|
//| 44-45: Position <Position>k__BackingField (2 bytes) |
//| |================================| |
//| | 0-1: Int16 value__ (2 bytes) | |
//| |================================| |
//|-----------------------------------------------------|
//| 46: Boolean wasRemoved (1 byte) |
//|-----------------------------------------------------|
//| 47: Boolean wasDeleted (1 byte) |
//|=====================================================|
[Fact]
public void DumpAccessOrderNode()
{
var layout = TypeLayout.GetLayout<AccessOrderNode<object, object>>(includePaddings: true);
testOutputHelper.WriteLine(layout.ToString());
}

//Type layout for 'TimeOrderNode`2'
//Size: 72 bytes.Paddings: 2 bytes(%2 of empty space)
//|====================================================|
//| Object Header(8 bytes) |
//|----------------------------------------------------|
//| Method Table Ptr(8 bytes) |
//|====================================================|
//| 0-7: LfuNodeList`2 list(8 bytes) |
//|----------------------------------------------------|
//| 8-15: LfuNode`2 next(8 bytes) |
//|----------------------------------------------------|
//| 16-23: LfuNode`2 prev(8 bytes) |
//|----------------------------------------------------|
//| 24-31: Object Key(8 bytes) |
//|----------------------------------------------------|
//| 32-39: Object<Value> k__BackingField(8 bytes) |
//|----------------------------------------------------|
//| 40-43: Position<Position> k__BackingField(4 bytes) |
//| |===============================| |
//| | 0-3: Int32 value__(4 bytes) | |
//| |===============================| |
//|----------------------------------------------------|
//| 44: Boolean wasRemoved(1 byte) |
//|----------------------------------------------------|
//| 45: Boolean wasDeleted(1 byte) |
//|----------------------------------------------------|
//| 46-47: padding(2 bytes) |
//|----------------------------------------------------|
//| 48-55: TimeOrderNode`2 prevTime(8 bytes) |
//|----------------------------------------------------|
//| 56-63: TimeOrderNode`2 nextTime(8 bytes) |
//|----------------------------------------------------|
//| 64-71: Duration timeToExpire(8 bytes) |
//| |===========================| |
//| | 0-7: Int64 raw(8 bytes) | |
//| |===========================| |
//|====================================================|
// Type layout for 'TimeOrderNode`2'
//Size: 72 bytes. Paddings: 0 bytes (%0 of empty space)
//|=====================================================|
//| Object Header (8 bytes) |
//|-----------------------------------------------------|
//| Method Table Ptr (8 bytes) |
//|=====================================================|
//| 0-7: Object data (8 bytes) |
//|-----------------------------------------------------|
//| 8-15: LfuNodeList`2 list (8 bytes) |
//|-----------------------------------------------------|
//| 16-23: LfuNode`2 next (8 bytes) |
//|-----------------------------------------------------|
//| 24-31: LfuNode`2 prev (8 bytes) |
//|-----------------------------------------------------|
//| 32-39: Object Key (8 bytes) |
//|-----------------------------------------------------|
//| 40-43: Int32 sequence (4 bytes) |
//|-----------------------------------------------------|
//| 44-45: Position <Position>k__BackingField (2 bytes) |
//| |================================| |
//| | 0-1: Int16 value__ (2 bytes) | |
//| |================================| |
//|-----------------------------------------------------|
//| 46: Boolean wasRemoved (1 byte) |
//|-----------------------------------------------------|
//| 47: Boolean wasDeleted (1 byte) |
//|-----------------------------------------------------|
//| 48-55: TimeOrderNode`2 prevTime (8 bytes) |
//|-----------------------------------------------------|
//| 56-63: TimeOrderNode`2 nextTime (8 bytes) |
//|-----------------------------------------------------|
//| 64-71: Duration timeToExpire (8 bytes) |
//| |============================| |
//| | 0-7: Int64 raw (8 bytes) | |
//| |============================| |
//|=====================================================|
[Fact]
public void DumpTimeOrderNode()
{
Expand Down
2 changes: 0 additions & 2 deletions BitFaster.Caching.UnitTests/Lru/ConcurrentLruSoakTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -345,8 +345,6 @@ private void Setter(ICache<int, Guid> cache, CancellationToken cancelToken, Task

private void Checker(ICache<int, Guid> cache,CancellationTokenSource source)
{
// On my machine, without SeqLock, this consistently fails below 100 iterations
// on debug build, and below 1000 on release build
for (int count = 0; count < 100_000; ++count)
{
cache.TryGet(1, out _);
Expand Down
Loading

0 comments on commit c60366d

Please sign in to comment.