Adding rewind to the sidecar - #802
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved findings affect rewind validation, large-history handling, test synchronization, and the Linux emulator test path.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds DTS rewind support to the sidecar so the .NET SDK can invoke rewind operations.
Changes:
- Implements rewind history rewriting and trace handling.
- Updates worker processing for rewind requests.
- Adds unit tests, emulator integration tests, and CI execution.
File summaries
| File | Summary |
|---|---|
test/Worker/Grpc.Tests/RewindOrchestrationHandlerTests.cs |
Tests rewind history rewriting and tracing. |
test/Worker/Grpc.Tests/GrpcDurableTaskWorkerTests.cs |
Tests worker rewind processing. |
test/Grpc.IntegrationTests/run-dts-emulator-tests.ps1 |
Starts the emulator and runs integration tests. |
test/Grpc.IntegrationTests/Grpc.IntegrationTests.csproj |
Adds required project references. |
test/Grpc.IntegrationTests/DtsRewindIntegrationTests.cs |
Adds end-to-end DTS rewind scenarios. |
test/Grpc.IntegrationTests/DtsEmulatorFactAttribute.cs |
Controls emulator-dependent test execution. |
src/Worker/Grpc/RewindOrchestrationHandler.cs |
Builds rewind responses and replacement history. |
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs |
Detects and processes rewind work items. |
src/Shared/Grpc/ProtoUtils.cs |
Centralizes trace-context serialization. |
.github/workflows/validate-build.yml |
Runs emulator tests in CI. |
Review details
Suppressed comments (3)
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs:678
- This sends the complete replacement history as one
RewindOrchestrationAction. The existing completion chunker rejects any single action larger than the configured chunk size whenLargePayloadsis not advertised (the default only advertises history streaming), and the payload interceptor does not externalizeRewindOrchestrationAction. Consequently, rewinding a sufficiently large failed orchestration is converted into a validation failure instead of being rewound. Add protocol-specific splitting/externalization for rewind history, or otherwise handle this size case before exposing rewind as supported.
await this.CompleteOrchestratorTaskWithChunkingAsync(
RewindOrchestrationHandler.CreateResponse(
request,
pastEvents,
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs:683
- This early-return path starts the orchestration activity but never stops or disposes it. When tracing is enabled, the activity remains active/current after the rewind response and its span is never closed; wrap completion in
try/finallyand disposetraceActivitythere, including when response creation or completion fails.
if (isInitialRewind)
{
await this.CompleteOrchestratorTaskWithChunkingAsync(
RewindOrchestrationHandler.CreateResponse(
request,
pastEvents,
completionToken,
traceActivity),
this.worker.grpcOptions.CompleteOrchestrationWorkItemChunkSizeInBytes,
cancellationToken);
return;
test/Grpc.IntegrationTests/DtsRewindIntegrationTests.cs:509
WaitForInstanceCompletionAsyncreturns immediately when an instance is already terminal. AfterRewindInstanceAsynconly enqueues the request, this helper can return the pre-rewindFailedmetadata before DTS processes the rewind; the later assertions (and the second rewind) then race the backend. Wait for the execution ID or status to change from the failed execution before waiting for the new terminal state.
return await client.WaitForInstanceCompletionAsync(
instanceId,
getInputsAndOutputs: true,
this.TimeoutToken);
- Files reviewed: 10/10 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved rewind-history correctness issues and unreliable or skipped integration coverage remain.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (4)
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs:658
- The missing-
ExecutionStartedEventguard is nested underParentTraceContext is not null, so an initial rewind without trace context bypasses it.CreateResponsecan then emit replacement history without anExecutionStartedevent, which cannot be replayed by the worker; perform this validation for every initial rewind and only conditionally clone the trace context.
if (isInitialRewind
&& rewindEvent!.ParentTraceContext is not null)
{
if (executionStartedEvent is null)
{
src/Worker/Grpc/RewindOrchestrationHandler.cs:32
- The validation only checks the second event. A malformed request whose first event is not
OrchestratorStartedis accepted and that event is then copied into the replacement history, despite the error contract requiringOrchestratorStartedfollowed byExecutionRewound. Validate the first event as well so an invalid rewind cannot produce an invalid history.
if (request.NewEvents.Count != 2
|| request.NewEvents[1].EventTypeCase != P.HistoryEvent.EventTypeOneofCase.ExecutionRewound)
test/Grpc.IntegrationTests/DtsRewindIntegrationTests.cs:211
- This is the only test in the new suite that exercises rewinding multiple failed sub-orchestrations, but the hard-coded
Skipmeans the DTS CI job never runs it. Regressions in the recursive child-rewrite path can therefore pass CI; make the scenario runnable with the image used by CI or add an equivalent non-skipped test when the required sidecar fix is available.
[DtsEmulatorFact(Skip = "Requires a DTS emulator image containing the sidecar rewind history fix.")]
test/Grpc.IntegrationTests/DtsRewindIntegrationTests.cs:580
- The fixed two-second delay is not a synchronization guarantee for the recreated child records. Under CI load the emulator may take longer to make these instances queryable, causing this test to fail even though rewind eventually succeeds. Poll each child with
TimeoutTokenuntil it exists and reaches the expected terminal state instead of sleeping for a fixed interval.
await Task.Delay(TimeSpan.FromSeconds(2));
- Files reviewed: 10/10 changed files
- Comments generated: 5
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Unresolved rewind cleanup, validation, synchronization, and CI path issues remain.
Review details
Suppressed comments (9)
Previously missed (2) — in code that hasn't changed since the last review.
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs:684
- This early-return path never disposes
traceActivity, unlike the normal path below (which callsDispose). Every initial rewind can therefore leave its orchestration span open/current, leaking tracing state across repeated work items. Dispose the activity in ausing/finallybefore returning, including when response creation or completion fails.
This issue also appears on line 676 of the same file.
test/Grpc.IntegrationTests/DtsRewindIntegrationTests.cs:276
- The fixed two-second delay is not a visibility guarantee for the recreated child state. Under eventual consistency or CI load, the following GetInstanceAsync calls can still observe missing/old metadata; poll with the test cancellation token until the expected child reaches its updated terminal state instead.
This issue also appears on line 580 of the same file.
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs:680
- The rewind history is serialized as one
RewindOrchestrationAction, but this call routes it throughCompleteOrchestratorTaskWithChunkingAsync, which rejects any individual action larger than the configured 1–3.9 MB chunk limit instead of splitting it. A failed orchestration with a larger history will therefore be completed as failed rather than rewound; this needs a protocol-compatible way to split the rewind payload or an explicit supported-size limit.
RewindOrchestrationHandler.CreateResponse(
request,
pastEvents,
completionToken,
traceActivity),
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs:655
- The missing-
ExecutionStartedEventguard is nested under theParentTraceContextcheck. A failed rewind without a parent trace therefore falls through and sends a replacement history with no execution-start event, instead of rejecting the malformed history; validate the event unconditionally before applying the optional trace-context rewrite.
if (isInitialRewind
&& rewindEvent!.ParentTraceContext is not null)
src/Worker/Grpc/RewindOrchestrationHandler.cs:46
- This local is computed but never read; the actual trace-context parsing below reads the rewritten
ExecutionStartedevent instead. Remove the dead declaration so the rewind path does not carry an unused fallback value and compiler warning.
P.TraceContext? orchestrationParentTraceContext = rewindEvent.ParentTraceContext
?? executionStartedEvent?.ExecutionStarted.ParentTraceContext;
test/Grpc.IntegrationTests/DtsRewindIntegrationTests.cs:775
RewindInstanceAsynconly enqueues the operation, whileWaitForInstanceCompletionAsyncreturns immediately when the instance is already terminal. If the sidecar has not processed the rewind yet, this helper returns the previousFailedmetadata, so every post-rewind assertion (and the second rewind) races the backend. Poll for an updated state/timestamp before calling the terminal wait.
return await client.WaitForInstanceCompletionAsync(
instanceId,
getInputsAndOutputs: true,
this.TimeoutToken);
test/Grpc.IntegrationTests/DtsRewindIntegrationTests.cs:454
- A fixed two-second sleep is not a reliable synchronization point for recreated child metadata. The emulator/storage path is eventually consistent, so a slower CI run can still return
nullon the followingGetInstanceAsync; poll until the child reachesCompletedusingTimeoutTokeninstead.
await Task.Delay(TimeSpan.FromSeconds(2));
test/Grpc.IntegrationTests/DtsRewindIntegrationTests.cs:580
- A fixed two-second sleep is not a reliable synchronization point for recreated child metadata. The emulator/storage path is eventually consistent, so a slower CI run can still return
nullon the followingGetInstanceAsynccalls; poll each child until it reachesCompletedusingTimeoutTokeninstead.
await Task.Delay(TimeSpan.FromSeconds(2));
test/Grpc.IntegrationTests/run-dts-emulator-tests.ps1:68
- The workflow runs this script with PowerShell on
ubuntu-latest, but this raw path uses a Windows backslash. On the Linux runner the backslash can be passed todotnetas part of the filename, so the project path is not found. Build the argument withJoin-Path(or use/) instead.
"$PSScriptRoot\Grpc.IntegrationTests.csproj" `
- Files reviewed: 10/10 changed files
- Comments generated: 0 new
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved critical rewind-history handling issues and additional runtime, integration-test, and CI concerns block approval.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (10)
Previously missed (2) — in code that hasn't changed since the last review.
test/Grpc.IntegrationTests/DtsRewindIntegrationTests.cs:276
- This fixed two-second delay makes the child assertions depend on emulator scheduling: slower CI can still race, while fast runs always pay the full delay. Poll
GetInstanceAsyncfor each child until the expected terminal state usingTimeoutToken, following the existing polling pattern inGrpcDurableTaskClientIntegrationTests.cs:664-683.
test/Grpc.IntegrationTests/DtsRewindIntegrationTests.cs:580 - A fixed two-second sleep does not guarantee that the purged children have been recreated and are visible through
GetInstanceAsync; under CI or emulator load this assertion can race, while fast runs just waste time. Poll for each child withTimeoutToken, following the existing integration-test polling pattern, instead of sleeping for a fixed duration.
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs:655
- The missing-history guard is nested under
ParentTraceContext. A malformed initial rewind without a parent trace therefore reachesCreateResponseand sends replacement history without anExecutionStartedevent, bypassing the runtime-state invariant. ValidateexecutionStartedEventfor every initial rewind, then conditionally clone only the trace context.
if (isInitialRewind)
{
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs:675
- On the initial rewind path this returns before the existing disposal at line 977, so any sampled
traceActivityremains undisposed/current and can leak across work items. Complete this call in atry/finally(or use a scopedusing) and dispose the activity before returning, including when completion fails.
orchestrationTraceContext);
if (isInitialRewind)
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs:620
- This path explicitly supports rewinds whose committed history is delivered through
StreamInstanceHistory, but all rewind tests populatePastEventsinline and leaveRequiresHistoryStreamingfalse. Add a regression test that combinesRequiresHistoryStreaming = truewithExecutionRewoundand verifies the streamed history is used to build the replacement history.
if (rewindEvent is not null || request.RequiresHistoryStreaming)
{
materializedPastEvents = await this.GetPastEventsAsync(request, cancellationToken);
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs:676
- The rewind response puts the complete replacement history into one
RewindOrchestrationAction. The existing completion helper treats each action as indivisible and, withoutLargePayloads, converts any action larger than the roughly 3.9 MB limit into a failed completion; response chunking cannot split this action. Large histories can therefore not be rewound, so this needs protocol-compatible chunking/externalization or an explicit supported-size design.
if (isInitialRewind)
{
test/Grpc.IntegrationTests/DtsRewindIntegrationTests.cs:775
RewindInstanceAsynconly enqueues the rewind, whileWaitForInstanceCompletionAsyncreturns immediately for any terminal status. Every post-rewind call to this helper can therefore return the oldFailedmetadata before DTS starts the new execution (andRewindTwiceAsynccan enqueue the second rewind too early). Add a state-transition guard that pollsGetInstanceAsyncuntil the status leavesFailedbefore waiting for terminal completion.
}
catch
{
host.Dispose();
test/Grpc.IntegrationTests/DtsRewindIntegrationTests.cs:454
- A fixed two-second sleep does not guarantee that a purged child has been recreated and is visible through
GetInstanceAsync; under CI or emulator load this assertion can race, while fast runs just waste time. Poll for the child metadata withTimeoutToken, following the existing integration-test polling pattern, instead of sleeping for a fixed duration.
nestedInstanceId,
test/Grpc.IntegrationTests/run-dts-emulator-tests.ps1:68
- The workflow runs this script on
ubuntu-latest, but this constructs the project path with a Windows\separator. PowerShell Core on Linux passes that backslash as part of the filename, sodotnet testcannot find the project and the new CI job fails before running tests. UseJoin-Path $PSScriptRoot 'Grpc.IntegrationTests.csproj'(as the repository's cross-platform scripts do).
"$PSScriptRoot\Grpc.IntegrationTests.csproj" `
test/Grpc.IntegrationTests/run-dts-emulator-tests.ps1:70
- This standalone script invokes
dotnet testwithout an explicit restore or the repository's NuGet configuration. The build and CodeQL workflows restore withnuget.config(which clears default feeds), so a clean runner can fail to restore or resolve a different package set; add an explicit restore with the checked-in config and run the test with--no-restore.
dotnet test `
"$PSScriptRoot\Grpc.IntegrationTests.csproj" `
--configuration $Configuration `
--filter "Category=DtsEmulator"
- Files reviewed: 10/10 changed files
- Comments generated: 2
- Review effort level: Lite
There was a problem hiding this comment.
🟡 Changes recommended
Large rewind histories can exceed the default transport limit, and trace-context and cross-platform path issues remain unresolved.
Get a fresh assessment by requesting another Copilot review.
Review details
Suppressed comments (2)
src/Worker/Grpc/RewindOrchestrationHandler.cs:116
- The replacement child context copies trace ID, flags, and trace state from the failed execution's parent context, not from the new rewind orchestration activity. If sampling changes for the new activity, failed sub-orchestrations can be propagated with stale sampling/state and diverge from normal child scheduling, which uses the current activity context (
src/Shared/Grpc/ProtoUtils.cs:335-338). Build this context fromorchestrationActivity.Contextwhen available, falling back toorchestrationParentContext.
ActivityContext newParentTraceContext = new(
orchestrationParentContext.TraceId,
ActivitySpanId.CreateRandom(),
orchestrationParentContext.TraceFlags,
orchestrationParentContext.TraceState);
test/Grpc.IntegrationTests/run-dts-emulator-tests.ps1:68
- The workflow runs this script with
pwshonubuntu-latest, but this argument builds the project path with a Windows backslash. On Unix PowerShell that can be passed todotnetas a filename containing\, so the emulator job can fail with a project-not-found error. UseJoin-Pathfor the cross-platform path.
"$PSScriptRoot\Grpc.IntegrationTests.csproj" `
- Files reviewed: 10/10 changed files
- Comments generated: 1
- Review effort level: Lite
There was a problem hiding this comment.
🔵 Needs a closer look
Two moderate findings remain: increased history memory use and a non-portable emulator test path.
Review details
Suppressed comments (2)
src/Worker/Grpc/GrpcDurableTaskWorker.Processor.cs:314
- For every history-streamed orchestration, this now retains the complete protobuf event list and then
BuildRuntimeStatecreates a second complete converted list. The previous streaming path converted each chunk as it arrived, so large histories now require roughly two full history representations and can add avoidable worker memory pressure; keep single-pass conversion for normal execution and materialize protobuf events only when rewind processing needs them (or otherwise release them before conversion).
List<P.HistoryEvent> pastEvents = new();
await foreach (P.HistoryChunk chunk in streamResponse.ResponseStream.ReadAllAsync(cancellation))
{
foreach (P.HistoryEvent protoEvent in chunk.Events)
{
pastEvents.Add(protoEvent);
test/Grpc.IntegrationTests/run-dts-emulator-tests.ps1:68
- The CI job runs this script on
ubuntu-latest, where\is not a filesystem separator. Passing"$PSScriptRoot\Grpc.IntegrationTests.csproj"therefore givesdotneta path containing a literal backslash and the test project cannot be found; construct the path withJoin-Pathinstead.
"$PSScriptRoot\Grpc.IntegrationTests.csproj" `
- Files reviewed: 12/12 changed files
- Comments generated: 0 new
- Review effort level: Lite
Summary
Adds rewind to the sidecar implementation so that the .NET SDK can directly invoke rewind against DTS