Skip to content

Commit

Permalink
Preliminary set of tests and fixes
Browse files Browse the repository at this point in the history
  • Loading branch information
sakno committed Jun 14, 2024
1 parent 468fb8f commit f53f8c3
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 2 deletions.
93 changes: 93 additions & 0 deletions src/DotNext.Tests/Threading/Leases/LeaseTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@

namespace DotNext.Threading.Leases;

public sealed class LeaseTests : Test
{
[Fact]
public static async Task AcquireOrRenewInitialState()
{
using var provider = new TestLeaseProvider(DefaultTimeout);
Null(await provider.TryRenewAsync(default, true));
Null(await provider.TryRenewAsync(default, false));
Null(await provider.ReleaseAsync(default));

var result = NotNull(await provider.TryAcquireOrRenewAsync(default));
True(result.State.Identity >> default(LeaseIdentity));
}

[Fact]
public static async Task Acquisition()
{
using var provider = new TestLeaseProvider(DefaultTimeout);
NotNull(await provider.TryAcquireAsync());
}

[Fact]
public static void Preceding()
{
True(default(LeaseIdentity) << new LeaseIdentity { Version = 1UL });
False(default(LeaseIdentity) >> new LeaseIdentity { Version = 1UL });
False(default(LeaseIdentity) << new LeaseIdentity { Version = 2UL });
}

[Fact]
public static async Task FightForLease()
{
using var provider = new TestLeaseProvider(DefaultTimeout);

var acquisition1 = Task.Run(async () => await provider.TryAcquireAsync());
var acquisition2 = Task.Run(async () => await provider.TryAcquireAsync());

var tasks = await Task.WhenAll(acquisition1, acquisition2);

True(tasks is [null, not null] or [not null, null]);
}

private sealed class TestLeaseProvider(TimeSpan ttl) : LeaseProvider<int>(ttl)
{
private readonly AsyncReaderWriterLock syncRoot = new();
private State currentState;

protected override async ValueTask<State> GetStateAsync(CancellationToken token)
{
await syncRoot.EnterReadLockAsync(token).ConfigureAwait(false);
try
{
return currentState;
}
finally
{
syncRoot.Release();
}
}

protected override async ValueTask<bool> TryUpdateStateAsync(State state, CancellationToken token)
{
bool result;
await syncRoot.EnterWriteLockAsync(token).ConfigureAwait(false);
try
{
if (result = currentState.Identity << state.Identity)
{
currentState = state;
}
}
finally
{
syncRoot.Release();
}

return result;
}

protected override void Dispose(bool disposing)
{
if (disposing)
{
syncRoot.Dispose();
}

base.Dispose(disposing);
}
}
}
26 changes: 26 additions & 0 deletions src/DotNext.Threading/Threading/Leases/LeaseIdentity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,32 @@ namespace DotNext.Threading.Leases;
/// </remarks>
public Guid Id { get; init; }

/// <summary>
/// Determines whether this identity immediately precedes the specified identity.
/// </summary>
/// <param name="other">The identity to compare.</param>
/// <returns><see langword="true"/> if this identity immediately precedes <paramref name="other"/>; otherwise, <see langword="false"/>.</returns>
public bool Precedes(in LeaseIdentity other)
=> other.Version - Version is 1UL && Id == other.Id;

/// <summary>
/// Determines whether identity <paramref name="x"/> immediately supersedes identity <paramref name="y"/>.
/// </summary>
/// <param name="x">The first identity to compare.</param>
/// <param name="y">The second identity to compare.</param>
/// <returns><see langword="true"/> if identity <paramref name="x"/> immediately supersedes <paramref name="y"/>; otherwise, <see langword="false"/>.</returns>
public static bool operator >>(in LeaseIdentity x, in LeaseIdentity y)
=> y.Precedes(in x);

/// <summary>
/// Determines whether identity <paramref name="x"/> immediately precedes identity <paramref name="y"/>.
/// </summary>
/// <param name="x">The first identity to compare.</param>
/// <param name="y">The second identity to compare.</param>
/// <returns><see langword="true"/> if identity <paramref name="x"/> immediately precedes <paramref name="y"/>; otherwise, <see langword="false"/>.</returns>
public static bool operator <<(in LeaseIdentity x, in LeaseIdentity y)
=> x.Precedes(in y);

private bool Equals(in LeaseIdentity other)
=> Version == other.Version && Id == other.Id;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ private readonly struct RenewalCondition(LeaseIdentity identity, bool reacquire)
bool ITransitionCondition.Invoke(in State state, TimeProvider provider, TimeSpan timeToLive, out TimeSpan remainingTime)
{
remainingTime = timeToLive;
return state.Identity == identity && (reacquire || !state.IsExpired(provider, timeToLive, out remainingTime));
return identity.Version is not LeaseIdentity.InitialVersion
&& state.Identity == identity
&& (reacquire || !state.IsExpired(provider, timeToLive, out remainingTime));
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/DotNext.Threading/Threading/Leases/LeaseProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ public readonly struct State
public required DateTimeOffset CreatedAt { get; init; }

internal bool IsExpired(TimeProvider provider, TimeSpan ttl, out TimeSpan remaining)
=> (remaining = provider.GetUtcNow() - CreatedAt) < ttl;
=> (remaining = provider.GetUtcNow() - CreatedAt) >= ttl;
}

/// <summary>
Expand Down

0 comments on commit f53f8c3

Please sign in to comment.