-
Notifications
You must be signed in to change notification settings - Fork 56
Add Standalone Activities support to Temporal Nexus Operation Handler #748
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
a88374a
fa37113
84020ad
40d9882
8520a4b
c234c4c
8ad5083
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 |
|---|---|---|
|
|
@@ -14,6 +14,7 @@ | |
| using Temporalio.Common; | ||
| using Temporalio.Converters; | ||
| using Temporalio.Exceptions; | ||
| using Temporalio.Nexus; | ||
|
|
||
| #if NETCOREAPP3_0_OR_GREATER | ||
| using System.Runtime.CompilerServices; | ||
|
|
@@ -739,6 +740,19 @@ private async Task<WorkflowHandle<TWorkflow, TResult>> StartWorkflowInternalAsyn | |
| } | ||
| var resp = await Client.Connection.WorkflowService.StartWorkflowExecutionAsync( | ||
| req, DefaultRetryOptions(input.Options.Rpc)).ConfigureAwait(false); | ||
| if (NexusOperationExecutionContext.HasCurrent) | ||
| { | ||
| // Prefer the link returned by the server; fall back to a | ||
| // WorkflowExecutionStarted link for older servers that don't populate it. | ||
| var nexusLink = resp.Link?.ToNexusLink() ?? new Link.Types.WorkflowEvent | ||
|
jmaeagle99 marked this conversation as resolved.
|
||
| { | ||
| Namespace = req.Namespace, | ||
| WorkflowId = req.WorkflowId, | ||
| RunId = resp.RunId, | ||
| EventRef = new() { EventId = 1, EventType = EventType.WorkflowExecutionStarted }, | ||
| }.ToNexusLink(); | ||
| NexusOperationExecutionContext.Current.HandlerContext.OutboundLinks.Add(nexusLink); | ||
| } | ||
|
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. Signal-with-start drops Nexus linksMedium Severity Outbound Nexus links for child workflow starts are only appended after a plain Additional Locations (1)Reviewed by Cursor Bugbot for commit 8520a4b. Configure here. |
||
| return new WorkflowHandle<TWorkflow, TResult>( | ||
| Client: Client, | ||
| Id: req.WorkflowId, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,95 @@ | ||
| using System; | ||
| using System.Text.Json; | ||
| using System.Text.Json.Serialization; | ||
|
|
||
| namespace Temporalio.Nexus | ||
| { | ||
| /// <summary> | ||
| /// Internal helper for building and parsing activity-execution operation tokens used by the | ||
| /// generic <see cref="TemporalOperationHandler"/> when an operation is backed by a standalone | ||
| /// activity. | ||
| /// </summary> | ||
| internal static class NexusActivityExecutionToken | ||
| { | ||
| /// <summary> | ||
| /// Token-type value identifying an activity-execution operation token. | ||
| /// </summary> | ||
| internal const int OperationTokenType = 2; | ||
|
|
||
| /// <summary> | ||
| /// Build a base64url-encoded activity-execution operation token. | ||
| /// </summary> | ||
| /// <param name="namespace_">Activity namespace.</param> | ||
| /// <param name="activityId">Activity ID.</param> | ||
| /// <param name="runId">Activity run ID. May be <c>null</c> when building the token used in | ||
| /// the completion-callback header (which is sent before the run ID is known).</param> | ||
| /// <returns>Base64url-encoded operation token.</returns> | ||
| internal static string Create(string namespace_, string activityId, string? runId) => | ||
| NexusWorkflowRunHandle.Base64UrlEncode(JsonSerializer.SerializeToUtf8Bytes( | ||
| new Token(namespace_, activityId, runId, null), | ||
| NexusWorkflowRunHandle.TokenSerializerOptions)); | ||
|
|
||
| /// <summary> | ||
| /// Parse an activity-execution operation token into its underlying fields. | ||
| /// </summary> | ||
| /// <param name="token">Base64url-encoded token string.</param> | ||
| /// <returns>Parsed token fields.</returns> | ||
| /// <exception cref="ArgumentException">If the token is invalid.</exception> | ||
| internal static Token Parse(string token) | ||
| { | ||
| byte[] bytes; | ||
| try | ||
| { | ||
| bytes = NexusWorkflowRunHandle.Base64UrlDecode(token); | ||
| } | ||
| catch (FormatException) | ||
| { | ||
| throw new ArgumentException("Token invalid"); | ||
| } | ||
| Token? tokenObj; | ||
| try | ||
| { | ||
| tokenObj = JsonSerializer.Deserialize<Token>( | ||
| bytes, NexusWorkflowRunHandle.TokenSerializerOptions); | ||
| } | ||
| catch (JsonException e) | ||
| { | ||
| throw new ArgumentException("Token invalid", e); | ||
| } | ||
| if (tokenObj == null) | ||
| { | ||
| throw new ArgumentException("Token invalid"); | ||
| } | ||
| if (tokenObj.Type != OperationTokenType) | ||
| { | ||
| throw new ArgumentException( | ||
| $"Invalid activity execution token type: {tokenObj.Type}, " + | ||
| $"expected: {OperationTokenType}"); | ||
| } | ||
| if (tokenObj.Version != null && tokenObj.Version != 0) | ||
| { | ||
| throw new ArgumentException($"Unsupported token version: {tokenObj.Version}"); | ||
| } | ||
| if (string.IsNullOrEmpty(tokenObj.ActivityId)) | ||
| { | ||
| throw new ArgumentException("Token invalid: missing activity ID (aid)"); | ||
| } | ||
| return tokenObj; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Represents the fields of an activity-execution operation token. | ||
| /// </summary> | ||
| internal record Token( | ||
| [property: JsonPropertyName("ns")] | ||
| string Namespace, | ||
| [property: JsonPropertyName("aid")] | ||
| string ActivityId, | ||
| [property: JsonPropertyName("rid")] | ||
| string? RunId, | ||
| [property: JsonPropertyName("v")] | ||
| int? Version, | ||
| [property: JsonPropertyName("t")] | ||
| int Type = OperationTokenType); | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| using System.Collections.Generic; | ||
| using System.Threading.Tasks; | ||
| using NexusRpc.Handlers; | ||
| using Temporalio.Client; | ||
|
|
||
| namespace Temporalio.Nexus | ||
| { | ||
| /// <summary> | ||
| /// Internal helper for starting standalone activities from Nexus operations and managing | ||
| /// activity-execution operation tokens. | ||
| /// </summary> | ||
| internal static class NexusActivityStartHelper | ||
| { | ||
| /// <summary> | ||
| /// Start a standalone activity and return the activity-execution operation token. This | ||
| /// handles all Nexus plumbing: cloning options, defaulting task queue / ID, processing | ||
| /// links, injecting callbacks, and adding outbound links. | ||
| /// </summary> | ||
| /// <param name="client">Temporal client.</param> | ||
| /// <param name="nexusStartContext">Nexus start context for callbacks and links.</param> | ||
| /// <param name="temporalContext">Temporal operation context for info and logging.</param> | ||
| /// <param name="activity">Activity type name.</param> | ||
| /// <param name="args">Activity arguments.</param> | ||
| /// <param name="options">Activity start options. Either ScheduleToCloseTimeout or | ||
| /// StartToCloseTimeout must be set; TaskQueue defaults to the operation's task queue.</param> | ||
| /// <returns>Base64url-encoded operation token.</returns> | ||
| internal static async Task<string> StartActivityAsync( | ||
| ITemporalClient client, | ||
| OperationStartContext nexusStartContext, | ||
| NexusOperationExecutionContext temporalContext, | ||
| string activity, | ||
| IReadOnlyCollection<object?> args, | ||
| StartActivityOptions options) | ||
| { | ||
| // Shallow clone so we can mutate | ||
| options = (StartActivityOptions)options.Clone(); | ||
| options.TaskQueue ??= temporalContext.Info.TaskQueue; | ||
|
|
||
| var namespace_ = client.Options.Namespace; | ||
| var activityId = options.Id!; | ||
|
|
||
| // Build the callback-header token without a run ID (we don't have it yet). | ||
| var callbackToken = NexusActivityExecutionToken.Create(namespace_, activityId, runId: null); | ||
|
|
||
| if (options.IdConflictPolicy == Api.Enums.V1.ActivityIdConflictPolicy.UseExisting) | ||
| { | ||
| options.OnConflictOptions = new() | ||
| { | ||
| AttachLinks = true, | ||
| AttachCompletionCallbacks = true, | ||
| AttachRequestId = true, | ||
| }; | ||
| } | ||
| if (NexusOperationStartHelper.CreateInboundLinks( | ||
| nexusStartContext, temporalContext) is { } links) | ||
| { | ||
| options.Links = links; | ||
| } | ||
| if (NexusOperationStartHelper.CreateCallback( | ||
| nexusStartContext, callbackToken, options.Links) is { } callback) | ||
| { | ||
| options.CompletionCallbacks = new[] { callback }; | ||
| } | ||
| options.RequestId = nexusStartContext.RequestId; | ||
|
|
||
| // Do the start call | ||
| var handle = await client.StartActivityAsync( | ||
|
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. In the Go implementation, we check for timeout presence to raise a handler error and surface the validation error. Is that necessary here as well?
Contributor
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. The SDK already does some of this validation, Started a discussion in slack on where we want to do the validation. |
||
| activity, args, options).ConfigureAwait(false); | ||
|
Quinn-With-Two-Ns marked this conversation as resolved.
|
||
|
|
||
| // Return a token that includes the run ID from the start response. | ||
| return NexusActivityExecutionToken.Create(namespace_, activityId, handle.RunId); | ||
| } | ||
| } | ||
| } | ||


Uh oh!
There was an error while loading. Please reload this page.