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
30 changes: 29 additions & 1 deletion src/core/Akka.TestKit/TestKitBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ namespace Akka.TestKit
/// </summary>
public abstract partial class TestKitBase : IActorRefFactory
{
// AsyncLocal for proper timeout propagation across async boundaries.
// This ensures WithinAsync timeout flows correctly to EventFilter and other async operations.
private readonly AsyncLocal<TimeSpan?> _asyncLocalEnd = new();

private class TestState
{
public TestState()
Expand Down Expand Up @@ -445,9 +449,21 @@ public TimeSpan Remaining
{
get
{
// Check AsyncLocal first (async context takes precedence)
var asyncEnd = _asyncLocalEnd.Value;
if (asyncEnd.HasValue)
{
if (asyncEnd < TimeSpan.Zero)
throw new InvalidOperationException($"End can not be negative, was: {asyncEnd}");

var asyncRemaining = asyncEnd.Value - Now;
return asyncRemaining < TimeSpan.Zero ? TimeSpan.Zero : asyncRemaining;
}

// Fallback to instance field
if(_testState.End is null)
throw new InvalidOperationException(@"Remaining may not be called outside of ""within""");

if (_testState.End < TimeSpan.Zero)
throw new InvalidOperationException($"End can not be negative, was: {_testState.End}");

Expand All @@ -466,6 +482,18 @@ public TimeSpan Remaining
/// <returns>TBD</returns>
protected TimeSpan RemainingOr(TimeSpan duration)
{
// Check AsyncLocal first (async context takes precedence for proper timeout propagation)
var asyncEnd = _asyncLocalEnd.Value;
if (asyncEnd.HasValue)
{
if (asyncEnd < TimeSpan.Zero)
throw new InvalidOperationException($"End can not be negative, was: {asyncEnd}");

var asyncRemaining = asyncEnd.Value - Now;
return asyncRemaining < TimeSpan.Zero ? TimeSpan.Zero : asyncRemaining;
}

// Fallback to instance field for backward compatibility with sync code paths
if (!_testState.End.HasValue) return duration;
if (_testState.End < TimeSpan.Zero)
throw new InvalidOperationException($"End can not be negative, was: {_testState.End}");
Expand Down
4 changes: 4 additions & 0 deletions src/core/Akka.TestKit/TestKitBase_Within.cs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,10 @@ public async Task<T> WithinAsync<T>(

var maxDiff = max.Min(rem);
var prevEnd = _testState.End;
var prevAsyncEnd = _asyncLocalEnd.Value; // Save previous AsyncLocal value for nesting support

_testState.End = start + maxDiff;
_asyncLocalEnd.Value = start + maxDiff; // Set AsyncLocal for proper async propagation

T ret = default;
using (var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
Expand All @@ -320,6 +323,7 @@ public async Task<T> WithinAsync<T>(
// Make sure we stop the delay task
cts.Cancel();
_testState.End = prevEnd;
_asyncLocalEnd.Value = prevAsyncEnd; // Restore previous AsyncLocal value
}
}

Expand Down
Loading