Skip to content

Commit

Permalink
Fixed #251
Browse files Browse the repository at this point in the history
  • Loading branch information
sakno committed Oct 16, 2024
1 parent cfe9bc3 commit 38d7d08
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 26 deletions.
27 changes: 3 additions & 24 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,30 +46,9 @@ All these things are implemented in 100% managed code on top of existing .NET AP
# What's new
Release Date: 10-13-2024

<a href="https://www.nuget.org/packages/dotnext/5.14.0">DotNext 5.14.0</a>
* Added helpers to `DelegateHelpers` class to convert delegates with synchronous signature to their asynchronous counterparts
* Added support of async enumerator to `SingletonList<T>`
* Fixed exception propagation in `DynamicTaskAwaitable`
* Added support of [ConfigureAwaitOptions](https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.configureawaitoptions) to `DynamicTaskAwaitable`

<a href="https://www.nuget.org/packages/dotnext.metaprogramming/5.14.0">DotNext.Metaprogramming 5.14.0</a>
* Updated dependencies

<a href="https://www.nuget.org/packages/dotnext.unsafe/5.14.0">DotNext.Unsafe 5.14.0</a>
* Updated dependencies

<a href="https://www.nuget.org/packages/dotnext.threading/5.14.0">DotNext.Threading 5.14.0</a>
* Updated dependencies

<a href="https://www.nuget.org/packages/dotnext.io/5.14.0">DotNext.IO 5.14.0</a>
* Updated dependencies

<a href="https://www.nuget.org/packages/dotnext.net.cluster/5.14.0">DotNext.Net.Cluster 5.14.0</a>
* Fixed graceful shutdown of Raft TCP listener
* Updated vulnerable dependencies

<a href="https://www.nuget.org/packages/dotnext.aspnetcore.cluster/5.14.0">DotNext.AspNetCore.Cluster 5.14.0</a>
* Updated vulnerable dependencies
<a href="https://www.nuget.org/packages/dotnext.threading/5.14.0">DotNext.Threading 5.15.0</a>
* Added support of synchronous lock acquisition to `AsyncExclusiveLock`, `AsyncReaderWriterLock`, `AsyncManualResetEvent`, `AsyncAutoResetEvent` so the users can easily migrate step-by-step from monitors and other synchronization primitives to async-friendly primitives
* Fixed random `InvalidOperationException` caused by `RandomAccessCache<TKey, TValue>`

Changelog for previous versions located [here](./CHANGELOG.md).

Expand Down
39 changes: 37 additions & 2 deletions src/DotNext.Tests/Runtime/Caching/RandomAccessCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ static async Task RequestLoop(RandomAccessCache<long, string> cache)
}

[Fact]
public static async Task AddRemove()
public static async Task AddRemoveAsync()
{
await using var cache = new RandomAccessCache<long, string>(15);

Expand All @@ -122,9 +122,29 @@ public static async Task AddRemove()
Equal("10", session.Value);
}
}

[Fact]
public static void AddRemove()
{
using var cache = new RandomAccessCache<long, string>(15);

using (var writeSession = cache.Change(10L, DefaultTimeout))
{
False(writeSession.TryGetValue(out _));
writeSession.SetValue("10");
}

False(cache.TryRemove(11L, DefaultTimeout, out _));
True(cache.TryRemove(10L, DefaultTimeout, out var session));

using (session)
{
Equal("10", session.Value);
}
}

[Fact]
public static async Task AddInvalidate()
public static async Task AddInvalidateAsync()
{
await using var cache = new RandomAccessCache<long, string>(15);

Expand All @@ -137,6 +157,21 @@ public static async Task AddInvalidate()
False(await cache.InvalidateAsync(11L));
True(await cache.InvalidateAsync(10L));
}

[Fact]
public static void AddInvalidate()
{
using var cache = new RandomAccessCache<long, string>(15);

using (var session = cache.Change(10L, DefaultTimeout))
{
False(session.TryGetValue(out _));
session.SetValue("10");
}

False(cache.Invalidate(11L, DefaultTimeout));
True(cache.Invalidate(10L, DefaultTimeout));
}

[Fact]
public static async Task AddTwice()
Expand Down
101 changes: 101 additions & 0 deletions src/DotNext.Threading/Runtime/Caching/RandomAccessCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,42 @@ public async ValueTask<ReadOrWriteSession> ChangeAsync(TKey key, CancellationTok
}
}

/// <summary>
/// Opens a session synchronously that can be used to modify the value associated with the key.
/// </summary>
/// <remarks>
/// The cache guarantees that the value cannot be evicted concurrently with the returned session. However,
/// the value can be evicted immediately after. The caller must dispose session.
/// </remarks>
/// <param name="key">The key of the cache record.</param>
/// <param name="timeout">The time to wait for the cache lock.</param>
/// <returns>The session that can be used to read or modify the cache record.</returns>
/// <exception cref="TimeoutException">The internal lock cannot be acquired in timely manner.</exception>
/// <exception cref="ObjectDisposedException">The cache is disposed.</exception>
public ReadOrWriteSession Change(TKey key, TimeSpan timeout)
{
var keyComparerCopy = KeyComparer;
var hashCode = keyComparerCopy?.GetHashCode(key) ?? EqualityComparer<TKey>.Default.GetHashCode(key);
var bucket = GetBucket(hashCode);

bool lockTaken;
if (!(lockTaken = bucket.TryAcquire(timeout)))
throw new TimeoutException();
try
{
if (bucket.Modify(keyComparerCopy, key, hashCode) is { } valueHolder)
return new(this, valueHolder);

lockTaken = false;
return new(this, bucket, key, hashCode);
}
finally
{
if (lockTaken)
bucket.Release();
}
}

/// <summary>
/// Tries to read the cached record.
/// </summary>
Expand Down Expand Up @@ -186,6 +222,37 @@ public bool TryRead(TKey key, out ReadSession session)
bucket.Release();
}
}

/// <summary>
/// Tries to invalidate cache record associated with the provided key synchronously.
/// </summary>
/// <param name="key">The key of the cache record to be removed.</param>
/// <param name="timeout"></param>
/// <param name="session">The session that can be used to read the removed cache record.</param>
/// <returns><see langword="true"/> if the record associated with <paramref name="key"/> exists; otherwise, <see langword="false"/>.</returns>
/// <exception cref="TimeoutException">The internal lock cannot be acquired in timely manner.</exception>
/// <exception cref="ObjectDisposedException">The cache is disposed.</exception>
public bool TryRemove(TKey key, TimeSpan timeout, out ReadSession session)
{
var keyComparerCopy = KeyComparer;
var hashCode = keyComparerCopy?.GetHashCode(key) ?? EqualityComparer<TKey>.Default.GetHashCode(key);
var bucket = GetBucket(hashCode);

if (!bucket.TryAcquire(timeout))
throw new TimeoutException();
try
{
session = bucket.TryRemove(keyComparerCopy, key, hashCode) is { } removedPair
? new(Eviction, removedPair)
: default;
}
finally
{
bucket.Release();
}

return session.IsValid;
}

/// <summary>
/// Invalidates the cache record associated with the specified key.
Expand Down Expand Up @@ -240,6 +307,38 @@ public async ValueTask<bool> InvalidateAsync(TKey key, CancellationToken token =

return true;
}

public bool Invalidate(TKey key, TimeSpan timeout)
{
var keyComparerCopy = KeyComparer;
var hashCode = keyComparerCopy?.GetHashCode(key) ?? EqualityComparer<TKey>.Default.GetHashCode(key);
var bucket = GetBucket(hashCode);

KeyValuePair? removedPair;
if (!bucket.TryAcquire(timeout))
throw new TimeoutException();
try
{
removedPair = bucket.TryRemove(keyComparerCopy, key, hashCode);
}
finally
{
bucket.Release();
}

if (removedPair is null)
{
return false;
}

if (removedPair.ReleaseCounter() is false)
{
Eviction?.Invoke(key, GetValue(removedPair));
ClearValue(removedPair);
}

return true;
}

/// <summary>
/// Invalidates the entire cache.
Expand Down Expand Up @@ -355,6 +454,8 @@ internal ReadSession(Action<TKey, TValue>? eviction, KeyValuePair valueHolder)
this.valueHolder = valueHolder;
}

internal bool IsValid => valueHolder is not null;

/// <summary>
/// Gets the value of the cache record.
/// </summary>
Expand Down

0 comments on commit 38d7d08

Please sign in to comment.