Skip to content
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
13 changes: 9 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion LICENSE.txt
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
44 changes: 44 additions & 0 deletions Src/UUIDNext.Test/Concurrent/ConcurrentStressTest.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}
}
}
13 changes: 13 additions & 0 deletions Src/UUIDNext.Test/Tools/BetterCacheTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<int, int> cache = new(2);
cache.GetOrAdd(1, _ => 1);
cache.GetOrAdd(2, _ => 1);

cache.GetOrAdd(1, _ => 1);

cache.GetOrAdd(3, _ => 1);
}
}
2 changes: 1 addition & 1 deletion Src/UUIDNext.Test/UUIDNext.Test.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<IsPackable>false</IsPackable>
<TargetFrameworks>net472;net8.0</TargetFrameworks>
<TargetFrameworks>net472;net10.0</TargetFrameworks>
<LangVersion>12</LangVersion>
<SignAssembly>True</SignAssembly>
<AssemblyOriginatorKeyFile>..\UUIDNext.snk</AssemblyOriginatorKeyFile>
Expand Down
55 changes: 40 additions & 15 deletions Src/UUIDNext/Tools/BetterCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ internal class BetterCache<TKey, TValue>(int capacity)
private readonly Dictionary<TKey, int> _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<TKey, TValue> addValueFactory, Func<TKey, TValue, TValue> updateValueFactory)
Expand Down Expand Up @@ -63,28 +63,44 @@ public TValue GetOrAdd(TKey key, Func<TKey, TValue> 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;
}

Expand All @@ -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)
Expand Down
Loading