Skip to content

Commit 0b147e9

Browse files
committed
NEXUS-484: Add support for workflow update Nexus operations
1 parent 68bc823 commit 0b147e9

11 files changed

Lines changed: 536 additions & 6 deletions

temporalio/client/_impl.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,11 @@ async def start_workflow_update(
787787
):
788788
break
789789

790+
# Add response link if its a Nexus operation
791+
nexus_ctx = temporalio.nexus._operation_context._try_start_operation_context()
792+
if nexus_ctx is not None and resp.HasField("link"):
793+
nexus_ctx._add_response_link(resp.link)
794+
790795
# Build the handle. If the user's wait stage is COMPLETED, make sure we
791796
# poll for result.
792797
handle: WorkflowUpdateHandle[Any] = WorkflowUpdateHandle(
@@ -852,6 +857,23 @@ async def _build_update_workflow_execution_request(
852857
)
853858
),
854859
)
860+
# Only set Nexus fields for StartWorkflowUpdateInput, skip for UpdateWithStartUpdateWorkflowInput
861+
if isinstance(input, StartWorkflowUpdateInput):
862+
if input.request_id:
863+
req.request.request_id = input.request_id
864+
if input.links:
865+
req.request.links.extend(input.links)
866+
if input.callbacks:
867+
req.request.completion_callbacks.extend(
868+
temporalio.api.common.v1.Callback(
869+
nexus=temporalio.api.common.v1.Callback.Nexus(
870+
url=callback.url,
871+
header=callback.headers,
872+
),
873+
links=input.links or [],
874+
)
875+
for callback in input.callbacks
876+
)
855877
if input.args:
856878
req.request.input.args.payloads.extend(
857879
await data_converter.encode(input.args)

temporalio/client/_interceptor.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,10 @@ class StartWorkflowUpdateInput:
322322
ret_type: type | None
323323
rpc_metadata: Mapping[str, str | bytes]
324324
rpc_timeout: timedelta | None
325+
# The following options are for Nexus Operation-backed updates. Experimental and unstable
326+
callbacks: Sequence[Callback] | None = None
327+
links: Sequence[temporalio.api.common.v1.Link] | None = None
328+
request_id: str | None = None
325329

326330

327331
@dataclass

temporalio/client/_workflow.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@
5959
ReturnType,
6060
SelfType,
6161
)
62+
from ._callback import Callback
6263
from ._exceptions import (
6364
WorkflowContinuedAsNewError,
6465
WorkflowFailureError,
@@ -896,6 +897,8 @@ async def start_update(
896897
rpc_timeout: timedelta | None = None,
897898
) -> WorkflowUpdateHandle[Any]: ...
898899

900+
# draft-review: check why this doesnt currently support run_id and first_execution_run_id
901+
# If it can be supported, wire it up for nexus operation-backed updates as well
899902
async def start_update(
900903
self,
901904
update: str | Callable,
@@ -955,6 +958,12 @@ async def _start_update(
955958
result_type: type | None = None,
956959
rpc_metadata: Mapping[str, str | bytes] = {},
957960
rpc_timeout: timedelta | None = None,
961+
# run_id: str | None = None,
962+
# first_execution_run_id: str | None = None,
963+
# The following options are for Nexus Operation-backed updates. Experimental and unstable
964+
callbacks: Sequence[Callback] | None = None,
965+
links: Sequence[temporalio.api.common.v1.Link] | None = None,
966+
request_id: str | None = None,
958967
) -> WorkflowUpdateHandle[Any]:
959968
if wait_for_stage == WorkflowUpdateStage.ADMITTED:
960969
raise ValueError("ADMITTED wait stage not supported")
@@ -976,6 +985,11 @@ async def _start_update(
976985
rpc_metadata=rpc_metadata,
977986
rpc_timeout=rpc_timeout,
978987
wait_for_stage=wait_for_stage,
988+
# run_id=run_id,
989+
# first_execution_run_id=first_execution_run_id,
990+
callbacks=callbacks,
991+
links=links,
992+
request_id=request_id,
979993
)
980994
)
981995

temporalio/nexus/__init__.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,15 +25,17 @@
2525
wait_for_worker_shutdown_sync,
2626
)
2727
from ._operation_handlers import (
28+
CancelUpdateWorkflowOptions,
2829
CancelWorkflowRunOptions,
2930
TemporalOperationHandler,
3031
)
3132
from ._temporal_client import TemporalNexusClient, TemporalOperationResult
32-
from ._token import WorkflowHandle
33+
from ._token import UpdateHandle, WorkflowHandle
3334

3435
__all__ = (
3536
"workflow_run_operation",
3637
"CancelWorkflowRunOptions",
38+
"CancelUpdateWorkflowOptions",
3739
"Info",
3840
"LoggerAdapter",
3941
"NexusCallback",
@@ -49,6 +51,7 @@
4951
"wait_for_worker_shutdown",
5052
"wait_for_worker_shutdown_sync",
5153
"WorkflowHandle",
54+
"UpdateHandle",
5255
"TemporalNexusClient",
5356
"TemporalOperationStartHandlerFunc",
5457
"TemporalOperationHandler",

temporalio/nexus/_operation_context.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -715,3 +715,43 @@ async def _start_nexus_backing_workflow(
715715
)
716716

717717
return WorkflowHandle[ReturnType]._unsafe_from_client_workflow_handle(wf_handle)
718+
719+
720+
async def _start_nexus_backed_workflow_update(
721+
*,
722+
temporal_context: _TemporalStartOperationContext,
723+
workflow_id: str,
724+
update: str | Callable,
725+
arg: Any = temporalio.common._arg_unset,
726+
args: Sequence[Any] = [],
727+
id: str | None = None,
728+
result_type: type | None = None,
729+
rpc_metadata: Mapping[str, str | bytes] = {},
730+
rpc_timeout: timedelta | None = None,
731+
# run_id: str | None = None,
732+
# first_execution_run_id: str | None = None,
733+
) -> temporalio.client.WorkflowUpdateHandle[Any]:
734+
# Default update ID to the Nexus request ID for retry-safety (matches sdk-go).
735+
update_id = id or temporal_context.nexus_context.request_id
736+
token = OperationToken(
737+
type=OperationTokenType.UPDATE_WORKFLOW,
738+
namespace=temporal_context.client.namespace,
739+
workflow_id=workflow_id,
740+
update_id=update_id,
741+
).encode()
742+
workflow_handle = temporal_context.client.get_workflow_handle(workflow_id)
743+
return await workflow_handle._start_update(
744+
update,
745+
arg,
746+
args=args,
747+
wait_for_stage=temporalio.client.WorkflowUpdateStage.ACCEPTED, # hardcoded as nexus only supports async updates
748+
id=update_id,
749+
result_type=result_type,
750+
rpc_metadata=rpc_metadata,
751+
rpc_timeout=rpc_timeout,
752+
callbacks=temporal_context._get_callbacks(token),
753+
links=temporal_context._get_request_links(),
754+
request_id=temporal_context.nexus_context.request_id,
755+
# run_id=run_id,
756+
# first_execution_run_id=first_execution_run_id,
757+
)

temporalio/nexus/_operation_handlers.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,23 @@ class CancelWorkflowRunOptions:
138138
"""The ID of the workflow to cancel."""
139139

140140

141+
@dataclass(frozen=True)
142+
class CancelUpdateWorkflowOptions:
143+
"""Options for cancelling the workflow update backing a Nexus operation.
144+
145+
These options are built by :py:class:`TemporalOperationHandler` and passed to
146+
:py:meth:`TemporalOperationHandler.cancel_workflow_update`.
147+
148+
.. warning::
149+
This API is experimental and unstable.
150+
"""
151+
152+
workflow_id: str
153+
"""The ID of the workflow where the update is running."""
154+
update_id: str
155+
"""The ID of the update to cancel."""
156+
157+
141158
class TemporalOperationHandler(OperationHandler[InputT, OutputT], ABC):
142159
"""Operation handler for Nexus operations that interact with Temporal.
143160
Implementations override the start_operation method.
@@ -190,6 +207,13 @@ async def cancel(self, ctx: CancelOperationContext, token: str) -> None:
190207
workflow_id=operation_token.workflow_id
191208
)
192209
await self.cancel_workflow_run(cancel_ctx, options)
210+
case OperationTokenType.UPDATE_WORKFLOW:
211+
assert operation_token.update_id is not None
212+
options = CancelUpdateWorkflowOptions(
213+
workflow_id=operation_token.workflow_id,
214+
update_id=operation_token.update_id,
215+
)
216+
await self.cancel_workflow_update(cancel_ctx, options)
193217

194218
async def cancel_workflow_run(
195219
self,
@@ -205,3 +229,23 @@ async def cancel_workflow_run(
205229
options.workflow_id
206230
)
207231
await workflow_handle.cancel()
232+
233+
# draft-review: maybe just move it inline, no need for a function just to error out
234+
# check after review in case theres some other way to override/supply custom cancels
235+
async def cancel_workflow_update(
236+
self,
237+
ctx: TemporalCancelOperationContext, # pyright: ignore[reportUnusedParameter]
238+
options: CancelUpdateWorkflowOptions, # pyright: ignore[reportUnusedParameter]
239+
) -> None:
240+
"""Cancels the workflow update backing the Nexus operation.
241+
242+
.. warning::
243+
This API is experimental and unstable.
244+
"""
245+
raise HandlerError(
246+
"""
247+
Cancellation is not natively supported for update-workflow Nexus operations.
248+
Override a TemporalOperationHandler and implement this method to run cancellable workflow updates.
249+
""",
250+
type=HandlerErrorType.NOT_IMPLEMENTED,
251+
)

0 commit comments

Comments
 (0)