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 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/ConcurrentStressTest.cs b/Src/UUIDNext.Test/Concurrent/ConcurrentStressTest.cs new file mode 100644 index 0000000..58d4b65 --- /dev/null +++ b/Src/UUIDNext.Test/Concurrent/ConcurrentStressTest.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading; +using NFluent; +using UUIDNext.Tools; +using Xunit; + +namespace UUIDNext.Test.Concurrent; + +public class ConcurrentStressTest +{ + [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/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.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 diff --git a/Src/UUIDNext/Tools/BetterCache.cs b/Src/UUIDNext/Tools/BetterCache.cs index 9cbcced..492f705 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,18 +110,27 @@ 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 "pointers" _firstIndex = index; + if (index == _lastIindex) + _lastIindex = item.PreviousIndex; } private readonly struct ListItem(int previousIndex, TKey key, TValue value, int nextIndex)