-
Notifications
You must be signed in to change notification settings - Fork 58
Add large-payload blob auto-purge (opt-in singleton job, worker/SDK side) #758
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
306d19f
60f6637
149c63a
4d52005
780d743
3a2215c
3fbf061
47651dc
7397fa6
4afeb8a
e74f633
50ae944
a680442
a5ed298
fff06b0
6e4f5d0
65e9cbb
8f436df
81284e0
a15c04d
ae5ed1a
1fcdb10
2026d71
e25e724
7bf5da8
de218c8
02b957c
e432d25
1ac131f
ea0b4c7
3c5c7ba
7d409ab
7e0d0ed
720e028
2b389fa
3dd2b14
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| // 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, and a row is correlated to it by | ||
| /// (<see cref="PartitionId"/>, <see cref="InstanceKey"/>, <see cref="PayloadId"/>). | ||
| /// </remarks> | ||
| /// <param name="PartitionId">The backend partition that owns the tombstoned row.</param> | ||
| /// <param name="InstanceKey">The orchestration instance key the payload belonged to.</param> | ||
| /// <param name="PayloadId">The backend identifier of the tombstoned payload row.</param> | ||
| /// <param name="Revision"> | ||
| /// The revision echoed unmodified from the fetched <see cref="LargePayloadTombstone"/>; used by the backend | ||
| /// as a compare-and-swap guard. | ||
| /// </param> | ||
| /// <param name="Disposition">The disposition of the deletion attempt.</param> | ||
| public sealed record LargePayloadPurgeResult( | ||
| int PartitionId, | ||
| long InstanceKey, | ||
| long PayloadId, | ||
| long Revision, | ||
| LargePayloadPurgeDisposition Disposition); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| // 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="PartitionId">The backend partition that owns the tombstoned row.</param> | ||
| /// <param name="InstanceKey">The orchestration instance key the payload belonged to.</param> | ||
| /// <param name="PayloadId">The backend identifier of the tombstoned payload row.</param> | ||
| /// <param name="Token"> | ||
| /// The self-describing <c>blob:v2:{fullBlobUrl}</c> payload token whose backing blob should be deleted. | ||
| /// </param> | ||
| /// <param name="Revision"> | ||
| /// An optimistic-concurrency guard echoed back unmodified in the corresponding | ||
| /// <see cref="LargePayloadPurgeResult"/> so the backend can reject duplicate or stale reports without taking | ||
| /// a per-row lease. | ||
| /// </param> | ||
| public sealed record LargePayloadTombstone( | ||
| int PartitionId, long InstanceKey, long PayloadId, string Token, long Revision); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -624,6 +624,82 @@ public override async Task<IList<HistoryEvent>> GetOrchestrationHistoryAsync( | |
| } | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public override async Task<List<LargePayloadTombstone>> GetLargePayloadTombstonesAsync( | ||
| int limit, CancellationToken cancellation = default) | ||
| { | ||
| if (limit <= 0 || limit > 1000) | ||
| { | ||
| throw new ArgumentOutOfRangeException( | ||
| nameof(limit), limit, "Limit must be greater than 0 and less than or equal to 1000."); | ||
| } | ||
|
|
||
| P.GetLargePayloadTombstonesResponse response; | ||
| try | ||
| { | ||
| response = await this.sidecarClient.GetLargePayloadTombstonesAsync( | ||
| new P.GetLargePayloadTombstonesRequest { Limit = limit }, | ||
| cancellationToken: cancellation); | ||
| } | ||
| catch (RpcException e) when (e.StatusCode == StatusCode.Cancelled) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [Medium / compatibility] During a mixed rollout, an older backend returns gRPC |
||
| { | ||
| throw new OperationCanceledException( | ||
| $"The {nameof(this.GetLargePayloadTombstonesAsync)} operation was canceled.", e, cancellation); | ||
| } | ||
|
|
||
| List<LargePayloadTombstone> result = new(response.Tombstones.Count); | ||
| foreach (P.LargePayloadTombstone tombstone in response.Tombstones) | ||
| { | ||
| result.Add(new LargePayloadTombstone( | ||
| tombstone.PartitionId, | ||
| tombstone.InstanceKey, | ||
| tombstone.PayloadId, | ||
| tombstone.Token, | ||
| tombstone.Revision)); | ||
| } | ||
|
|
||
| return result; | ||
| } | ||
|
|
||
| /// <inheritdoc/> | ||
| public override async Task ReportLargePayloadPurgeResultsAsync( | ||
| IEnumerable<LargePayloadPurgeResult> results, CancellationToken cancellation = default) | ||
| { | ||
| Check.NotNull(results); | ||
|
|
||
| P.ReportLargePayloadPurgeResultsRequest request = new(); | ||
| foreach (LargePayloadPurgeResult result in results) | ||
| { | ||
| request.Results.Add(new P.LargePayloadPurgeResult | ||
| { | ||
| PartitionId = result.PartitionId, | ||
| InstanceKey = result.InstanceKey, | ||
| PayloadId = result.PayloadId, | ||
| Revision = result.Revision, | ||
|
|
||
| // The managed disposition enum declares the same numeric values as its protobuf counterpart, | ||
| // so it maps across by value. This is the only enum on the message and it only travels | ||
| // outbound, so the SDK can never receive a value it does not know. | ||
| Disposition = (P.LargePayloadPurgeDisposition)result.Disposition, | ||
| }); | ||
| } | ||
|
|
||
| if (request.Results.Count == 0) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| try | ||
| { | ||
| await this.sidecarClient.ReportLargePayloadPurgeResultsAsync(request, cancellationToken: cancellation); | ||
| } | ||
| catch (RpcException e) when (e.StatusCode == StatusCode.Cancelled) | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same here, we need to handle other exception cases as well |
||
| { | ||
| throw new OperationCanceledException( | ||
| $"The {nameof(this.ReportLargePayloadPurgeResultsAsync)} operation was canceled.", e, cancellation); | ||
| } | ||
| } | ||
|
|
||
| static AsyncDisposable GetCallInvoker(GrpcDurableTaskClientOptions options, ILogger logger, out CallInvoker callInvoker) | ||
| { | ||
| Func<GrpcChannel, CancellationToken, Task<GrpcChannel>>? recreator = options.Internal.ChannelRecreator; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
move to constant