Add large-payload blob auto-purge (worker/SDK side)#758
Draft
YunchuWang wants to merge 1 commit into
Draft
Conversation
82ae04d to
0ac2dc3
Compare
fc9b8a1 to
da0f8aa
Compare
Implements the worker/SDK side of large-payload blob auto-purge. The DTS backend soft-deletes blob-externalized payload rows and streams their tokens to the worker over a bidirectional gRPC stream; the worker deletes the backing Azure blobs immediately, acks, and the backend then hard-deletes the row. - PayloadStore: add virtual DeleteAsync (default throws NotSupportedException, so existing external subclasses keep compiling); BlobPayloadStore overrides it to decode the blob:v1: token and call DeleteIfExistsAsync (idempotent). - BlobPayloadAutoPurgeService: public BackgroundService that opens one bidi PurgeExternalPayloads stream, deletes + acks per tombstone, logs-and-continues on a bad token (no ack, so the backend can re-push), and reconnects with jittered backoff. - The service is independently constructable from a CallInvoker (or gRPC channel) + PayloadStore, so hosts that enable large payloads outside UseExternalizedPayloads (e.g. the DTS Azure Managed worker SDK) can reuse it. - Auto-registered from UseExternalizedPayloads for the OSS path (gated entirely on enable; resolves the worker's existing DTS CallInvoker lazily at start). - Proto: add PurgeExternalPayloads rpc + TombstonedPayload/PayloadPurged messages (authoritative change is a follow-up in microsoft/durabletask-protobuf). - Add BlobPayloadStore.DeleteAsync + BlobPayloadAutoPurgeService construction tests. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
da0f8aa to
0752610
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Implements the worker/SDK side of large-payload blob auto-purge in the existing
Microsoft.DurableTask.Extensions.AzureBlobPayloadspackage (shipped as a new version — no new package).Large orchestration payloads are externalized to Azure Blob Storage as tokens
blob:v1:<container>:<blobName>. The DTS backend stores those tokens but cannot delete the blobs — it has no storage credentials. New design: the backend soft-deletes blob-externalized payload rows and streams their tokens to the worker over a bidirectional gRPC stream; the worker deletes the blobs immediately and acks; the backend then hard-deletes the row. This PR implements the worker side.There is no customer-facing retention config, no options, and no separate enable API — the backend owns retention and decides what to purge; the SDK just drains whatever the backend streams. On the OSS worker everything is gated on the existing
UseExternalizedPayloads(...)call.Changes
PayloadStore.DeleteAsync(token, ct)— newvirtualmethod on the payload-store abstraction, with a default that throwsNotSupportedExceptionso existing external subclasses keep compiling (non-breaking).BlobPayloadStoreoverrides it: decodes theblob:v1:token to container + blob name and callsDeleteIfExistsAsync(idempotent: deleting a missing blob is a no-op, so redelivered tombstones and concurrent replicas are safe).BlobPayloadAutoPurgeService(public sealed BackgroundService) — opens one bidirectional gRPC stream (PurgeExternalPayloads). Push model: blocks awaiting server pushes when idle (not a polling loop). For each pushed tombstone it deletes the blob then acks on the same stream. A failed delete for one token is logged and skipped without acking — so the backend keeps the row soft-deleted and can re-push it — and the stream keeps draining (one bad token can't kill it). Resilient reconnect with jittered backoff (1s→30s cap) on stream drop/error; stops cleanly on host shutdown via the stopping token.CallInvoker(or gRPCChannelBase) + aPayloadStore+ an optionalILogger(defaults toNullLogger), so hosts that enable large payloads through a different code path (e.g. the DTS Azure Managed worker SDK, which wires payloads via its ownAzureBlobPayloadCallInvokerFactoryrather thanUseExternalizedPayloads) can construct it directly and start/stop it viaIHostedService. No dependency on the OSS DI path.UseExternalizedPayloads(...)the service is registered via an internal constructor that resolves the sameCallInvokerthe worker already uses to reach DTS, lazily at start. If the enable API is never called, the service is never registered and never runs.PurgeExternalPayloadsrpc plusTombstonedPayload(server→client) andPayloadPurgeAck(client→server ack) messages toTaskHubSidecarServiceinsrc/Grpc/orchestrator_service.proto(the service the worker already dials). Existing field numbers are preserved; field names usecamelCaseto match the rest of that file.BlobPayloadStore.DeleteAsync(token decode, correct blob targeted, idempotency, container-mismatch / invalid-token validation) and forBlobPayloadAutoPurgeServiceconstruction from aCallInvoker/ChannelBase+PayloadStore(locks in the public reuse contract, plain-ILoggeracceptance + null-arg guards), in a newtest/AzureBlobPayloads.Testsproject.gRPC contract
The SDK/worker is the gRPC client dialing DTS on the worker-facing
TaskHubSidecarService(wire path/TaskHubSidecarService/PurgeExternalPayloads):Placement on
TaskHubSidecarServiceis confirmed correct: the durabletask-dotnet worker (includingWorker.AzureManaged) speaks only this service. Field names usecamelCaseto matchorchestrator_service.protoconvention; field numbers/types are wire-compatible with the backend. The ack message is namedPayloadPurgeAck(an acknowledgement sent upstream by the worker) to fit the file's naming and de-confuse the bidi signature.Notes
CallInvoker(guaranteed set after theUseExternalizedPayloadspost-configure). The payload interceptor only wraps unary/server-streaming calls, so the bidi purge stream passes through untouched.PayloadStore.DeleteAsyncisvirtual(notabstract) — no breaking change for externalPayloadStoresubclasses.netstandard2.0;net6.0;net8.0;net10.0); the service avoids APIs unavailable on netstandard2.0.Verification
dotnet build Microsoft.DurableTask.sln— succeeds (0 errors).dotnet test test/AzureBlobPayloads.Tests— 11/11 pass.Draft — kept as a draft while the backend PR lands.