Skip to content

feat: add .NET-aligned large payload storage and auto-purge - #387

Closed
wangbill (YunchuWang) wants to merge 3 commits into
mainfrom
yunchuwang-large-payload-parity
Closed

wangbill (YunchuWang) wants to merge 3 commits into
mainfrom
yunchuwang-large-payload-parity

Conversation

@YunchuWang

@YunchuWang wangbill (YunchuWang) commented Oct 16, 2026 •

Copy link
Copy Markdown
Member

Summary

What changed?

  • Add opt-in external payload storage to the core gRPC client and worker, plus the optional @microsoft/durabletask-js-azureblob package. Core users do not acquire Azure Storage dependencies.
  • Transparently externalize/hydrate orchestration, activity, history, custom-status, management, and entity V1/V2 payloads. Limit payload I/O to eight operations per message, drain started work on failure/cancellation, and convert permanent activity/orchestration upload failures into non-retriable completions.
  • Implement the DTS auto-purge lifecycle: public setting API, reserved unversioned runner/activities, authenticated reconnect-safe transport, capability/filter integration, conditional ownership-checked deletion, retry/quarantine classification, and configuration-preserving continue-as-new.
  • Add regression tests, real .NET gzip fixtures, a runnable example, API documentation/changelogs, and new-package build/release wiring. The lockfile reuses existing Azure dependency versions.
  • Reserve core 0.4.1 for the new payload APIs. Both affected extensions require core >=0.4.1, and the Functions provider's exact core pin is synchronized. Real packed-consumer checks reject the incompatible published 0.4.0 and accept/load the new core and extensions.
  • Keep the public payload exception constructor structural ({ cause?: unknown }), so ES2020 declaration consumers do not need the global ES2022 ErrorOptions type.

Why is this change needed?

  • Large serialized values otherwise remain inline in gRPC/history messages. Applications need transparent out-of-band payloads without changing orchestration code, and instance purge needs a safe way to reclaim tracked external blobs.
  • The behavioral reference is microsoft/durabletask-dotnet at 91739687ff647699079cdadd627daa00562982b7, including its interceptor, storage, and auto-purge tests.
  • Match the reference defaults and wire contract: inclusive 256 KiB serialized UTF-8 threshold, 1 MiB maximum threshold, 10 MiB default payload limit, gzip, private durabletask-payloads container, managed_by=dts, credential-free v2 URLs, and legacy v1 reads. Cross-account access requires identity; configured account keys/SAS are not forwarded to another endpoint.

Issues / work items


Project checklist

  • Release notes are not required for the next release
    • Otherwise: Notes added to CHANGELOG.md
  • Backport is not required
    • Otherwise: Backport tracked by issue/PR #issue_or_pr
  • All required tests have been added/updated (unit tests, E2E tests)
  • Breaking change?
    • If yes:
      • Impact: The new Azure-managed package release requires core >=0.4.1 because its public payload option types do not exist in published 0.4.0. The new Blob extension has the same minimum. Existing core APIs and disabled runtime behavior are preserved.
      • Migration guidance: Publish/install core 0.4.1 before the dependent packages; the Functions provider exact-pins 0.4.1. Configure storage on every participating client/worker before writing externalized payloads, and enable auto-purge separately.

AI-assisted code disclosure (required)

Was an AI tool used? (select one)

  • No
  • Yes, AI helped write parts of this PR (e.g., GitHub Copilot)
  • Yes, an AI agent generated most of this PR

If AI was used:

  • Tool(s): GitHub Copilot.
  • AI-assisted areas/files: Payload contracts/transport, Azure Blob extension, auto-purge/protobuf integration, tests, example, documentation, and package/release wiring.
  • What you changed after AI output: Iterative fixes and regression coverage for shared-initialization cancellation, Azure SDK bodyless HEAD error codes, endpoint/name-escaping separation, streamed download retryability, 150,000-element history/entity messages, minimum-core peer resolution, and ES2020 public declarations. Human review is still required.

AI verification (required if AI was used):

  • I understand the code and can explain it
  • I verified referenced APIs/types exist and are correct
  • I reviewed edge cases/failure paths (timeouts, retries, cancellation, exceptions)
  • I reviewed concurrency/async behavior
  • I checked for unintended breaking or behavior changes

The human attestations above are intentionally left for the maintainer. Automated evidence is recorded below.


Testing

Automated tests

  • Result: Passed locally.
  • npm ci --offline --no-audit --no-fund — passed against the minimal lockfile.
  • npm run test:unit — passed: 3,001 tests across 131 suites. The 10 opt-in Azurite tests were skipped in this command and run separately below.
  • npm --registry=https://packagefeedproxy.microsoft.io/npm run test:payload-packages -- $oldCoreArchive ($oldCoreArchive is the actual npm-published microsoft-durabletask-js-0.4.0.tgz) — passed. npm rejects both extensions with old core using ERESOLVE; the new packed core/extensions install and load; public declarations compile with lib: ES2020, strict checking, skipLibCheck: false, and no ambient ES2022 ErrorOptions; runtime error causes remain intact. Before the fixes, the check reproduced both acceptance of old core and TS2304 from the emitted declaration.
  • npm --prefix test\e2e-functions\test-app run build — passed without changing the test-app's compiler target or libraries.
  • npm run test:unit -w @microsoft/durabletask-js -- --runTestsByPath test\payloads.spec.ts test\client-error-cause.spec.ts test\orchestration-id-reuse-policy.spec.ts — passed: 84 tests after the declaration repair.
  • npm run test:unit -w @microsoft/durabletask-js-azureblob — passed: 39 tests after the declaration repair; the 10 service-dependent tests remain opt-in.
  • npm run test:azurite -w @microsoft/durabletask-js-azureblob with a local-only DURABLETASK_BLOB_TEST_CONNECTION_STRING — passed: all 10 tests, including actual .NET-produced gzip blobs and ownership/snapshot cleanup.
  • npm run build — passed for all five workspaces.
  • npm run lint — passed.
  • node_modules\.bin\prettier.cmd --check $files ($files = staged TS/JS/JSON/Markdown files, excluding generated protobuf bindings) — passed.
  • git diff --check origin/main...HEAD — passed.
  • bash.exe -n tools\generate-grpc-javascript.sh — passed.
  • npm pack --dry-run --json --workspace @microsoft/durabletask-js --workspace @microsoft/durabletask-js-azureblob — passed; core includes the new protocol, extension includes its compiled entry points, and test/reference artifacts are excluded.
  • Additional checks: release YAML parsed with js-yaml; the new example type-checked with TypeScript; generated JS/.NET purge protocol descriptors compared byte-for-byte and matched (SHA-256 5DDA0563E9A4F61340C6A975AF8CB5994863766674F24C0B561AB037C3335569).

Manual validation (only if runtime/behavior changed)

  • Environment (OS, Node.js version, components): Windows, Node.js v24.14.0, Azure Storage Blob SDK 12.31.0, local Azurite 3.37.0, loopback gRPC test servers.
  • Steps + observed results:
    1. Ran the real Azure SDK pipeline against isolated in-memory Azurite: UTF-8/gzip and uncompressed round trips, v1/v2 reads, same-account container rotation, .NET fixture hashes, owned-blob/snapshot deletion, and preservation of unowned blobs passed.
    2. Exercised client/worker/query/history and auto-purge RPCs through loopback gRPC tests: awaited hydration, request/response transformation, authenticated metadata/deadlines, start verification, and error behavior passed.
    3. Ran all owning unit suites, build/type-check, lint, formatting, package-content, and protocol-contract checks listed above.
  • Evidence (optional): Reconstructible .NET 10.0.13 gzip fixtures are checked in under the blob package's tests. Full local logs are retained in the Copilot session, not committed.

Notes for reviewers

  • Disabled runtime behavior remains unchanged; the supported core package minimum is intentionally raised as described above. Payload-enabled single-instance purges additionally set the DTS orchestration/entity-kind flag from the instance ID; existing completion-status and timeout behavior are preserved.
  • Auto-purge is separately opt-in and backend-controlled. Enabling writes the setting first, then starts/verifies/configures the fixed runner; failures do not roll back earlier successful steps. Disabling only writes the setting.
  • Cleanup never deletes an unowned blob and conditions deletion on the ownership read's ETag. Legacy v1 references are not automatically deleted; uncertain failures remain retryable. Soft delete and retained blob versions can continue consuming storage.
  • New protocol bindings were generated using the repository's protoc tools, not hand-authored. The generation script now includes the DTS-only contract and normalizes generator-emitted trailing whitespace.
  • Service-dependent limitation: local validation did not run an actual DTS container/cloud task hub or live mixed-.NET/JS orchestration because Docker Desktop's daemon is unavailable. Protocol compatibility, real Blob SDK/Azurite behavior, .NET blob fixtures, and loopback transport behavior were verified. Hosted CI passed the existing DTS and Functions E2E suites on final head c9b53fc; live mixed-.NET/JS large-payload execution remains unverified.
  • The DTS sample is intentionally excluded from the existing single-service sample CI matrix because it also needs Blob/Azurite configuration. It does not silently enable task-hub-wide cleanup.
  • Core 0.4.1 is the first payload-capable source version and was verified not already published/tagged. Prepare that first core release with explicit version: 0.4.1, then publish the extensions/Functions provider. Only core's own version advances here; extension version numbers stay independent. No npm package was published by this PR.
  • The Functions E2E job on intermediate head d759df4 exposed an ES2022 ErrorOptions leak in the public declaration. The structural signature repair is now covered by actual packed ES2020 consumers and the unchanged test-app build. All 30 executed hosted checks passed on final head c9b53fc (one inapplicable sample lane skipped); human review attestations remain unchecked.

Add opt-in payload externalization and hydration across client, worker, history, and entity transports, an optional Azure Blob store, and the DTS auto-purge lifecycle aligned with durabletask-dotnet 91739687ff647699079cdadd627daa00562982b7.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 81e20512-d3a2-4f8e-a574-f396140ec9c7
Copilot AI balanced review requested due to automatic review settings October 16, 2026 18:17
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.microsoft.durabletask.largepayloads.SetLargePayloadAutoPurgeRequest.toObject = function(includeInstance, msg) {
var f, obj = {
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.microsoft.durabletask.largepayloads.SetLargePayloadAutoPurgeResponse.toObject = function(includeInstance, msg) {
var f, obj = {
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.microsoft.durabletask.largepayloads.SetLargePayloadAutoPurgeResponse.serializeBinaryToWriter = function(message, writer) {
var f = undefined;
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.microsoft.durabletask.largepayloads.LargePayloadTombstone.toObject = function(includeInstance, msg) {
var f, obj = {
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.microsoft.durabletask.largepayloads.LargePayloadPurgeResult.toObject = function(includeInstance, msg) {
var f, obj = {
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.microsoft.durabletask.largepayloads.GetLargePayloadTombstonesRequest.toObject = function(includeInstance, msg) {
var f, obj = {
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.microsoft.durabletask.largepayloads.GetLargePayloadTombstonesResponse.toObject = function(includeInstance, msg) {
var f, obj = {
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.microsoft.durabletask.largepayloads.ReportLargePayloadPurgeResultsRequest.toObject = function(includeInstance, msg) {
var f, obj = {
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.microsoft.durabletask.largepayloads.ReportLargePayloadPurgeResultsResponse.toObject = function(includeInstance, msg) {
var f, obj = {
* @suppress {unusedLocalVariables} f is only used for nested messages
*/
proto.microsoft.durabletask.largepayloads.ReportLargePayloadPurgeResultsResponse.serializeBinaryToWriter = function(message, writer) {
var f = undefined;

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

Carryover payload externalization and the extension’s minimum core dependency need correction before approval.

2 open findings
What changed in this PR

Adds opt-in external payload storage and automatic cleanup to the Durable Task SDK, with an optional Azure Blob package that keeps storage dependencies out of core.

Changes:

  • Integrates payload externalization and hydration into clients, workers, and Azure-managed builders.
  • Adds .NET-compatible Blob storage and the DTS auto-purge lifecycle.
  • Adds regression tests, interoperability fixtures, documentation, a sample, and release wiring.
File Description
tsconfig.base.json Adds Azure Blob package mapping.
tools/​generate-grpc-javascript.sh Generates purge bindings and normalizes whitespace.
README.md Introduces payload configuration.
packages/​durabletask-js/​test/​payloads.spec.ts Tests payload processing and bounded concurrency.
packages/​durabletask-js/​test/​payload-transport.spec.ts Tests gRPC payload integration.
packages/​durabletask-js/​test/​payload-purge.spec.ts Tests cleanup lifecycle and runner behavior.
packages/​durabletask-js/​test/​client-purge.spec.ts Tests payload-enabled purge routing.
packages/​durabletask-js/​src/​worker/​task-hub-grpc-worker.ts Integrates payload processing and purge tasks.
packages/​durabletask-js/​src/​proto/​large_payload_purge_pb.d.ts Adds generated purge message types.
packages/​durabletask-js/​src/​proto/​large_payload_purge_grpc_pb.js Adds generated purge RPC bindings.
packages/​durabletask-js/​src/​proto/​large_payload_purge_grpc_pb.d.ts Adds generated purge service types.
packages/​durabletask-js/​src/​payloads/​run-bounded.ts Bounds concurrent storage operations.
packages/​durabletask-js/​src/​payloads/​purge.ts Implements the durable cleanup runner.
packages/​durabletask-js/​src/​payloads/​purge-worker.ts Registers cleanup activities and classifies outcomes.
packages/​durabletask-js/​src/​payloads/​purge-transport.ts Reuses authenticated transport for purge RPCs.
packages/​durabletask-js/​src/​payloads/​payload-store.ts Defines storage contracts and options.
packages/​durabletask-js/​src/​payloads/​payload-processor.ts Maps protobuf payload fields for storage.
packages/​durabletask-js/​src/​payloads/​history-stream.ts Hydrates streamed history with backpressure.
packages/​durabletask-js/​src/​index.ts Exports public payload APIs.
packages/​durabletask-js/​src/​client/​client.ts Integrates payload processing and auto-purge configuration.
packages/​durabletask-js-azuremanaged/​test/​unit/​payload-builders.spec.ts Tests builder configuration preservation.
packages/​durabletask-js-azuremanaged/​src/​worker-builder.ts Adds worker payload configuration.
packages/​durabletask-js-azuremanaged/​src/​client-builder.ts Adds client payload configuration.
packages/​durabletask-js-azuremanaged/​README.md Documents builder options.
packages/​durabletask-js-azuremanaged/​CHANGELOG.md Records builder support.
packages/​durabletask-js-azureblob/​tsconfig.json Configures package TypeScript sources.
packages/​durabletask-js-azureblob/​tsconfig.build.json Configures compiled package output.
packages/​durabletask-js-azureblob/​test/​dotnet-payload-fixtures.json Adds .NET gzip interoperability vectors.
packages/​durabletask-js-azureblob/​test/​blob-payload-store.spec.ts Tests storage, authentication, and deletion.
packages/​durabletask-js-azureblob/​test/​azurite.spec.ts Adds optional local storage integration tests.
packages/​durabletask-js-azureblob/​src/​index.ts Exports the Blob store.
packages/​durabletask-js-azureblob/​src/​blob-payload-store.ts Implements Blob storage and conditional cleanup.
packages/​durabletask-js-azureblob/​README.md Documents configuration and cleanup contracts.
packages/​durabletask-js-azureblob/​package.json Defines package dependencies and publishing metadata.
packages/​durabletask-js-azureblob/​LICENSE Adds the package license.
packages/​durabletask-js-azureblob/​jest.config.js Configures package tests.
packages/​durabletask-js-azureblob/​CHANGELOG.md Records initial functionality.
package-lock.json Registers the new workspace package.
jest.config.js Adds Azure Blob test mapping.
internal/​protocol/​protos/​large_payload_purge.proto Defines the DTS cleanup protocol.
examples/​azure-managed/​README.md Lists the payload sample.
examples/​azure-managed/​large-payloads/​sample.json Declares sample infrastructure requirements.
examples/​azure-managed/​large-payloads/​README.md Documents sample setup and cleanup.
examples/​azure-managed/​large-payloads/​index.ts Demonstrates a large-payload round trip.
eng/​templates/​build.yml Packages and stages the extension.
eng/​ci/​release.yml Adds extension publishing support.
CHANGELOG.md Records payload and auto-purge features.
.github/​workflows/​prepare-release.yaml Adds independent extension release preparation.

🧠 Review effort: Balanced


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

"@azure/storage-blob": "^12.26.0"
},
"peerDependencies": {
"@microsoft/durabletask-js": ">=0.4.0"
Comment on lines +58 to +60
for (const event of action.getRewindorchestration()?.getNewhistoryList() ?? []) {
this._historyFields(event, fields);
}
wangbill (YunchuWang) and others added 2 commits October 16, 2026 14:48
Reserve core 0.4.1 for the new payload APIs, raise both affected extension peer floors, and keep the Functions provider's exact core pin synchronized. Add packed-consumer npm checks rejecting published core 0.4.0 and loading the new core with both extensions.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 81e20512-d3a2-4f8e-a574-f396140ec9c7
Expose structural cause options instead of the ambient ES2022 ErrorOptions type. Extend the packed-consumer check to compile public declarations with ES2020 and skipLibCheck=false, verify the ES2022 global is absent, and retain runtime error-cause behavior.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 81e20512-d3a2-4f8e-a574-f396140ec9c7
@YunchuWang

Copy link
Copy Markdown
Member Author

Superseded by six focused, dependency-stacked PRs:

Order PR Review scope
1 #388 Core payload contracts, mapping and bounded I/O
2 #389 Client/worker/history integration and Azure-managed builders
3 #390 Optional interoperable Azure Blob provider
4 #391 Purge protocol, generated bindings and client lifecycle
5 #392 Persistent cleanup worker and reserved tasks
6 #393 Release wiring, usage documentation and packed consumers

Each PR targets its predecessor, so its diff is phase-specific rather than cumulative against main. Review/merge in order 1 -> 6. After a prerequisite merges, synchronize the next branch with main and retarget its PR before merging it; do not merge a downstream phase into its temporary predecessor branch.

The complete feature is preserved, with the management-only purge-kind correction in phase 4 and unrelated formatting noise removed. Aggregate local checks and per-phase evidence are documented in the replacements. The final tip 72c74983b940ec1a4c829f3e4f90e53e73efebc5 also passed all 10 jobs in the existing hosted DTS integration run; validation context distinguishes this from workflows gated to main and unverified live mixed-.NET/JS execution.

Closing this oversized PR in favor of the reviewable stack. The original c9b53fc commit and branch are retained as a reference. No PR has been merged and no npm package has been published. Do not publish incomplete intermediate phases.

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