From 75e9427a2a9bf3172177d2c11cf7990763be3646 Mon Sep 17 00:00:00 2001 From: Matthieu Mourisson Date: Mon, 30 Mar 2026 00:20:55 +0200 Subject: [PATCH 1/4] Create test to reproduce #40 --- LICENSE.txt | 2 +- .../Concurrent/UuidConcurrentTest.cs | 44 +++++++++++++++++++ Src/UUIDNext.Test/UUIDNext.Test.csproj | 2 +- 3 files changed, 46 insertions(+), 2 deletions(-) create mode 100644 Src/UUIDNext.Test/Concurrent/UuidConcurrentTest.cs diff --git a/LICENSE.txt b/LICENSE.txt index a0b9667..cec61f1 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,6 +1,6 @@ BSD Zero Clause License -Copyright (c) 2025 by Matthieu Mourisson (mareek@gmail.com) +Copyright (c) 2026 by Matthieu Mourisson (mareek@gmail.com) Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted. diff --git a/Src/UUIDNext.Test/Concurrent/UuidConcurrentTest.cs b/Src/UUIDNext.Test/Concurrent/UuidConcurrentTest.cs new file mode 100644 index 0000000..7688762 --- /dev/null +++ b/Src/UUIDNext.Test/Concurrent/UuidConcurrentTest.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; +using NFluent; +using UUIDNext.Tools; +using Xunit; + +namespace UUIDNext.Test.Concurrent; + +public class UuidConcurrentTest +{ + [Theory] + [InlineData(8, 16_384)] + public void SpecificDateStressTest(int threadCount, int dateCount) + { + int exceptionCount = 0; + DateTimeOffset baseDate = new(new(2000, 1, 1, 0, 0, 0, DateTimeKind.Utc)); + var threads = new Thread[threadCount]; + for (int i = 0; i < threadCount; i++) + threads[i] = new(Action); + + for (int i = 0; i < threadCount; i++) + threads[i].Start(); + + for (int i = 0; i < threadCount; i++) + threads[i].Join(); + + Check.That(exceptionCount).IsZero(); + + void Action() + { + for (int dateOffset = 0; dateOffset < dateCount; dateOffset++) + try + { + var date = baseDate.AddMinutes(dateOffset); + var uuid = UuidToolkit.CreateUuidV7FromSpecificDate(date); + } + catch (Exception) + { + Interlocked.Increment(ref exceptionCount); + return; + } + } + } +} diff --git a/Src/UUIDNext.Test/UUIDNext.Test.csproj b/Src/UUIDNext.Test/UUIDNext.Test.csproj index 200a0d5..f3747b5 100644 --- a/Src/UUIDNext.Test/UUIDNext.Test.csproj +++ b/Src/UUIDNext.Test/UUIDNext.Test.csproj @@ -2,7 +2,7 @@ false - net472;net8.0 + net472;net10.0 12 True ..\UUIDNext.snk From 60eb78151111fef9f55e0caea8b8302d831b4b0c Mon Sep 17 00:00:00 2001 From: Matthieu Mourisson Date: Tue, 31 Mar 2026 00:57:29 +0200 Subject: [PATCH 2/4] Added a lot of comment to BetterCache to ease debugging --- Src/UUIDNext/Tools/BetterCache.cs | 53 ++++++++++++++++++++++--------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/Src/UUIDNext/Tools/BetterCache.cs b/Src/UUIDNext/Tools/BetterCache.cs index 9cbcced..45dd02d 100644 --- a/Src/UUIDNext/Tools/BetterCache.cs +++ b/Src/UUIDNext/Tools/BetterCache.cs @@ -15,7 +15,7 @@ internal class BetterCache(int capacity) private readonly Dictionary _keysIndex = new(capacity); private readonly ListItem[] _items = new ListItem[capacity]; private int _firstIndex = -1; - private int _lastIindex = -1; + private int _lastIindex = capacity - 1; private int _firstAvailbleIndex = capacity - 1; public TValue AddOrUpdate(TKey key, Func addValueFactory, Func updateValueFactory) @@ -63,28 +63,44 @@ public TValue GetOrAdd(TKey key, Func factory) private void AddOnTop(TKey key, TValue value) { ListItem newItem = new(-1, key, value, _firstIndex); + + // if the cache is not full, we put the new item at the first available index if (_firstAvailbleIndex != -1) { - if (_firstIndex == -1) - _lastIindex = _firstAvailbleIndex; - else - _items[_firstIndex] = _items[_firstIndex].WithPreviousIndex(_firstAvailbleIndex); + // put the new item at the first avilable index + var newFirstIndex = _firstAvailbleIndex; + _items[newFirstIndex] = newItem; + + // if the cache is not empty, move the previous first item to the second place + if (_firstIndex != -1) + _items[_firstIndex] = _items[_firstIndex].WithPreviousIndex(newFirstIndex); - _items[_firstAvailbleIndex] = newItem; - _firstIndex = _firstAvailbleIndex; + // update the "pointers" + _firstIndex = newFirstIndex; _firstAvailbleIndex--; } else { - var lastItem = _items[_lastIindex]; - _keysIndex.Remove(lastItem.Key); - var newLastIndex = lastItem.PreviousIndex; - _items[_lastIindex] = newItem; - _items[_firstIndex] = _items[_firstIndex].WithPreviousIndex(_lastIindex); + // remove last item from cache + var itemToRemove = _items[_lastIindex]; + _keysIndex.Remove(itemToRemove.Key); + + // set the penultimate item as the last item + var newLastIndex = itemToRemove.PreviousIndex; _items[newLastIndex] = _items[newLastIndex].WithNextIndex(-1); - _firstIndex = _lastIindex; + + // move the previous first item to the second place + var newFirstIndex = _lastIindex; + _items[_firstIndex] = _items[_firstIndex].WithPreviousIndex(newFirstIndex); + + // set the new item as the first item + _items[newFirstIndex] = newItem; + + // update the "pointers" _lastIindex = newLastIndex; + _firstIndex = newFirstIndex; } + _keysIndex[key] = _firstIndex; } @@ -94,17 +110,24 @@ private void AddOnTop(TKey key, TValue value) private void MoveToTop(int index) { + // if the item is already first we've got nothing to do if (index == _firstIndex) return; var item = _items[index]; + // re remove the item from its position in the "chain" of items _items[item.PreviousIndex] = _items[item.PreviousIndex].WithNextIndex(item.NextIndex); - if (item.NextIndex != -1) + if (item.NextIndex != -1) // same thing but backward _items[item.NextIndex] = _items[item.NextIndex].WithPreviousIndex(item.PreviousIndex); + // move the previous first item to the second place _items[_firstIndex] = _items[_firstIndex].WithPreviousIndex(index); - _items[index] = item.WithNextIndex(_firstIndex).WithPreviousIndex(-1); + + // we put the item at the first place + _items[index] = new(-1, item.Key, item.Value, _firstIndex); + + // update the "pointer" _firstIndex = index; } From bd85b0e436d724735173e714238ac6c5805e8d65 Mon Sep 17 00:00:00 2001 From: Matthieu Mourisson Date: Wed, 1 Apr 2026 01:04:47 +0200 Subject: [PATCH 3/4] Fixes #40 --- ...uidConcurrentTest.cs => ConcurrentStressTest.cs} | 2 +- Src/UUIDNext.Test/Tools/BetterCacheTest.cs | 13 +++++++++++++ Src/UUIDNext/Tools/BetterCache.cs | 4 +++- 3 files changed, 17 insertions(+), 2 deletions(-) rename Src/UUIDNext.Test/Concurrent/{UuidConcurrentTest.cs => ConcurrentStressTest.cs} (97%) diff --git a/Src/UUIDNext.Test/Concurrent/UuidConcurrentTest.cs b/Src/UUIDNext.Test/Concurrent/ConcurrentStressTest.cs similarity index 97% rename from Src/UUIDNext.Test/Concurrent/UuidConcurrentTest.cs rename to Src/UUIDNext.Test/Concurrent/ConcurrentStressTest.cs index 7688762..58d4b65 100644 --- a/Src/UUIDNext.Test/Concurrent/UuidConcurrentTest.cs +++ b/Src/UUIDNext.Test/Concurrent/ConcurrentStressTest.cs @@ -6,7 +6,7 @@ namespace UUIDNext.Test.Concurrent; -public class UuidConcurrentTest +public class ConcurrentStressTest { [Theory] [InlineData(8, 16_384)] diff --git a/Src/UUIDNext.Test/Tools/BetterCacheTest.cs b/Src/UUIDNext.Test/Tools/BetterCacheTest.cs index 251a48e..78ad2be 100644 --- a/Src/UUIDNext.Test/Tools/BetterCacheTest.cs +++ b/Src/UUIDNext.Test/Tools/BetterCacheTest.cs @@ -54,4 +54,17 @@ public void EnsureAddOrUpdateMethodWorks() var first = cache.AddOrUpdate("0", _ => 0, (_, v) => v + 1); Check.That(first).Is(0); } + + [Fact] + [Trait("bug", "#40")] + public void TestLastIndexBug() + { + BetterCache cache = new(2); + cache.GetOrAdd(1, _ => 1); + cache.GetOrAdd(2, _ => 1); + + cache.GetOrAdd(1, _ => 1); + + cache.GetOrAdd(3, _ => 1); + } } diff --git a/Src/UUIDNext/Tools/BetterCache.cs b/Src/UUIDNext/Tools/BetterCache.cs index 45dd02d..492f705 100644 --- a/Src/UUIDNext/Tools/BetterCache.cs +++ b/Src/UUIDNext/Tools/BetterCache.cs @@ -127,8 +127,10 @@ private void MoveToTop(int index) // we put the item at the first place _items[index] = new(-1, item.Key, item.Value, _firstIndex); - // update the "pointer" + // update the "pointers" _firstIndex = index; + if (index == _lastIindex) + _lastIindex = item.PreviousIndex; } private readonly struct ListItem(int previousIndex, TKey key, TValue value, int nextIndex) From 916a819dc5f17eec88dba50542cf36d6147b0519 Mon Sep 17 00:00:00 2001 From: Matthieu Mourisson Date: Wed, 1 Apr 2026 01:13:19 +0200 Subject: [PATCH 4/4] Updated CI to .NET 10 --- .github/workflows/ci.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c5028d7..51930a5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,14 +22,19 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Setup .NET - uses: actions/setup-dotnet@v4 + uses: actions/setup-dotnet@v5 with: - dotnet-version: 9.0.x + dotnet-version: 10.x - name: Restore dependencies run: dotnet restore - name: Build run: dotnet build --no-restore - name: Test - run: dotnet test --no-build --verbosity normal --framework net8.0 + run: dotnet test --no-build --verbosity normal --framework net10.0 --logger "trx;LogFileName=TestResults.trx" + - name: Upload test results + uses: actions/upload-artifact@v7 + with: + name: TestResults + path: ./src/Test/TestResults/TestResults.trx \ No newline at end of file