Skip to content

Clear persisted output during Azure Storage rewind - #1397

Draft
wangbill (YunchuWang) wants to merge 1 commit into
mainfrom
yunchuwang-persisted-rewind-output-cleanup
Draft

Clear persisted output during Azure Storage rewind#1397
wangbill (YunchuWang) wants to merge 1 commit into
mainfrom
yunchuwang-persisted-rewind-output-cleanup

Conversation

@YunchuWang

Copy link
Copy Markdown
Member

Summary

  • Remove the persisted Output property when Azure Storage rewinds a failed orchestration to Pending.
  • Read the complete Instances row and use an ETag-guarded Azure Table Replace, preserving unrelated and unknown properties while rejecting concurrent changes with 412 instead of clobbering them.
  • Add raw-storage unit and Azurite coverage for property deletion, ETag conflicts, missing output, repeated rewind, later terminal output, same-instance-ID reuse, and large output storage.

Persisted behavior

This is deletion at the provider persistence source, not API-layer hiding. Before this change, UpdateStatusForRewindAsync used Merge, which could update RuntimeStatus but could not remove the existing Output property. After this change, the Instances row has no Output property while the rewound orchestration is pending. A later Completed or Failed checkpoint writes its new output normally.

No host, client, serializer, or API projection suppression is added.

Large-output blobs

The large-output blob is not deleted by this change. The Instances-table URL is deleted, but the exact gzip blob remains because the rewound ExecutionCompleted history row still stores ResultBlobName, and history loading eagerly dereferences that property before deserializing the rewritten GenericEvent.

Deleting that blob without first coordinating removal of every history reference can break replay/history reads. Azure Table and Blob updates are not transactional, so crash- and retry-safe reclamation needs a durable cleanup ledger plus execution/write fencing (or a broader history-storage design change). The focused regression verifies that the exact history-referenced blob remains available. Existing purge behavior still removes all blobs for the instance.

Existing instance ID reuse

Current main already handles an accepted new execution safely: SetNewExecutionAsync builds a fresh row and ETag-replaces the old row, which removes the previous Output. A raw Azurite regression now protects that invariant; no additional production change was needed for this path.

Tests

  • dotnet test Test\DurableTask.AzureStorage.Tests\DurableTask.AzureStorage.Tests.csproj -f net8.0 --filter "Name~Rewind" --no-restore -p:NoWarn=NU1605
  • dotnet test Test\DurableTask.AzureStorage.Tests\DurableTask.AzureStorage.Tests.csproj -f net48 --filter "Name~Rewind" --no-restore -p:NoWarn=NU1605
  • dotnet test Test\DurableTask.AzureStorage.Tests\DurableTask.AzureStorage.Tests.csproj -f net8.0 --filter "FullyQualifiedName~RewindOutputTrackingStoreTests" --no-restore -p:NoWarn=NU1605
  • dotnet test Test\DurableTask.AzureStorage.Tests\DurableTask.AzureStorage.Tests.csproj -f net48 --filter "FullyQualifiedName~RewindOutputTrackingStoreTests" --no-restore -p:NoWarn=NU1605

Consumption

After this change ships in a new Microsoft.Azure.DurableTask.AzureStorage package, Azure/azure-functions-durable-extension must update its provider dependency from 2.9.1 to that released version to consume the fix.

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.

@@ -0,0 +1,179 @@
// ----------------------------------------------------------------------------------
Comment on lines +124 to +127
bool created = await this.trackingStore.SetNewExecutionAsync(
CreateExecutionStartedEvent(instanceId, "execution-2"),
new ETag(existing.ETag.ToString()),
inputPayloadOverride: null);
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.

2 participants