-
Notifications
You must be signed in to change notification settings - Fork 207
NEXUS-484: Support UpdateWorkflow as a Nexus Operation #1631
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
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 | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -59,6 +59,7 @@ | |||||||||||||||||||
| ReturnType, | ||||||||||||||||||||
| SelfType, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| from ._callback import Callback | ||||||||||||||||||||
| from ._exceptions import ( | ||||||||||||||||||||
| WorkflowContinuedAsNewError, | ||||||||||||||||||||
| WorkflowFailureError, | ||||||||||||||||||||
|
|
@@ -955,6 +956,12 @@ async def _start_update( | |||||||||||||||||||
| result_type: type | None = None, | ||||||||||||||||||||
| rpc_metadata: Mapping[str, str | bytes] = {}, | ||||||||||||||||||||
| rpc_timeout: timedelta | None = None, | ||||||||||||||||||||
| run_id: str | None = None, | ||||||||||||||||||||
| first_execution_run_id: str | None = None, | ||||||||||||||||||||
| # The following options are for Nexus Operation-backed updates. Experimental and unstable | ||||||||||||||||||||
| callbacks: Sequence[Callback] | None = None, | ||||||||||||||||||||
| links: Sequence[temporalio.api.common.v1.Link] | None = None, | ||||||||||||||||||||
| request_id: str | None = None, | ||||||||||||||||||||
| ) -> WorkflowUpdateHandle[Any]: | ||||||||||||||||||||
| if wait_for_stage == WorkflowUpdateStage.ADMITTED: | ||||||||||||||||||||
| raise ValueError("ADMITTED wait stage not supported") | ||||||||||||||||||||
|
|
@@ -963,11 +970,16 @@ async def _start_update( | |||||||||||||||||||
| temporalio.workflow._UpdateDefinition.get_name_and_result_type(update) | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if run_id == None: | ||||||||||||||||||||
| run_id = self._run_id | ||||||||||||||||||||
| if first_execution_run_id == None: | ||||||||||||||||||||
| first_execution_run_id = self.first_execution_run_id | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
Comment on lines
+973
to
+977
Contributor
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. If we decide to keep this, let's make sure to use
Suggested change
|
||||||||||||||||||||
| return await self._client._impl.start_workflow_update( | ||||||||||||||||||||
| StartWorkflowUpdateInput( | ||||||||||||||||||||
| id=self._id, | ||||||||||||||||||||
| run_id=self._run_id, | ||||||||||||||||||||
| first_execution_run_id=self.first_execution_run_id, | ||||||||||||||||||||
| run_id=run_id, | ||||||||||||||||||||
| first_execution_run_id=first_execution_run_id, | ||||||||||||||||||||
| update_id=id, | ||||||||||||||||||||
| update=update_name, | ||||||||||||||||||||
| args=temporalio.common._arg_or_args(arg, args), | ||||||||||||||||||||
|
|
@@ -976,6 +988,9 @@ async def _start_update( | |||||||||||||||||||
| rpc_metadata=rpc_metadata, | ||||||||||||||||||||
| rpc_timeout=rpc_timeout, | ||||||||||||||||||||
| wait_for_stage=wait_for_stage, | ||||||||||||||||||||
| callbacks=callbacks, | ||||||||||||||||||||
| links=links, | ||||||||||||||||||||
| request_id=request_id, | ||||||||||||||||||||
| ) | ||||||||||||||||||||
| ) | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -715,3 +715,47 @@ async def _start_nexus_backing_workflow( | |
| ) | ||
|
|
||
| return WorkflowHandle[ReturnType]._unsafe_from_client_workflow_handle(wf_handle) | ||
|
|
||
|
|
||
| async def _start_nexus_backed_workflow_update( # pyright: ignore[reportUnusedFunction] | ||
| *, | ||
| temporal_context: _TemporalStartOperationContext, | ||
| workflow_id: str, | ||
| update: str | Callable, | ||
| arg: Any = temporalio.common._arg_unset, | ||
| args: Sequence[Any] = [], | ||
| id: str | None = None, | ||
| result_type: type | None = None, | ||
| rpc_metadata: Mapping[str, str | bytes] = {}, | ||
| rpc_timeout: timedelta | None = None, | ||
| run_id: str | None = None, | ||
| first_execution_run_id: str | None = None, | ||
|
Comment on lines
716
to
+732
Contributor
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. Instead of getting the workflow handle with just workflow_handle = temporal_context.client.get_workflow_handle(
workflow_id,
run_id=run_id,
first_execution_run_id=first_execution_run_id,
)Then in the details of |
||
| ) -> temporalio.client.WorkflowUpdateHandle[Any]: | ||
| # Default update ID to the Nexus request ID for retry-safety (matches sdk-go). | ||
| update_id = id or temporal_context.nexus_context.request_id | ||
| # This token is different from the actual token returned to the caller | ||
| # because return token will have the run_id that is unknowable before | ||
| # making the call. If run_id is passed, then it will be the same | ||
| token = OperationToken( | ||
| type=OperationTokenType.UPDATE_WORKFLOW, | ||
| namespace=temporal_context.client.namespace, | ||
| workflow_id=workflow_id, | ||
| update_id=update_id, | ||
| run_id=run_id, | ||
| ).encode() | ||
| workflow_handle = temporal_context.client.get_workflow_handle(workflow_id) | ||
| return await workflow_handle._start_update( | ||
| update, | ||
| arg, | ||
| args=args, | ||
| wait_for_stage=temporalio.client.WorkflowUpdateStage.ACCEPTED, # hardcoded as nexus only supports async updates | ||
| id=update_id, | ||
| result_type=result_type, | ||
| rpc_metadata=rpc_metadata, | ||
| rpc_timeout=rpc_timeout, | ||
| callbacks=temporal_context._get_callbacks(token), | ||
| links=temporal_context._get_request_links(), | ||
| request_id=temporal_context.nexus_context.request_id, | ||
| run_id=run_id, | ||
| first_execution_run_id=first_execution_run_id, | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -138,6 +138,25 @@ class CancelWorkflowRunOptions: | |
| """The ID of the workflow to cancel.""" | ||
|
|
||
|
|
||
| @dataclass(frozen=True) | ||
| class CancelUpdateWorkflowOptions: | ||
|
Contributor
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. Missing run ID |
||
| """Options for cancelling the workflow update backing a Nexus operation. | ||
|
|
||
| These options are built by :py:class:`TemporalOperationHandler` and passed to | ||
| :py:meth:`TemporalOperationHandler.cancel_workflow_update`. | ||
|
|
||
| .. warning:: | ||
| This API is experimental and unstable. | ||
| """ | ||
|
|
||
| workflow_id: str | ||
| """The ID of the workflow where the update is running.""" | ||
| update_id: str | ||
| """The ID of the update to cancel.""" | ||
| run_id: str | ||
| """The workflow runID that accepted the update""" | ||
|
|
||
|
|
||
| class TemporalOperationHandler(OperationHandler[InputT, OutputT], ABC): | ||
| """Operation handler for Nexus operations that interact with Temporal. | ||
| Implementations override the start_operation method. | ||
|
|
@@ -190,6 +209,15 @@ async def cancel(self, ctx: CancelOperationContext, token: str) -> None: | |
| workflow_id=operation_token.workflow_id | ||
| ) | ||
| await self.cancel_workflow_run(cancel_ctx, options) | ||
| case OperationTokenType.UPDATE_WORKFLOW: | ||
| assert operation_token.update_id is not None | ||
| assert operation_token.run_id is not None | ||
| cancel_options = CancelUpdateWorkflowOptions( | ||
| workflow_id=operation_token.workflow_id, | ||
| update_id=operation_token.update_id, | ||
| run_id=operation_token.run_id, | ||
| ) | ||
| await self.cancel_workflow_update(cancel_ctx, cancel_options) | ||
|
|
||
| async def cancel_workflow_run( | ||
| self, | ||
|
|
@@ -205,3 +233,23 @@ async def cancel_workflow_run( | |
| options.workflow_id | ||
| ) | ||
| await workflow_handle.cancel() | ||
|
|
||
| # draft-review: maybe just move it inline, no need for a function just to error out | ||
| # check after review in case theres some other way to override/supply custom cancels | ||
|
Comment on lines
+237
to
+238
Contributor
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 way to customize cancellation is to inherit from |
||
| async def cancel_workflow_update( | ||
| self, | ||
| ctx: TemporalCancelOperationContext, # pyright: ignore[reportUnusedParameter] | ||
| options: CancelUpdateWorkflowOptions, # pyright: ignore[reportUnusedParameter] | ||
| ) -> None: | ||
| """Cancels the workflow update backing the Nexus operation. | ||
|
|
||
| .. warning:: | ||
| This API is experimental and unstable. | ||
| """ | ||
| raise HandlerError( | ||
| """ | ||
| Cancellation is not natively supported for update-workflow Nexus operations. | ||
| Override a TemporalOperationHandler and implement this method to run cancellable workflow updates. | ||
| """, | ||
| type=HandlerErrorType.NOT_IMPLEMENTED, | ||
| ) | ||
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.
As mentioned in other comments, I think we should remove these and always use
self._run_idandself._first_execution_run_id