Skip to content

Clear persisted instance output during Azure Storage rewind - #1397

Open
wangbill (YunchuWang) wants to merge 8 commits into
mainfrom
yunchuwang-persisted-rewind-output-cleanup
Open

wangbill (YunchuWang) wants to merge 8 commits into
mainfrom
yunchuwang-persisted-rewind-output-cleanup

Conversation

@YunchuWang

@YunchuWang wangbill (YunchuWang) commented Sep 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Remove the persisted Output property from the current Instances row when Azure Storage rewinds the selected execution.
  • Bind the reset to the execution selected from History and use a full-row, exact-ETag replacement, preserving unrelated and unknown properties.
  • Preserve child revival targets when another rewind has already performed an equivalent reset.
  • Preserve existing rewind marker meaning so an explicit parent retry can rediscover stranded descendants after a non-equivalent reset conflict.
  • Retain existing history payloads, blob references, and blob bytes. This revision removes the earlier immediate blob-deletion implementation and its supporting exception/logging changes.

Why this revision is narrower

The original rewind used Azure Table Merge, which updated the status but could not remove the previous Output property. This change fixes the persisted current-instance record, rather than hiding output in an API serializer.

The previous revision also deleted output blobs. Deterministic review probes demonstrated that physical deletion could break an already-started public status query. Immediate physical deletion is no longer part of this PR.

This is not complete failure-data erasure. Rewind continues to rewrite failed history events as before, but retains their payload properties and blob references. Associated blobs remain on the existing lifecycle until an explicit instance purge. No automatic GC, timer, service, cleanup ledger, or new recovery protocol is introduced or assumed.

Execution identity and concurrency

Each recursive rewind captures its execution ID and Instances ETag before changing that node's history. Before replacing the Instances row, the implementation rereads the complete entity and validates that it still belongs to the selected execution.

  • A stale E1 rewind cannot reset or remove output from a newer E2 using the same instance ID.
  • An unchanged pre-failure Pending, Running, or Suspended projection still receives a conditional reset. Its changed ETag rejects a delayed original failure write when UseInstanceTableEtag is enabled.
  • A genuinely changed, equivalent same-execution reset or advanced state is preserved rather than reset again.
  • Failed and Pending-with-Output rows are reset using the latest full row and its exact ETag.
  • A replacement conflict triggers one reread and identity check. Only equivalent same-execution progress is accepted; other conflicts remain explicit errors. There is no wildcard replacement or unbounded retry.

This lets the existing rewind flow retain the child targets it has already discovered and enqueue them after an equivalent competing reset.

Recovering after a failed reset

A same-execution terminal-state repair can change the parent ETag and leave it Failed. That conflict still surfaces as an error, and the service still sends no revival messages until the entire target enumeration succeeds.

To keep a later parent retry useful, this revision preserves and reselects the existing GenericEvent marker with reason Rewound: SubOrchestrationInstanceFailed. It also preserves the original meaning of rewound completion markers instead of overwriting it with Rewound: GenericEvent. No new stored field or recovery record is added.

Historical recovery requires evidence for the child's current execution, including its rewound completion marker and the persisted ExecutionStarted.ParentInstance relationship. Parent instance ID, parent execution ID, and the scheduled child edge must match. A live current-execution completion, unrelated reused execution, or unprovable ownership rejects historical recovery.

  • Pending children with cleared output and stale Failed projections can be reset and recovered.
  • Running or Suspended intermediates are traversed only for eligible descendants; their own state, output, and history are not reset.
  • A branch with no eligible descendant does not suppress the parent's fallback revival target.
  • Operation-local edge state prevents repeated historical markers from multiplying recursive storage work. Genuine live failure rows always perform current child-history work, even if an older marker for the same edge was already traversed.
  • Targets are recorded only after successful recursive enumeration. New live targets are retained; identical targets already emitted by a successful pass of the same edge are not duplicated.

This is explicit retry recovery, not an automatic background recovery service or an internal retry loop. Once contention stops, retrying the parent can rediscover the previously reset descendants.

Regression coverage

The permanent tests cover:

  • Raw Instances Output removal and preservation of unrelated fields.
  • Retention of failure history payloads and both large-result and structured-failure blobs.
  • A public status read paused before downloading its output, then resumed after rewind.
  • A public E1 rewind paused while a same-ID E2 completes, preserving E2 state, output, and completion history.
  • Two concurrent public parent rewinds, actual queued parent/child targets, and successful parent/child completion.
  • A delayed child failure checkpoint with UseInstanceTableEtag=true, including public suspend/resume: stale write rejected with 412, raw Pending/no-Output state, and successful revival.
  • The full current runtime-status matrix, execution changes before/after conflicts, non-equivalent conflicts, missing output, repeated rewind, and later Completed/Failed output.
  • One and three parent terminal-repair conflicts, followed by parent-only retry and actual descendant revival.
  • Pending descendants below Running or Suspended intermediates, without resetting the intermediate.
  • Reused children that have their own rewind markers but belong to a different parent instance, parent execution, or scheduled edge.
  • Fresh terminal evidence, later genuine child failures, and already-completed historical children.
  • Repeated nested historical markers: actual leaf traversal/reset/target counts remain one rather than multiplying.
  • A fresh child checkpoint arriving between historical traversal and a live failure row: live work is still processed after both empty and nonempty earlier recovery passes.

Focused tests passed on both target frameworks: 78 passed, 0 failed, 0 skipped on net8.0; 78 passed, 0 failed, 0 skipped on net48.

To rerun the focused coverage:

dotnet test test\DurableTask.AzureStorage.Tests\DurableTask.AzureStorage.Tests.csproj --framework net8.0 --filter "Name~Rewind|Name~TerminalRepairConflicts|FullyQualifiedName~RewindOutputTrackingStoreTests" --no-restore -p:NoWarn=NU1605 --nologo
dotnet test test\DurableTask.AzureStorage.Tests\DurableTask.AzureStorage.Tests.csproj --framework net48 --filter "Name~Rewind|Name~TerminalRepairConflicts|FullyQualifiedName~RewindOutputTrackingStoreTests" --no-restore -p:NoWarn=NU1605 --nologo

Scope and remaining limitations

  • Azure Storage provider only; this does not change Durable Task Scheduler persistence.
  • History, Instances, and queue writes remain nontransactional. General pre-existing crash windows are not converted into a durable recovery protocol.
  • Recovery requires intact original rewind-marker meaning and identifiable parent-child ownership. It does not reconstruct already ambiguous markers from older builds; recovery behavior during mixed-version deployment remains version-dependent.
  • Explicit parent retry is required after an operation returns an error. No automatic recovery process is introduced.
  • With UseInstanceTableEtag=false, existing unconditional delayed writers still lack conditional-write protection. This PR does not redesign that provider mode.
  • Rows whose execution identity cannot be established fail safely rather than allowing a destructive reset against an unverified execution.
  • After a provider package release containing this fix, azure-functions-durable-extension needs to update its dependency to consume it.

Related: Azure/azure-functions-durable-extension#968

Replace the complete Azure Table instance row with its current ETag so rewind removes the terminal Output property without clobbering concurrent fields. Add unit and Azurite coverage for persisted state, retries, terminal rewrites, instance reuse, and shared large-output blobs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI lite review requested due to automatic review settings September 3, 2026 21:40

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The core behavior change is narrowly scoped, uses ETag-guarded replaces to avoid concurrency clobbering, and is backed by targeted unit and Azurite scenario coverage.

Pull request overview

This PR fixes Azure Storage rewind persistence by ensuring the Instances-table Output property is physically removed when rewinding a terminal orchestration back to Pending, using an ETag-guarded full-entity Replace to avoid clobbering concurrent updates and to preserve unrelated properties.

Changes:

  • Update AzureTableTrackingStore.UpdateStatusForRewindAsync to read the full Instances row, remove Output, and Replace using the current ETag (instead of Merge).
  • Add raw-storage unit coverage for output deletion, idempotency when output is missing, and ETag conflict propagation.
  • Add Azurite scenario coverage for large-output rewinds to ensure the instance Output is removed while the history-referenced blob remains available.
File summaries
File Description
src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs Switch rewind status update from Merge to ETag-guarded Replace and remove persisted Output property.
test/DurableTask.AzureStorage.Tests/RewindOutputTrackingStoreTests.cs Add raw Azurite tests validating Output removal, idempotency, terminal rewrites, and instance ID reuse behavior.
test/DurableTask.AzureStorage.Tests/AzureTableTrackingStoreTest.cs Add mock-based unit tests asserting full-entity Replace with current ETag and 412 propagation behavior.
test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs Add end-to-end coverage for large-output rewind: instance Output cleared while shared history blob remains.
Review details
  • Files reviewed: 4/4 changed files
  • Comments generated: 2
  • Review effort level: Lite

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Remove terminal history payload references before clearing the instance output and deleting the uniquely owned large-message blobs.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: cf71194c-b467-4684-b5f6-f31ff9a2e60e
Copilot AI review requested due to automatic review settings September 4, 2026 17:28
@YunchuWang wangbill (YunchuWang) changed the title Clear persisted output during Azure Storage rewind Delete persisted output during Azure Storage rewind Sep 4, 2026
Comment thread test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Blob deletion exceptions can abort the rewind after storage pointers are removed, leaving the instance rewound but not revived; making deletion best-effort (or otherwise non-fatal post-pointer-removal) would improve reliability.

Once you've addressed the issues Copilot identified, you can request another Copilot review.

Review details
  • Files reviewed: 5/5 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs Outdated
Normalize ordinary blob retry and timeout failures, log them after table dereference, and continue producing rewind targets while preserving caller cancellation.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b4b381ba-7e3f-4548-ab7d-73eac7a4466a
Copilot AI review requested due to automatic review settings September 4, 2026 19:11
@YunchuWang

Copy link
Copy Markdown
Member Author

Copilot review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

The changes affect core rewind/persistence semantics and storage cleanup ordering in production and should receive final human validation despite strong test coverage.

Review details
  • Files reviewed: 8/8 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI commented Sep 4, 2026

Copy link
Copy Markdown

Copilot review

Review completed on d090a750; it generated 0 new code comments. I also checked workflow status: the completed code-quality run has no failed jobs, and the CodeQL runs are still in progress.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b4b381ba-7e3f-4548-ab7d-73eac7a4466a
Copilot AI review requested due to automatic review settings September 8, 2026 18:10
@YunchuWang wangbill (YunchuWang) changed the title Delete persisted output during Azure Storage rewind Clear persisted instance output during Azure Storage rewind Sep 8, 2026
@YunchuWang
wangbill (YunchuWang) marked this pull request as ready for review September 8, 2026 18:12

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The functional change (removing persisted Output via ETag-bound replace while preserving other fields) is implemented coherently and is backed by comprehensive unit and scenario test coverage for concurrency and regression cases.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Comment thread test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs
Comment thread test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs
Comment thread test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs Fixed
Comment thread test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs Fixed
Comment thread test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs Fixed
Comment thread test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs Fixed
Comment thread test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs Fixed
Comment thread test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs Fixed
Comment thread test/DurableTask.AzureStorage.Tests/AzureStorageScenarioTests.cs Fixed
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b4b381ba-7e3f-4548-ab7d-73eac7a4466a
Copilot AI review requested due to automatic review settings September 8, 2026 18:35
@YunchuWang

Copy link
Copy Markdown
Member Author

Addressed the test-code review comments in 5acbb3f. Injected handlers and cached HTTP transports now have explicit test-scoped ownership; DelegatingHandler disposes its inner handler through the normal base disposal chain, without duplicate manual disposal. Added explicit non-null assertions, preserved absent-versus-null history checks with TryGetValue, and removed the shadowed local name. This follow-up changes tests only; the narrowed production implementation is unchanged.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟢 Approval recommended

The change is narrowly scoped to correct a known Azure Table merge limitation, adds execution/ETag fencing for safety, and includes substantial unit and scenario regression coverage for the new behavior.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@YunchuWang
wangbill (YunchuWang) marked this pull request as draft September 8, 2026 21:28
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b4b381ba-7e3f-4548-ab7d-73eac7a4466a
Copilot AI review requested due to automatic review settings September 9, 2026 00:43
@YunchuWang
wangbill (YunchuWang) marked this pull request as ready for review September 9, 2026 00:44
Comment thread src/DurableTask.AzureStorage/Tracking/AzureTableTrackingStore.cs Fixed

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It modifies core rewind/concurrency behavior in the Azure Storage provider with intricate recovery logic, so it warrants final human review despite strong test coverage.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b4b381ba-7e3f-4548-ab7d-73eac7a4466a
Copilot AI review requested due to automatic review settings September 9, 2026 01:06

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔵 Needs a closer look

It makes substantial, concurrency-sensitive changes to the Azure Table rewind algorithm and instance mutation semantics that warrant final human review despite strong test coverage.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: b4b381ba-7e3f-4548-ab7d-73eac7a4466a
Copilot AI review requested due to automatic review settings September 14, 2026 18:08

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Three unresolved critical issues remain in AzureTableTrackingStore.cs.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 7/7 changed files
  • Comments generated: 3
  • Review effort level: Lite

Comment on lines +466 to +473
TableEntity liveChildRewindStartEntity =
await this.GetInstanceEntityForRewindAsync(childInstanceId, cancellationToken);
var liveEdge = new RewindRecoveryEdge(
instanceId,
executionId,
taskScheduledId.GetValueOrDefault(),
childInstanceId,
liveChildRewindStartEntity.GetString(nameof(OrchestrationInstance.ExecutionId)));
Comment on lines 592 to 596
// "clear" failure event by making RewindEvent: replay ignores row while dummy event preserves rowKey
entity[nameof(TaskFailedEvent.Reason)] = "Rewound: " + entity.GetString(nameof(HistoryEvent.EventType));
entity[nameof(TaskFailedEvent.Reason)] = RewoundReasonPrefix + eventType;
entity[nameof(TaskFailedEvent.EventType)] = nameof(EventType.GenericEvent);

await this.HistoryTable.ReplaceEntityAsync(entity, entity.ETag, cancellationToken);
Comment on lines +699 to +700
$"{AzureTableQueryFilter.ColumnEquals(nameof(OrchestrationInstance.ExecutionId), executionId)} and " +
$"{AzureTableQueryFilter.ColumnEquals(nameof(HistoryEvent.EventType), nameof(EventType.ExecutionCompleted))}";
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants