Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
306d19f
Add opt-in blob payload auto-purge job to AzureBlobPayloads
YunchuWang Jul 8, 2026
60f6637
Address PR #758 review feedback: naming, self-heal, poison-ack, start…
YunchuWang Jul 14, 2026
149c63a
Refine BlobPurgeJobStarter: pre-check bridge status before rescheduling
YunchuWang Jul 14, 2026
4d52005
Classify RequestFailedException 400 as permanent in DeleteExternalBlo…
YunchuWang Jul 14, 2026
780d743
Reuse shared PayloadStore and register purge starter conditionally on…
YunchuWang Jul 14, 2026
3a2215c
Register fallback PayloadStore in shared Core for both client and worker
YunchuWang Jul 14, 2026
3fbf061
Merge branch 'main' into yunchuwang-wangbill-blob-payload-autopurge-sdk
YunchuWang Jul 14, 2026
47651dc
Stop self-registering PayloadStore on the client; consume the shared …
YunchuWang Jul 14, 2026
7397fa6
Validate PayloadPurgeBatchSize once at specification (fail fast on ou…
YunchuWang Jul 15, 2026
4afeb8a
Translate gRPC Cancelled to OperationCanceledException in GetTombston…
YunchuWang Jul 15, 2026
e74f633
Raise auto-purge MaxBatchSize to 1000 (inclusive); relax gRPC GetTomb…
YunchuWang Jul 15, 2026
50ae944
Register PayloadStore in the client builder extension (symmetry with …
YunchuWang Jul 30, 2026
a680442
Merge branch 'main' into yunchuwang-wangbill-blob-payload-autopurge-sdk
YunchuWang Jul 31, 2026
a5ed298
Resolve v2 tokens in DeleteAsync and discard payloads in unreachable …
YunchuWang Jul 31, 2026
fff06b0
Align unreachable-account log, exception wording and v2 delete test n…
YunchuWang Jul 31, 2026
6e4f5d0
Gate blob auto-purge on v2 tokens and deleting stores
YunchuWang Jul 31, 2026
65e9cbb
Fix auto-purge starter registration and client resolution
YunchuWang Jul 31, 2026
8f436df
Back off on zero-ack purge cycles and document TokenPrefixV1
YunchuWang Jul 31, 2026
81284e0
docs(AzureBlobPayloads): reframe v1-token handling as a defensive guard
YunchuWang Aug 2, 2026
a15c04d
Merge branch 'main' into yunchuwang-wangbill-blob-payload-autopurge-sdk
berndverst Aug 4, 2026
ae5ed1a
Merge branch 'main' into yunchuwang-wangbill-blob-payload-autopurge-sdk
berndverst Aug 4, 2026
1fcdb10
Reshape large-payload auto-purge to the finalized design
YunchuWang Aug 11, 2026
2026d71
Clear three new-code warnings in the auto-purge files
YunchuWang Aug 11, 2026
e25e724
Pin the no-inbound-enum invariant the numeric purge casts rely on
YunchuWang Aug 11, 2026
7bf5da8
Clear the two remaining PR-introduced style warnings
YunchuWang Aug 11, 2026
de218c8
Narrow the purge reason enum from 11 values to 7
YunchuWang Aug 11, 2026
02b957c
Re-sync purge reason comments from canonical contract
YunchuWang Aug 11, 2026
e432d25
Remove purge reason and storage error code from the contract
YunchuWang Aug 11, 2026
1ac131f
Dedupe the purge bridge in the backend instead of checking first
YunchuWang Aug 12, 2026
ea0b4c7
Dedupe the purge bridge on Pending and Running only
YunchuWang Aug 12, 2026
3c5c7ba
Add Stop operation and make the purge batch size configurable at runtime
YunchuWang Aug 12, 2026
7d409ab
Read the purge job state before signalling Stop
YunchuWang Aug 13, 2026
7e0d0ed
Self-heal the blob purge job by re-signalling Run on every host start
YunchuWang Aug 13, 2026
720e028
Make LastModifiedAt mean a real change, and drop internal backend sym…
YunchuWang Aug 13, 2026
2b389fa
Drop an uncorrelatable backend column name from a proto comment
YunchuWang Aug 13, 2026
3dd2b14
Reconcile the blob auto-purge job periodically instead of once per ho…
YunchuWang Aug 13, 2026
3a1b673
Correct comments that claimed the purge starter defers client constru…
YunchuWang Aug 13, 2026
4cc0baa
Enable entity support when externalized payloads are configured
YunchuWang Aug 14, 2026
b8e581b
Purge externalized blobs over the worker's own gRPC transport
YunchuWang Aug 15, 2026
51a0f1c
Collapse purge client abstraction into direct sidecar client injection
YunchuWang Aug 15, 2026
00cbb68
Disable blob auto-purge on backends missing the purge RPCs
YunchuWang Aug 16, 2026
d146c50
Batch blob-purge deletes into chunks and bound each delete's wall-clo…
YunchuWang Aug 16, 2026
7833e11
Address three review items: version/soft-delete docs, shared limit co…
YunchuWang Aug 17, 2026
46a4246
Test named-builder, combined-host, empty-fetch, and continue-as-new p…
YunchuWang Aug 17, 2026
0857bde
docs: correct blob soft delete/versioning to storage-account-level scope
YunchuWang Aug 17, 2026
720368e
docs: document delete guarantee on base PayloadStore contract
YunchuWang Aug 17, 2026
86049ff
Merge branch 'main' into yunchuwang-wangbill-blob-payload-autopurge-sdk
YunchuWang Aug 17, 2026
0034bb0
Document per-host single-configuration constraint for externalized pa…
YunchuWang Aug 17, 2026
a814ffc
Quarantine unowned blobs, keep the disable path reconciling, drop cli…
YunchuWang Aug 17, 2026
9be9931
Reword disabled-path test helper comment for the perpetual reconcile …
YunchuWang Aug 17, 2026
ce3b547
Correct stale purge-disposition docs left by the F1 quarantine change
YunchuWang Aug 18, 2026
7669cf9
Revert F1: unowned blobs report Deleted again, not Quarantined
YunchuWang Aug 18, 2026
e2ea41b
Adopt the DTS large-payload purge service
YunchuWang Aug 27, 2026
adb1585
Merge origin/main into the auto-purge branch
YunchuWang Aug 27, 2026
45867b1
Gate work-item intake on a confirmed auto-purge setting
YunchuWang Aug 27, 2026
2190e3c
Follow the worker's transport for purge activity calls
YunchuWang Aug 27, 2026
4bb7c75
Correct the tombstone-token claims and two stale connect-setup docs
YunchuWang Aug 27, 2026
5ccd78e
Report an established work-item connection only once one exists
YunchuWang Aug 27, 2026
86e39f9
Make large-payload auto-purge an explicit client-owned operation
YunchuWang Aug 28, 2026
ec28a77
Move the auto-purge client seam into the Internal namespace
YunchuWang Aug 28, 2026
954bd26
Correct stale auto-purge recovery semantics in comments and messages
YunchuWang Aug 28, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions src/Client/Core/LargePayloadPurgeDisposition.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DurableTask.Client;

/// <summary>
/// The outcome of a single large-payload blob deletion attempt. The split is by whether a failure can
/// self-heal. Mirrors the <c>LargePayloadPurgeDisposition</c> protobuf enum.
/// </summary>
public enum LargePayloadPurgeDisposition
{
/// <summary>
/// No disposition was specified.
/// </summary>
Unspecified = 0,

/// <summary>
/// Terminal success. The blob was deleted, was already absent, or was deliberately left in place because
/// it is not owned by the payload store. The backend deletes the tombstone in all three cases.
/// </summary>
Deleted = 1,

/// <summary>
/// The failure may self-heal, so the row stays pending and the backend sets the next attempt.
/// </summary>
Retry = 2,

/// <summary>
/// A deterministic failure or protocol violation that retrying can never fix. The backend preserves the
/// evidence, alerts, and stops automatic retries.
/// </summary>
Quarantined = 3,
}
34 changes: 34 additions & 0 deletions src/Client/Core/LargePayloadPurgeResult.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DurableTask.Client;

/// <summary>
/// Serializable outcome of exactly one attempted large-payload blob deletion. Mirrors the
/// <c>LargePayloadPurgeResult</c> protobuf message but is safe to pass through the orchestration/activity
/// boundary. The backend owns retry scheduling and branches solely on
/// <see cref="Disposition"/>: it deletes rows reported as
/// <see cref="LargePayloadPurgeDisposition.Deleted"/>, reschedules
/// <see cref="LargePayloadPurgeDisposition.Retry"/> on its own backoff, and moves
/// <see cref="LargePayloadPurgeDisposition.Quarantined"/> rows out of the active fetch. The worker never
/// computes a retry delay.
/// </summary>
/// <remarks>
/// The disposition is deliberately the only outcome field: anything finer would be write-only on the backend.
/// Why an attempt failed stays in the worker's own telemetry, which holds the cause at full fidelity rather
/// than as a lossy classification.
/// </remarks>
/// <param name="TombstoneToken">
/// The opaque correlation token echoed unchanged from the fetched <see cref="LargePayloadTombstone"/>. It is
/// what identifies the row being reported on, so it must be passed back exactly as received: callers must not
/// parse it, derive from it, or construct one.
/// <para>
/// Opaqueness here is encapsulation, not security. The token is not an authentication credential and carries
/// no integrity guarantee, so treating a well-formed token as proof that the caller was entitled to report on
/// that row would be wrong. Authentication and task-hub scope are the security boundary.
/// </para>
/// </param>
/// <param name="Disposition">The disposition of the deletion attempt.</param>
public sealed record LargePayloadPurgeResult(
string TombstoneToken,
LargePayloadPurgeDisposition Disposition);
29 changes: 29 additions & 0 deletions src/Client/Core/LargePayloadTombstone.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DurableTask.Client;

/// <summary>
/// Serializable representation of a tombstoned large-payload row whose external blob a credentialed caller
/// must delete. Mirrors the <c>LargePayloadTombstone</c> protobuf message but is safe to pass through the
/// orchestration/activity boundary.
/// </summary>
/// <param name="TombstoneToken">
/// The opaque, backend-issued correlation token for this exact tombstone version. Its format is deliberately
/// not part of the contract: callers must not parse it, and must echo it back unchanged in the corresponding
/// <see cref="LargePayloadPurgeResult"/> so the backend can resolve the row it came from.
/// </param>
/// <param name="PayloadToken">
/// The self-describing <c>blob:v2:{fullBlobUrl}</c> payload token whose backing blob should be deleted.
/// </param>
public sealed record LargePayloadTombstone(string TombstoneToken, string PayloadToken)
{
/// <summary>
/// The maximum number of tombstones a single <c>GetLargePayloadTombstones</c> request may ask for. The
/// service clamps a larger request down to its own maximum, so this is the largest value that is worth
/// asking for rather than a value that will be rejected. Validators that bound a caller-supplied limit
/// compare against this shared value rather than a hard-coded literal so the bound cannot drift between
/// the client and the auto-purge extension.
/// </summary>
public const int MaxRequestLimit = 1000;
}
31 changes: 30 additions & 1 deletion src/Client/Grpc/GrpcDurableTaskClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,19 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using static Microsoft.DurableTask.Protobuf.TaskHubSidecarService;
using LP = Microsoft.DurableTask.Protobuf.LargePayloads;
using P = Microsoft.DurableTask.Protobuf;

namespace Microsoft.DurableTask.Client.Grpc;

/// <summary>
/// Durable Task client implementation that uses gRPC to connect to a remote "sidecar" process.
/// </summary>
public sealed class GrpcDurableTaskClient : DurableTaskClient
public sealed class GrpcDurableTaskClient : DurableTaskClient, Internal.ILargePayloadAutoPurgeClient
{
readonly ILogger logger;
readonly TaskHubSidecarServiceClient sidecarClient;
readonly LP.LargePayloadPurge.LargePayloadPurgeClient largePayloadPurgeClient;
readonly GrpcDurableTaskClientOptions options;
readonly DurableEntityClient? entityClient;
AsyncDisposable asyncDisposable;
Expand Down Expand Up @@ -56,6 +58,13 @@ public GrpcDurableTaskClient(string name, GrpcDurableTaskClientOptions options,
this.asyncDisposable = GetCallInvoker(options, logger, out CallInvoker callInvoker);
this.sidecarClient = new TaskHubSidecarServiceClient(callInvoker);

// Built on the SAME effective invoker as the sidecar client, which is what keeps the DTS-only purge
// service on this client's existing channel and inside its configured interceptors. That invoker may be
// a ChannelRecreatingCallInvoker, so the generated client follows the client's channel swaps rather than
// pinning the channel it happened to see here. Constructed unconditionally: it opens no connection of
// its own, so an app that never touches auto-purge pays only the allocation.
this.largePayloadPurgeClient = new LP.LargePayloadPurge.LargePayloadPurgeClient(callInvoker);

if (this.options.EnableEntitySupport)
{
this.entityClient = new GrpcDurableEntityClient(this.Name, this.DataConverter, this.sidecarClient, logger);
Expand Down Expand Up @@ -625,6 +634,26 @@ public override async Task<IList<HistoryEvent>> GetOrchestrationHistoryAsync(
}
}

/// <inheritdoc/>
async Task Internal.ILargePayloadAutoPurgeClient.SetLargePayloadAutoPurgeAsync(bool enabled, CancellationToken cancellation)
{
try
{
await this.largePayloadPurgeClient.SetLargePayloadAutoPurgeAsync(
new LP.SetLargePayloadAutoPurgeRequest { Enabled = enabled },
cancellationToken: cancellation);
}
catch (RpcException e) when (e.StatusCode == StatusCode.Cancelled)
{
throw new OperationCanceledException(
"The SetLargePayloadAutoPurge operation was canceled.", e, cancellation);
}
catch (RpcException e) when (e.StatusCode == StatusCode.Unimplemented)
{
throw new NotImplementedException(e.Status.Detail);
}
}

static AsyncDisposable GetCallInvoker(GrpcDurableTaskClientOptions options, ILogger logger, out CallInvoker callInvoker)
{
AsyncDisposable disposable = GetCallInvokerCore(options, logger, out CallInvoker core);
Expand Down
39 changes: 39 additions & 0 deletions src/Client/Grpc/Internal/ILargePayloadAutoPurgeClient.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace Microsoft.DurableTask.Client.Grpc.Internal;

/// <summary>
/// Exposes the backend's large-payload auto-purge setting to the Azure Blob payloads extension, which owns the
/// public API that turns the feature on and off.
/// </summary>
/// <remarks>
/// <para>
/// This is an internal API that supports the DurableTask infrastructure and not subject to
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
/// any release. You should not implement it, and should not use it directly in your code. Doing so
/// can result in application failures when updating to a new DurableTask release.
/// </para>
/// <para>
/// Deliberately narrow. The extension needs exactly one backend operation, and it must reach it over the
/// client's own transport - the same post-interceptor <c>CallInvoker</c> the client uses for every other RPC,
/// so the call carries the configured auth chain and follows channel recreation. Handing out the raw
/// <c>CallInvoker</c> instead would let a caller build arbitrary clients on the transport. The other two purge
/// RPCs are not here: they are executed by the worker's activities on the worker's transport, and no client
/// ever calls them.
/// </para>
/// <para>
/// Implemented explicitly by <see cref="GrpcDurableTaskClient"/>, so a client that is not the gRPC client - or
/// a gRPC client from an SDK version that predates this - fails the cast rather than silently doing nothing.
/// </para>
/// </remarks>
public interface ILargePayloadAutoPurgeClient
{
/// <summary>
/// Sets the large-payload blob auto-purge setting for the caller's authenticated task hub.
/// </summary>
/// <param name="enabled">The setting to persist.</param>
/// <param name="cancellation">The cancellation token.</param>
/// <returns>A task that completes once the backend has acknowledged the setting.</returns>
Task SetLargePayloadAutoPurgeAsync(bool enabled, CancellationToken cancellation = default);
}
Loading
Loading