-
Notifications
You must be signed in to change notification settings - Fork 104
Nexus: worker, workflow-backed operations, and workflow caller #813
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
94667cf
e97acf1
bba27cd
7692510
54dc188
75cb096
31ce1fc
8ce108f
ecc0876
d5c1184
20e82fc
26543ae
e512834
80cf8aa
32b1604
e1b996d
3f3ab83
2e3181e
8aa937f
93d047b
2b77eb1
a7fc543
dd7ecff
95f8b4a
6d6887a
b0d7a60
0322767
8c2de6e
7bc3692
a24be8d
3549629
fdb3e37
c79bde5
b91207e
60fcee3
3715460
2b5debc
b3ddaf9
e04218f
9e99ca7
8046b97
4ceba6d
7bd6bb5
f958627
6fcf72f
3ed5174
7fcd501
04ce78d
7ec57ff
7301307
4cbb09a
1b84f11
bdfc019
2b1dece
086efa5
b6dfb96
93e0775
29344ad
60e4668
e79f222
c7b0170
d731ac2
8755353
a1a3df6
1db7ff0
07c6d39
2616755
27d7e41
c0cf503
efb9df5
400260d
7355554
ec31690
7f9c144
602d412
ec1f05a
c6a8e32
63d19b2
b0c1180
9ab2d19
19968d7
fb3238b
0cc4359
0c1982b
fa0344b
f1bd90d
3526b89
29f11ca
97f1d48
009faca
844a9c3
af00209
914b35e
16432f7
0f3b85e
686f156
33f4f82
39f220c
6ce70ba
f723602
ddaee4f
fc285e0
bd88867
d153d94
7358f00
2f160aa
7e53850
15beaff
132f693
d057268
0ec14d8
b1b9ea3
2ba2bc1
f36c215
38c1c57
3320f28
5e563c0
a6e9777
72d14df
db09733
e721f55
ca5f572
e7091ac
a9bac66
bf2a02d
9b6f836
f53a783
e30bba2
b3de2ef
54a0a86
332658d
0bba35e
3ee29e7
5be55aa
109c976
ad42e67
5a5d9c6
86a9a61
caef930
b288eaa
6c08c80
d5043d7
e4141da
b4fcd07
b51063b
7c5f107
638a121
c6dbb52
0fc88c0
2ca30ff
b3146ea
ac3c96e
de2aa48
915fde1
4462e3a
4de6b48
38a50fa
0fb7837
7656a02
075ec7c
6c1e894
d5fce2a
8f3681b
8117ea1
7b62955
4e34462
8889dde
41b9709
c2d4825
a4fd205
a5f67d2
f7f8a51
01960ed
5103f80
03513b5
b6bcd6c
a840316
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 |
---|---|---|
|
@@ -11,6 +11,7 @@ keywords = [ | |
"workflow", | ||
] | ||
dependencies = [ | ||
"nexus-rpc", | ||
"protobuf>=3.20,<6", | ||
"python-dateutil>=2.8.2,<3 ; python_version < '3.11'", | ||
"types-protobuf>=3.20", | ||
|
@@ -44,7 +45,7 @@ dev = [ | |
"psutil>=5.9.3,<6", | ||
"pydocstyle>=6.3.0,<7", | ||
"pydoctor>=24.11.1,<25", | ||
"pyright==1.1.377", | ||
"pyright==1.1.400", | ||
"pytest~=7.4", | ||
"pytest-asyncio>=0.21,<0.22", | ||
"pytest-timeout~=2.2", | ||
|
@@ -53,6 +54,8 @@ dev = [ | |
"twine>=4.0.1,<5", | ||
"ruff>=0.5.0,<0.6", | ||
"maturin>=1.8.2", | ||
"pytest-cov>=6.1.1", | ||
"httpx>=0.28.1", | ||
"pytest-pretty>=1.3.0", | ||
] | ||
|
||
|
@@ -162,6 +165,7 @@ exclude = [ | |
"tests/worker/workflow_sandbox/testmodules/proto", | ||
"temporalio/bridge/worker.py", | ||
"temporalio/contrib/opentelemetry.py", | ||
"temporalio/contrib/pydantic.py", | ||
"temporalio/converter.py", | ||
"temporalio/testing/_workflow.py", | ||
"temporalio/worker/_activity.py", | ||
|
@@ -173,6 +177,10 @@ exclude = [ | |
"tests/api/test_grpc_stub.py", | ||
"tests/conftest.py", | ||
"tests/contrib/test_opentelemetry.py", | ||
"tests/contrib/pydantic/models.py", | ||
"tests/contrib/pydantic/models_2.py", | ||
"tests/contrib/pydantic/test_pydantic.py", | ||
"tests/contrib/pydantic/workflows.py", | ||
"tests/test_converter.py", | ||
"tests/test_service.py", | ||
"tests/test_workflow.py", | ||
|
@@ -208,3 +216,6 @@ exclude = [ | |
[tool.uv] | ||
# Prevent uv commands from building the package by default | ||
package = false | ||
|
||
[tool.uv.sources] | ||
nexus-rpc = { git = "https://github.com/nexus-rpc/sdk-python" } | ||
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. This should be removed before merging, right? We need a release of the Nexus SDK to pypi. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -464,9 +464,17 @@ async def start_workflow( | |
rpc_metadata: Mapping[str, str] = {}, | ||
rpc_timeout: Optional[timedelta] = None, | ||
request_eager_start: bool = False, | ||
stack_level: int = 2, | ||
priority: temporalio.common.Priority = temporalio.common.Priority.default, | ||
versioning_override: Optional[temporalio.common.VersioningOverride] = None, | ||
# The following options should not be considered part of the public API. They | ||
# are deliberately not exposed in overloads, and are not subject to any | ||
# backwards compatibility guarantees. | ||
nexus_completion_callbacks: Sequence[NexusCompletionCallback] = [], | ||
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. Nexus callbacks are only one type of callback that we were planning on supporting. I wouldn't mention nexus in the name here. |
||
workflow_event_links: Sequence[ | ||
temporalio.api.common.v1.Link.WorkflowEvent | ||
] = [], | ||
request_id: Optional[str] = None, | ||
stack_level: int = 2, | ||
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. You are also missing the conflict options which are required for 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. I would for now throw if a user tries to set |
||
) -> WorkflowHandle[Any, Any]: | ||
"""Start a workflow and return its handle. | ||
|
||
|
@@ -529,7 +537,6 @@ async def start_workflow( | |
name, result_type_from_type_hint = ( | ||
temporalio.workflow._Definition.get_name_and_result_type(workflow) | ||
) | ||
|
||
return await self._impl.start_workflow( | ||
StartWorkflowInput( | ||
workflow=name, | ||
|
@@ -557,6 +564,9 @@ async def start_workflow( | |
rpc_timeout=rpc_timeout, | ||
request_eager_start=request_eager_start, | ||
priority=priority, | ||
nexus_completion_callbacks=nexus_completion_callbacks, | ||
workflow_event_links=workflow_event_links, | ||
request_id=request_id, | ||
) | ||
) | ||
|
||
|
@@ -5193,6 +5203,10 @@ class StartWorkflowInput: | |
rpc_timeout: Optional[timedelta] | ||
request_eager_start: bool | ||
priority: temporalio.common.Priority | ||
# The following options are experimental and unstable. | ||
nexus_completion_callbacks: Sequence[NexusCompletionCallback] | ||
workflow_event_links: Sequence[temporalio.api.common.v1.Link.WorkflowEvent] | ||
request_id: Optional[str] | ||
Comment on lines
+5207
to
+5209
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. May want to mention here these are unstable/experimental 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.
Done |
||
versioning_override: Optional[temporalio.common.VersioningOverride] = None | ||
|
||
|
||
|
@@ -5807,8 +5821,26 @@ async def _build_start_workflow_execution_request( | |
self, input: StartWorkflowInput | ||
) -> temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest: | ||
req = temporalio.api.workflowservice.v1.StartWorkflowExecutionRequest() | ||
req.request_eager_execution = input.request_eager_start | ||
await self._populate_start_workflow_execution_request(req, input) | ||
# _populate_start_workflow_execution_request is used for both StartWorkflowInput | ||
# and UpdateWithStartStartWorkflowInput. UpdateWithStartStartWorkflowInput does | ||
# not have the following two fields so they are handled here. | ||
req.request_eager_execution = input.request_eager_start | ||
if input.request_id: | ||
req.request_id = input.request_id | ||
|
||
req.completion_callbacks.extend( | ||
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. Put the links on the callbacks so they can be associated. |
||
temporalio.api.common.v1.Callback( | ||
nexus=temporalio.api.common.v1.Callback.Nexus( | ||
url=callback.url, header=callback.header | ||
) | ||
) | ||
for callback in input.nexus_completion_callbacks | ||
) | ||
req.links.extend( | ||
temporalio.api.common.v1.Link(workflow_event=link) | ||
for link in input.workflow_event_links | ||
) | ||
return req | ||
|
||
async def _build_signal_with_start_workflow_execution_request( | ||
|
@@ -7231,6 +7263,21 @@ def api_key(self, value: Optional[str]) -> None: | |
self.service_client.update_api_key(value) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class NexusCompletionCallback: | ||
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. May want to mention this is unstable/experimental and also not really for user use (I understand exposing because it's exposed in the interceptor) 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. Done @dataclass(frozen=True)
class NexusCompletionCallback:
"""Nexus callback to attach to events such as workflow completion.
.. warning::
This option is experimental and unstable.
""" 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. Don't make callbacks Nexus specific and don't call them |
||
"""Nexus callback to attach to events such as workflow completion. | ||
|
||
.. warning:: | ||
This option is experimental and unstable. | ||
""" | ||
|
||
url: str | ||
"""Callback URL.""" | ||
|
||
header: Mapping[str, str] | ||
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. I think in Python we use |
||
"""Header to attach to callback request.""" | ||
|
||
|
||
async def _encode_user_metadata( | ||
converter: temporalio.converter.DataConverter, | ||
summary: Optional[Union[str, temporalio.api.common.v1.Payload]], | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -16,6 +16,7 @@ | |
from datetime import datetime | ||
from enum import IntEnum | ||
from itertools import zip_longest | ||
from logging import getLogger | ||
from typing import ( | ||
Any, | ||
Awaitable, | ||
|
@@ -40,6 +41,7 @@ | |
import google.protobuf.json_format | ||
import google.protobuf.message | ||
import google.protobuf.symbol_database | ||
import nexusrpc | ||
import typing_extensions | ||
|
||
import temporalio.api.common.v1 | ||
|
@@ -60,6 +62,8 @@ | |
if sys.version_info >= (3, 10): | ||
from types import UnionType | ||
|
||
logger = getLogger(__name__) | ||
|
||
|
||
class PayloadConverter(ABC): | ||
"""Base payload converter to/from multiple payloads/values.""" | ||
|
@@ -911,6 +915,12 @@ def _error_to_failure( | |
failure.child_workflow_execution_failure_info.retry_state = ( | ||
temporalio.api.enums.v1.RetryState.ValueType(error.retry_state or 0) | ||
) | ||
# TODO(nexus-prerelease): test coverage for this | ||
elif isinstance(error, temporalio.exceptions.NexusOperationError): | ||
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. For symmetry reasons, I suspect we also need to convert 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. I need to add test coverage per the comment, and for what you're saying. Maybe this is something to resolve when we merge the workflow caller. |
||
failure.nexus_operation_execution_failure_info.SetInParent() | ||
failure.nexus_operation_execution_failure_info.operation_token = ( | ||
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. What about the rest of the error fields? They should be copied over too. 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. Please also convert from |
||
error.operation_token | ||
) | ||
|
||
def from_failure( | ||
self, | ||
|
@@ -1006,6 +1016,33 @@ def from_failure( | |
if child_info.retry_state | ||
else None, | ||
) | ||
elif failure.HasField("nexus_handler_failure_info"): | ||
nexus_handler_failure_info = failure.nexus_handler_failure_info | ||
try: | ||
_type = nexusrpc.HandlerErrorType[nexus_handler_failure_info.type] | ||
except KeyError: | ||
logger.warning( | ||
f"Unknown Nexus HandlerErrorType: {nexus_handler_failure_info.type}" | ||
) | ||
_type = nexusrpc.HandlerErrorType.INTERNAL | ||
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 other SDKs we preserve the original string type for forwards compatibility. |
||
return nexusrpc.HandlerError( | ||
failure.message or "Nexus handler error", | ||
type=_type, | ||
retryable={ | ||
temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_RETRYABLE: True, | ||
temporalio.api.enums.v1.NexusHandlerErrorRetryBehavior.NEXUS_HANDLER_ERROR_RETRY_BEHAVIOR_NON_RETRYABLE: False, | ||
}.get(nexus_handler_failure_info.retry_behavior), | ||
Comment on lines
+1031
to
+1034
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. nit: you can do this without incurring the cost of constructing a dict on every conversion. |
||
) | ||
elif failure.HasField("nexus_operation_execution_failure_info"): | ||
nexus_op_failure_info = failure.nexus_operation_execution_failure_info | ||
err = temporalio.exceptions.NexusOperationError( | ||
failure.message or "Nexus operation error", | ||
scheduled_event_id=nexus_op_failure_info.scheduled_event_id, | ||
endpoint=nexus_op_failure_info.endpoint, | ||
service=nexus_op_failure_info.service, | ||
operation=nexus_op_failure_info.operation, | ||
operation_token=nexus_op_failure_info.operation_token, | ||
) | ||
else: | ||
err = temporalio.exceptions.FailureError(failure.message or "Failure error") | ||
err._failure = failure | ||
|
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.
I do not see this section
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.
Bump, we should add this section showing how to make simple Nexus Python operation and how to call Nexus operations from workflows.