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
5 changes: 3 additions & 2 deletions .editorconfig
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,8 @@ csharp_style_prefer_top_level_statements = true:silent
csharp_style_prefer_tuple_swap = true:suggestion
csharp_style_prefer_utf8_string_literals = true:suggestion
csharp_style_throw_expression = true:warning
csharp_style_unused_value_assignment_preference = discard_variable:warning
csharp_style_unused_value_expression_statement_preference = discard_variable:warning
csharp_style_unused_value_assignment_preference = discard_variable:suggestion
csharp_style_unused_value_expression_statement_preference = discard_variable:suggestion

# New Lines
csharp_style_allow_embedded_statements_on_same_line_experimental = true:silent
Expand Down Expand Up @@ -321,6 +321,7 @@ dotnet_diagnostic.CA2007.severity = error # CA2007: Consider calli

[tests/**.cs]

dotnet_diagnostic.CA1062.severity = none # CA1062: Validate arguments of public methods
dotnet_diagnostic.CA1707.severity = none # CA1707: Identifiers should not contain underscores
dotnet_diagnostic.CA1724.severity = none # CA1724: Type names should not match namespaces
dotnet_diagnostic.CA1822.severity = none # CA1822: Mark members as static
Expand Down
9 changes: 8 additions & 1 deletion src/Immediate.Cache.Shared/ApplicationCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -224,9 +224,16 @@ private async Task RunHandler(CancellationTokenSource tokenSource)
{
lock (_lock)
{
// if token source has been cancelled (aka `RemoveValue` or `SetValue`)
// then finish the loop and quite or try again
if (!tokenSource.IsCancellationRequested)
{
_responseSource?.SetException(ex);
return;

// only return if we have an exception and that is the final value
// of this handler
return;
}
}
}

Expand Down
61 changes: 61 additions & 0 deletions tests/Immediate.Cache.FunctionalTests/ApplicationCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -421,4 +421,65 @@ public async Task TransformWorksWhenMultipleSimultaneous()
Assert.Equal(12, transformedResponse2.Value);
Assert.Equal(2, transformation2.TimesExecuted);
}

[Fact]
public async Task GetValueWorksWhenRepeatedCallAfterFail()
{
var query = new FailingLoad.Query { Key = "Load fails" };

var cache = _serviceProvider.GetRequiredService<FailingLoadCache>();
var waiting = cache.GetValue(query, TestContext.Current.CancellationToken);

await query.Started.Task;

query.Release.SetResult();

// caller waiting during the load
await ValidateFailingLoad(waiting);

// caller arriving afterwards
await ValidateFailingLoad(cache.GetValue(query, TestContext.Current.CancellationToken));

cache.RemoveValue(query);

// caller after another RemoveValue
await ValidateFailingLoad(cache.GetValue(query, TestContext.Current.CancellationToken));
}

[Fact]
public async Task GetValueWorksWhenRepeatedCallAfterFailAndRemove()
{
var query = new FailingLoad.Query { Key = "Load fails after RemoveValue" };

var cache = _serviceProvider.GetRequiredService<FailingLoadCache>();
var waiting = cache.GetValue(query, TestContext.Current.CancellationToken);

await query.Started.Task;

cache.RemoveValue(query);

query.Release.SetResult();

// caller waiting during the load
await ValidateFailingLoad(waiting);

// caller arriving afterwards
await ValidateFailingLoad(cache.GetValue(query, TestContext.Current.CancellationToken));

cache.RemoveValue(query);

// caller after another RemoveValue
await ValidateFailingLoad(cache.GetValue(query, TestContext.Current.CancellationToken));
}

private static async Task ValidateFailingLoad(ValueTask<string> task)
{
var ex = await Assert.ThrowsAsync<FailingLoad.FailingLoadException>(
async () => await task
.AsTask()
.WaitAsync(TimeSpan.FromMinutes(3), TestContext.Current.CancellationToken)
);

Assert.Equal(FailingLoad.FailingLoadMessage, ex.Message);
}
}
6 changes: 0 additions & 6 deletions tests/Immediate.Cache.FunctionalTests/DelayGetValueCache.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
using System.Diagnostics.CodeAnalysis;
using Immediate.Cache.Shared;

namespace Immediate.Cache.FunctionalTests;

[CacheFor<DelayGetValue>]
public sealed partial class DelayGetValueCache
{
[SuppressMessage(
"Design",
"CA1062:Validate arguments of public methods",
Justification = "Not a public method"
)]
protected override string TransformKey(DelayGetValue.Query request) =>
$"DelayGetValue(query: {request.Value})";

Expand Down
48 changes: 48 additions & 0 deletions tests/Immediate.Cache.FunctionalTests/FailingLoadCache.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Immediate.Cache.Shared;
using Immediate.Handlers.Shared;

namespace Immediate.Cache.FunctionalTests;

[Handler]
public sealed partial class FailingLoad
{
public const string FailingLoadMessage = "load failed";

public sealed class Query
{
public required string Key { get; init; }
public TaskCompletionSource Started { get; } = new();
public TaskCompletionSource Release { get; } = new();
}

private async ValueTask<string> HandleAsync(Query query, CancellationToken _)
{
query.Started.TrySetResult();

// Stands in for work that fails without observing the token, such as a database query whose rows changed underneath it.
await query.Release.Task;

throw new FailingLoadException(FailingLoadMessage);
}

public sealed class FailingLoadException : Exception
{
public FailingLoadException()
{
}

public FailingLoadException(string message) : base(message)
{
}

public FailingLoadException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

[CacheFor<FailingLoad>]
public sealed partial class FailingLoadCache
{
protected override string TransformKey(FailingLoad.Query request) => request.Key;
}
15 changes: 0 additions & 15 deletions tests/Immediate.Cache.FunctionalTests/GetValue.cs

This file was deleted.

19 changes: 13 additions & 6 deletions tests/Immediate.Cache.FunctionalTests/GetValueCache.cs
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
using System.Diagnostics.CodeAnalysis;
using Immediate.Cache.Shared;
using Immediate.Handlers.Shared;

namespace Immediate.Cache.FunctionalTests;

[Handler]
public sealed partial class GetValue
{
public sealed record Query(int Value);
public sealed record Response(int Value, bool ExecutedHandler);

private ValueTask<Response> HandleAsync(
Query query,
CancellationToken _
) => ValueTask.FromResult(new Response(query.Value, ExecutedHandler: true));
}

[CacheFor<GetValue>]
public sealed partial class GetValueCache
{
[SuppressMessage(
"Design",
"CA1062:Validate arguments of public methods",
Justification = "Not a public method"
)]
protected override string TransformKey(GetValue.Query request) =>
$"GetValue(query: {request.Value})";
}
Loading