Preserve framed reads across fake worker emission gates
ci / windows-x86 (push) Successful in 1m20s
ci / nightly-windev (push) Has been skipped
ci / java (push) Successful in 2m17s
ci / windows-x86 (pull_request) Successful in 1m25s
ci / nightly-windev (pull_request) Has been skipped
ci / java (pull_request) Successful in 2m27s
ci / portable (push) Successful in 9m43s
ci / portable (pull_request) Successful in 9m47s

This commit is contained in:
Joseph Doherty
2026-10-13 09:42:09 -04:00
parent 19920b13da
commit 42c120aa83
5 changed files with 213 additions and 50 deletions
@@ -478,24 +478,42 @@ public sealed class SessionManagerTests
[Fact]
public async Task InvokeAsync_WhenWorkerHandshakingThenReadyWithinTimeout_Succeeds()
{
FakeTimeProvider clock = new(new DateTimeOffset(2026, 1, 1, 0, 0, 0, TimeSpan.Zero));
FakeWorkerClient workerClient = new() { State = WorkerClientState.Handshaking };
SessionManager manager = CreateManager(
new FakeSessionWorkerClientFactory(workerClient),
options: CreateOptions(workerReadyWaitTimeoutMs: 500));
options: CreateOptions(workerReadyWaitTimeoutMs: 500),
timeProvider: clock);
GatewaySession session = await manager.OpenSessionAsync(CreateOpenRequest(), "client-1", ownerKeyId: null, CancellationToken.None);
Assert.Equal(SessionState.Ready, session.State);
// Flip the worker to Ready shortly after the invoke starts waiting.
_ = Task.Run(async () =>
TaskCompletionSource handshakingObserved = new(TaskCreationOptions.RunContinuationsAsynchronously);
TaskCompletionSource allowReady = new(TaskCreationOptions.RunContinuationsAsynchronously);
workerClient.OnStateRead = state =>
{
await Task.Delay(50, CancellationToken.None);
workerClient.State = WorkerClientState.Ready;
});
WorkerCommandReply reply = await manager.InvokeAsync(
if (state == WorkerClientState.Handshaking)
{
handshakingObserved.TrySetResult();
}
};
Task transition = TransitionAsync();
Task<WorkerCommandReply> invoke = manager.InvokeAsync(
session.SessionId,
CreateCommand(MxCommandKind.Ping),
CancellationToken.None);
await handshakingObserved.Task.WaitAsync(TimeSpan.FromSeconds(5));
Assert.False(invoke.IsCompleted);
Assert.Equal(0, workerClient.InvokeCount);
allowReady.SetResult();
await transition.WaitAsync(TimeSpan.FromSeconds(5));
clock.Advance(TimeSpan.FromMilliseconds(50));
WorkerCommandReply reply = await invoke.WaitAsync(TimeSpan.FromSeconds(5));
async Task TransitionAsync()
{
await allowReady.Task;
workerClient.State = WorkerClientState.Ready;
}
Assert.NotNull(reply);
Assert.Equal(1, workerClient.InvokeCount);
@@ -1406,14 +1424,28 @@ public sealed class SessionManagerTests
private sealed class FakeWorkerClient : IWorkerClient
{
private WorkerClientState _state = WorkerClientState.Ready;
/// <inheritdoc />
public string SessionId { get; init; } = "session-1";
/// <inheritdoc />
public int? ProcessId { get; init; } = 1234;
/// <summary>Gets or sets the optional state-observation gate.</summary>
public Action<WorkerClientState>? OnStateRead { get; set; }
/// <inheritdoc />
public WorkerClientState State { get; set; } = WorkerClientState.Ready;
public WorkerClientState State
{
get
{
OnStateRead?.Invoke(_state);
return _state;
}
set => _state = value;
}
/// <inheritdoc />
public DateTimeOffset LastHeartbeatAt { get; init; } = DateTimeOffset.UtcNow;