Skip to content

Commit

Permalink
Introduce unit-tests for issues fixed in v8 (#1157)
Browse files Browse the repository at this point in the history
  • Loading branch information
martintmk authored May 2, 2023
1 parent dbd9d67 commit 7ccf2e7
Show file tree
Hide file tree
Showing 11 changed files with 253 additions and 24 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ public async Task TryWaitForCompletedExecutionAsync_FinishedTask_Ok()
var task = await context.TryWaitForCompletedExecutionAsync(TimeSpan.Zero);

task.Should().NotBeNull();
task!.ExecutionTask!.IsCompleted.Should().BeTrue();
task!.ExecutionTaskSafe!.IsCompleted.Should().BeTrue();
task.Outcome.Result.Should().Be("dummy");
task.AcceptOutcome();
context.LoadedTasks.Should().Be(1);
Expand All @@ -128,7 +128,7 @@ public async Task TryWaitForCompletedExecutionAsync_ConcurrentExecution_Ok()

_timeProvider.Advance(TimeSpan.FromDays(1));
await context.TryWaitForCompletedExecutionAsync(TimeSpan.Zero);
await context.Tasks.First().ExecutionTask!;
await context.Tasks.First().ExecutionTaskSafe!;
context.Tasks.First().AcceptOutcome();
}

Expand Down Expand Up @@ -171,7 +171,7 @@ public async Task TryWaitForCompletedExecutionAsync_HedgedExecution_Ok()
_timeProvider.DelayEntries.Last().Delay.Should().Be(hedgingDelay);
_timeProvider.Advance(TimeSpan.FromDays(1));
await task;
await context.Tasks.First().ExecutionTask!;
await context.Tasks.First().ExecutionTaskSafe!;
context.Tasks.First().AcceptOutcome();
}

Expand Down Expand Up @@ -385,7 +385,7 @@ public async Task Complete_EnsurePendingTasksCleaned()

await context.TryWaitForCompletedExecutionAsync(HedgingStrategyOptions.InfiniteHedgingDelay);

var pending = context.Tasks[1].ExecutionTask!;
var pending = context.Tasks[1].ExecutionTaskSafe!;
pending.Wait(10).Should().BeFalse();

context.Tasks[0].AcceptOutcome();
Expand Down
12 changes: 6 additions & 6 deletions src/Polly.Core.Tests/Hedging/Controller/TaskExecutionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ await execution.InitializeAsync(HedgedTaskType.Primary, _snapshot,
"dummy-state",
1);

await execution.ExecutionTask!;
await execution.ExecutionTaskSafe!;
((DisposableResult)execution.Outcome.Result!).Name.Should().Be(value);
execution.IsHandled.Should().Be(handled);
AssertPrimaryContext(execution.Context, execution);
Expand All @@ -72,7 +72,7 @@ public async Task Initialize_Secondary_Ok(string value, bool handled)

(await execution.InitializeAsync<DisposableResult, string>(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue();

await execution.ExecutionTask!;
await execution.ExecutionTaskSafe!;

((DisposableResult)execution.Outcome.Result!).Name.Should().Be(value);
execution.IsHandled.Should().Be(handled);
Expand Down Expand Up @@ -124,7 +124,7 @@ public async Task Initialize_SecondaryWhenTaskGeneratorThrows_EnsureOutcome()

(await execution.InitializeAsync<DisposableResult, string>(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue();

await execution.ExecutionTask!;
await execution.ExecutionTaskSafe!;
execution.Outcome.Exception.Should().BeOfType<FormatException>();
}

Expand All @@ -136,7 +136,7 @@ public async Task Initialize_ExecutionTaskDoesNotThrows()

(await execution.InitializeAsync<DisposableResult, string>(HedgedTaskType.Secondary, _snapshot, null!, "dummy-state", 4)).Should().BeTrue();

await execution.ExecutionTask!.Invoking(async t => await t).Should().NotThrowAsync();
await execution.ExecutionTaskSafe!.Invoking(async t => await t).Should().NotThrowAsync();
}

[InlineData(true)]
Expand Down Expand Up @@ -167,7 +167,7 @@ await execution.InitializeAsync(primary ? HedgedTaskType.Primary : HedgedTaskTyp

// act
_cts.Cancel();
await execution.ExecutionTask!;
await execution.ExecutionTaskSafe!;

// assert
execution.Outcome.Exception.Should().BeAssignableTo<OperationCanceledException>();
Expand All @@ -191,7 +191,7 @@ public async Task ResetAsync_Ok(bool accept)
var execution = Create();
var token = default(CancellationToken);
await InitializePrimaryAsync(execution, result, context => token = context.CancellationToken);
await execution.ExecutionTask!;
await execution.ExecutionTaskSafe!;

if (accept)
{
Expand Down
18 changes: 15 additions & 3 deletions src/Polly.Core.Tests/Hedging/HedgingResilienceStrategyTests.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using Polly.Hedging;
using Polly.Strategy;
using Xunit.Abstractions;

namespace Polly.Core.Tests.Hedging;

Expand All @@ -11,7 +12,7 @@ public class HedgingResilienceStrategyTests : IDisposable
private const string Failure = "Failure";

private static readonly TimeSpan LongDelay = TimeSpan.FromDays(1);
private static readonly TimeSpan AssertTimeout = TimeSpan.FromSeconds(10);
private static readonly TimeSpan AssertTimeout = TimeSpan.FromSeconds(15);

private readonly HedgingStrategyOptions _options = new();
private readonly List<IResilienceArguments> _events = new();
Expand All @@ -21,15 +22,17 @@ public class HedgingResilienceStrategyTests : IDisposable
private readonly PrimaryStringTasks _primaryTasks;
private readonly List<object?> _results = new();
private readonly CancellationTokenSource _cts = new();
private readonly ITestOutputHelper _testOutput;

public HedgingResilienceStrategyTests()
public HedgingResilienceStrategyTests(ITestOutputHelper testOutput)
{
_telemetry = TestUtilities.CreateResilienceTelemetry(args => _events.Add(args));
_timeProvider = new HedgingTimeProvider { AutoAdvance = _options.HedgingDelay };
_actions = new HedgingActions(_timeProvider);
_primaryTasks = new PrimaryStringTasks(_timeProvider);
_options.HedgingDelay = TimeSpan.FromSeconds(1);
_options.MaxHedgedAttempts = _actions.MaxHedgedTasks;
_testOutput = testOutput;
}

public void Dispose()
Expand Down Expand Up @@ -121,14 +124,23 @@ public async void ExecuteAsync_EnsureHedgedTasksCancelled_Ok()
using var cancelled = new ManualResetEvent(false);
ConfigureHedging(async context =>
{
#pragma warning disable CA1031 // Do not catch general exception types
try
{
await _timeProvider.Delay(LongDelay, context.CancellationToken);
_testOutput.WriteLine("Hedged task executing...");
await Task.Delay(LongDelay, context.CancellationToken);
_testOutput.WriteLine("Hedged task executing...done (not-cancelled)");
}
catch (OperationCanceledException)
{
_testOutput.WriteLine("Hedged task executing...cancelled");
cancelled.Set();
}
catch (Exception e)
{
_testOutput.WriteLine($"Hedged task executing...error({e})");
}
#pragma warning restore CA1031 // Do not catch general exception types

return Failure;
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
using Polly.CircuitBreaker;

namespace Polly.Core.Tests.Issues;

public partial class IssuesTests
{
[Fact]
public void CircuitBreakerStateSharing_959()
{
var options = new AdvancedCircuitBreakerStrategyOptions
{
FailureThreshold = 1,
MinimumThroughput = 10
};

// handle int results
options.ShouldHandle.HandleResult(-1);

// handle string results
options.ShouldHandle.HandleResult("error");

// create the strategy
var strategy = new ResilienceStrategyBuilder { TimeProvider = TimeProvider.Object }.AddAdvancedCircuitBreaker(options).Build();

// now trigger the circuit breaker by evaluating multiple result types
for (int i = 0; i < 5; i++)
{
strategy.Execute(_ => -1);
strategy.Execute(_ => "error");
}

// now the circuit breaker should be open
strategy.Invoking(s => s.Execute(_ => 0)).Should().Throw<BrokenCircuitException>();
strategy.Invoking(s => s.Execute(_ => "valid-result")).Should().Throw<BrokenCircuitException>();

// now wait for recovery
TimeProvider.AdvanceTime(options.BreakDuration);

// OK, circuit is closed now
strategy.Execute(_ => 0);
strategy.Execute(_ => "valid-result");
}
}
30 changes: 30 additions & 0 deletions src/Polly.Core.Tests/Issues/IssuesTests.FlowingContext_849.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using Polly.Retry;

namespace Polly.Core.Tests.Issues;

public partial class IssuesTests
{
[Fact]
public void FlowingContext_849()
{
var contextChecked = false;
var retryOptions = new RetryStrategyOptions();

// configure the predicate and use the context
retryOptions.ShouldRetry.HandleResult<int>((_, args) =>
{
// access the context to evaluate the retry
ResilienceContext context = args.Context;
context.Should().NotBeNull();
contextChecked = true;
return false;
});

var strategy = new ResilienceStrategyBuilder().AddRetry(retryOptions).Build();

// execute the retry
strategy.Execute(_ => 0);

contextChecked.Should().BeTrue();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using Polly.Retry;

namespace Polly.Core.Tests.Issues;

public partial class IssuesTests
{
[Fact]
public void HandleMultipleResults_898()
{
var isRetryKey = new ResiliencePropertyKey<bool>("is-retry");
var options = new RetryStrategyOptions
{
BackoffType = RetryBackoffType.Constant,
RetryCount = 1,
BaseDelay = TimeSpan.FromMilliseconds(1),
};

// now add a callback updates the resilience context with the retry marker
options.OnRetry.Register((_, args) => args.Context.Properties.Set(isRetryKey, true));

// handle int results
options.ShouldRetry.HandleResult(-1);

// handle string results
options.ShouldRetry.HandleResult("error");

// create the strategy
var strategy = new ResilienceStrategyBuilder { TimeProvider = TimeProvider.Object }.AddRetry(options).Build();

// check that int-based results is retried
bool isRetry = false;
strategy.Execute(_ =>
{
if (isRetry)
{
return 0;
}

isRetry = true;
return -1;
}).Should().Be(0);

// check that string-based results is retried
isRetry = false;
strategy.Execute(_ =>
{
if (isRetry)
{
return "no-error";
}

isRetry = true;
return "error";
}).Should().Be("no-error");
}
}
6 changes: 6 additions & 0 deletions src/Polly.Core.Tests/Issues/IssuesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace Polly.Core.Tests.Issues;

public partial class IssuesTests
{
private FakeTimeProvider TimeProvider { get; } = new FakeTimeProvider().SetupUtcNow().SetupAnyDelay();
}
12 changes: 6 additions & 6 deletions src/Polly.Core/Hedging/Controller/HedgingExecutionContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ private ExecutionInfo<TResult> CreateExecutionInfoWhenNoExecution<TResult>()
// if there are no more executing tasks we need to check finished ones
if (_executingTasks.Count == 0)
{
var finishedExecution = _tasks.First(static t => t.ExecutionTask!.IsCompleted);
var finishedExecution = _tasks.First(static t => t.ExecutionTaskSafe!.IsCompleted);
finishedExecution.AcceptOutcome();
return new ExecutionInfo<TResult>(null, false, finishedExecution.Outcome.AsOutcome<TResult>());
}
Expand All @@ -168,22 +168,22 @@ private Task<Task> WaitForTaskCompetitionAsync()
return _executingTasks.Count switch
{
1 => AwaitTask(_executingTasks[0], ContinueOnCapturedContext),
2 => Task.WhenAny(_executingTasks[0].ExecutionTask!, _executingTasks[1].ExecutionTask!),
_ => Task.WhenAny(_executingTasks.Select(v => v.ExecutionTask!))
2 => Task.WhenAny(_executingTasks[0].ExecutionTaskSafe!, _executingTasks[1].ExecutionTaskSafe!),
_ => Task.WhenAny(_executingTasks.Select(v => v.ExecutionTaskSafe!))
};
#pragma warning restore S109 // Magic numbers should not be used

static async Task<Task> AwaitTask(TaskExecution task, bool continueOnCapturedContext)
{
// ExecutionTask never fails
await task.ExecutionTask!.ConfigureAwait(continueOnCapturedContext);
await task.ExecutionTaskSafe!.ConfigureAwait(continueOnCapturedContext);
return Task.FromResult(task);
}
}

private TaskExecution? TryRemoveExecutedTask()
{
if (_executingTasks.FirstOrDefault(static v => v.ExecutionTask!.IsCompleted) is TaskExecution execution)
if (_executingTasks.FirstOrDefault(static v => v.ExecutionTaskSafe!.IsCompleted) is TaskExecution execution)
{
_executingTasks.Remove(execution);
return execution;
Expand Down Expand Up @@ -235,7 +235,7 @@ private async Task CleanupInBackgroundAsync()
{
foreach (var task in _tasks)
{
await task.ExecutionTask!.ConfigureAwait(false);
await task.ExecutionTaskSafe!.ConfigureAwait(false);
await task.ResetAsync().ConfigureAwait(false);
_executionPool.Return(task);
}
Expand Down
18 changes: 13 additions & 5 deletions src/Polly.Core/Hedging/Controller/TaskExecution.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,15 @@ internal sealed class TaskExecution

public TaskExecution(HedgingHandler.Handler handler) => _handler = handler;

public Task? ExecutionTask { get; private set; }
/// <summary>
/// Gets the task that represents the execution of the hedged task.
/// </summary>
/// <remarks>
/// This property is not-null once the <see cref="TaskExecution"/> is initialized.
/// Awaiting this task will never throw as all exceptions are caught and stored
/// into <see cref="Outcome"/> property.
/// </remarks>
public Task? ExecutionTaskSafe { get; private set; }

public Outcome Outcome { get; private set; }

Expand All @@ -47,7 +55,7 @@ internal sealed class TaskExecution

public void AcceptOutcome()
{
if (ExecutionTask?.IsCompleted == true)
if (ExecutionTaskSafe?.IsCompleted == true)
{
IsAccepted = true;
}
Expand Down Expand Up @@ -98,15 +106,15 @@ public async ValueTask<bool> InitializeAsync<TResult, TState>(
}
catch (Exception e)
{
ExecutionTask = ExecuteCreateActionException<TResult>(e);
ExecutionTaskSafe = ExecuteCreateActionException<TResult>(e);
return true;
}

ExecutionTask = ExecuteSecondaryActionAsync(action);
ExecutionTaskSafe = ExecuteSecondaryActionAsync(action);
}
else
{
ExecutionTask = ExecutePrimaryActionAsync(primaryCallback, state);
ExecutionTaskSafe = ExecutePrimaryActionAsync(primaryCallback, state);
}

return true;
Expand Down
Loading

0 comments on commit 7ccf2e7

Please sign in to comment.