Skip to content

Commit 478e452

Browse files
committed
Use endpoint for system Nexus detection
1 parent 5f1d352 commit 478e452

6 files changed

Lines changed: 109 additions & 50 deletions

File tree

scripts/gen_payload_visitor.py

Lines changed: 5 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,11 @@ async def visit(
188188
async def _visit_nexus_operation_input_payload(
189189
self,
190190
fs: VisitorFunctions,
191-
service: str,
192-
operation: str,
191+
endpoint: str,
193192
payload: Payload,
194193
) -> None:
195194
new_payload = await temporalio.nexus.system.maybe_visit_payload(
196-
service,
197-
operation,
195+
endpoint,
198196
payload,
199197
fs,
200198
self.skip_search_attributes,
@@ -292,8 +290,7 @@ def walk(self, desc: Descriptor) -> bool:
292290
(
293291
"system_nexus",
294292
field.name,
295-
"o.service",
296-
"o.operation",
293+
"o.endpoint",
297294
"o.input",
298295
)
299296
)
@@ -406,11 +403,11 @@ def walk(self, desc: Descriptor) -> bool:
406403
)
407404
)
408405
elif item[0] == "system_nexus":
409-
_, field_name, service_expr, operation_expr, payload_expr = item
406+
_, field_name, endpoint_expr, payload_expr = item
410407
lines.append(
411408
f' if o.HasField("{field_name}"):\n'
412409
" await self._visit_nexus_operation_input_payload(\n"
413-
f" fs, {service_expr}, {operation_expr}, {payload_expr}\n"
410+
f" fs, {endpoint_expr}, {payload_expr}\n"
414411
" )"
415412
)
416413
else: # oneof_group

temporalio/bridge/_visitor.py

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,11 @@ async def visit(self, fs: VisitorFunctions, root: Any) -> None:
5858
async def _visit_nexus_operation_input_payload(
5959
self,
6060
fs: VisitorFunctions,
61-
service: str,
62-
operation: str,
61+
endpoint: str,
6362
payload: Payload,
6463
) -> None:
6564
new_payload = await temporalio.nexus.system.maybe_visit_payload(
66-
service,
67-
operation,
65+
endpoint,
6866
payload,
6967
fs,
7068
self.skip_search_attributes,
@@ -476,9 +474,7 @@ async def _visit_coresdk_workflow_commands_ScheduleNexusOperation(
476474
self, fs: VisitorFunctions, o: Any
477475
):
478476
if o.HasField("input"):
479-
await self._visit_nexus_operation_input_payload(
480-
fs, o.service, o.operation, o.input
481-
)
477+
await self._visit_nexus_operation_input_payload(fs, o.endpoint, o.input)
482478

483479
async def _visit_coresdk_workflow_commands_WorkflowCommand(
484480
self, fs: VisitorFunctions, o: Any

temporalio/nexus/system/__init__.py

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,12 @@
22

33
from __future__ import annotations
44

5-
import typing
6-
7-
import google.protobuf.message
8-
import nexusrpc
9-
105
import temporalio.api.common.v1
116
import temporalio.converter
127
from temporalio.bridge._visitor_functions import VisitorFunctions
138
from temporalio.converter import BinaryProtoPayloadConverter, CompositePayloadConverter
14-
from temporalio.nexus.system import workflow_service
9+
10+
TEMPORAL_SYSTEM_ENDPOINT = "__temporal_system"
1511

1612

1713
class SystemNexusPayloadConverter(CompositePayloadConverter):
@@ -22,32 +18,23 @@ def __init__(self) -> None:
2218
super().__init__(BinaryProtoPayloadConverter())
2319

2420

25-
def _operation(
26-
service: str, operation: str
27-
) -> nexusrpc.Operation[typing.Any, typing.Any] | None:
28-
return workflow_service.__nexus_operation_registry__.get((service, operation))
21+
def is_system_endpoint(endpoint: str) -> bool:
22+
"""Return whether a Nexus endpoint is the Temporal system endpoint."""
23+
return endpoint == TEMPORAL_SYSTEM_ENDPOINT
2924

3025

3126
async def maybe_visit_payload(
32-
service: str,
33-
operation: str,
27+
endpoint: str,
3428
payload: temporalio.api.common.v1.Payload,
3529
visitor_functions: VisitorFunctions,
3630
skip_search_attributes: bool,
3731
) -> temporalio.api.common.v1.Payload | None:
38-
"""Visit nested payloads if the payload is a recognized system Nexus envelope."""
39-
operation_def = _operation(service, operation)
40-
if operation_def is None:
41-
return None
42-
input_type = operation_def.input_type
43-
if not (
44-
isinstance(input_type, type)
45-
and issubclass(input_type, google.protobuf.message.Message)
46-
):
32+
"""Visit nested payloads if the payload is for the Temporal system endpoint."""
33+
if not is_system_endpoint(endpoint):
4734
return None
4835

4936
payload_converter = get_payload_converter()
50-
value = payload_converter.from_payload(payload, input_type)
37+
value = payload_converter.from_payload(payload)
5138
from ._payload_visitor import PayloadVisitor
5239

5340
await PayloadVisitor(skip_search_attributes=skip_search_attributes).visit(
@@ -56,19 +43,15 @@ async def maybe_visit_payload(
5643
return payload_converter.to_payload(value)
5744

5845

59-
def is_system_operation(service: str, operation: str) -> bool:
60-
"""Return whether a Nexus operation uses a generated system envelope."""
61-
return _operation(service, operation) is not None
62-
63-
6446
def get_payload_converter() -> temporalio.converter.PayloadConverter:
6547
"""Return the fixed payload converter for system Nexus outer envelopes."""
6648
return SystemNexusPayloadConverter()
6749

6850

6951
__all__ = [
52+
"TEMPORAL_SYSTEM_ENDPOINT",
7053
"get_payload_converter",
71-
"is_system_operation",
54+
"is_system_endpoint",
7255
"maybe_visit_payload",
7356
"SystemNexusPayloadConverter",
7457
]

temporalio/nexus/system/_payload_visitor.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,11 @@ async def visit(self, fs: VisitorFunctions, root: Any) -> None:
5858
async def _visit_nexus_operation_input_payload(
5959
self,
6060
fs: VisitorFunctions,
61-
service: str,
62-
operation: str,
61+
endpoint: str,
6362
payload: Payload,
6463
) -> None:
6564
new_payload = await temporalio.nexus.system.maybe_visit_payload(
66-
service,
67-
operation,
65+
endpoint,
6866
payload,
6967
fs,
7068
self.skip_search_attributes,

temporalio/worker/_workflow_instance.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2090,9 +2090,7 @@ async def operation_handle_fn() -> OutputT:
20902090

20912091
payload_converter = (
20922092
temporalio.nexus.system.get_payload_converter()
2093-
if temporalio.nexus.system.is_system_operation(
2094-
input.service, input.operation_name
2095-
)
2093+
if temporalio.nexus.system.is_system_endpoint(input.endpoint)
20962094
else self._context_free_payload_converter
20972095
)
20982096
handle = _NexusOperationHandle(

tests/nexus/test_temporal_system_nexus.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@
1515
import temporalio.converter
1616
import temporalio.nexus.system as nexus_system
1717
from temporalio import workflow
18+
from temporalio.bridge._visitor import PayloadVisitor
19+
from temporalio.bridge.proto.workflow_completion.workflow_completion_pb2 import (
20+
WorkflowActivationCompletion,
21+
)
1822
from temporalio.client import Client
1923
from temporalio.converter import ExternalStorage, PayloadCodec
2024
from temporalio.testing import WorkflowEnvironment
@@ -139,6 +143,89 @@ def _assert_start_nexus_operation_interceptor_trace() -> None:
139143
assert request.workflow_type.name == "test-workflow"
140144

141145

146+
class _MarkingPayloadVisitor:
147+
def __init__(self) -> None:
148+
self.visited_payload_count = 0
149+
self.system_envelope_count = 0
150+
151+
async def visit_payload(self, payload: temporalio.api.common.v1.Payload) -> None:
152+
self.visited_payload_count += 1
153+
payload.metadata["visited"] = b"true"
154+
155+
async def visit_payloads(
156+
self, payloads: Sequence[temporalio.api.common.v1.Payload]
157+
) -> None:
158+
for payload in payloads:
159+
await self.visit_payload(payload)
160+
161+
async def visit_system_nexus_envelope(
162+
self, payload: temporalio.api.common.v1.Payload
163+
) -> None:
164+
_ = payload
165+
self.system_envelope_count += 1
166+
167+
168+
def _new_schedule_nexus_completion(
169+
endpoint: str, payload: temporalio.api.common.v1.Payload
170+
) -> WorkflowActivationCompletion:
171+
completion = WorkflowActivationCompletion()
172+
command = completion.successful.commands.add()
173+
schedule = command.schedule_nexus_operation
174+
schedule.seq = 1
175+
schedule.endpoint = endpoint
176+
schedule.service = "not-a-registered-system-service"
177+
schedule.operation = "NotARegisteredSystemOperation"
178+
schedule.input.CopyFrom(payload)
179+
return completion
180+
181+
182+
def _new_system_nexus_request_payload() -> temporalio.api.common.v1.Payload:
183+
nested_payload = temporalio.converter.PayloadConverter.default.to_payload(
184+
"workflow-input"
185+
)
186+
assert nested_payload is not None
187+
request = workflowservice_pb2.SignalWithStartWorkflowExecutionRequest()
188+
request.input.payloads.add().CopyFrom(nested_payload)
189+
payload = nexus_system.get_payload_converter().to_payload(request)
190+
assert payload is not None
191+
return payload
192+
193+
194+
async def test_schedule_system_nexus_endpoint_ignores_operation_registry() -> None:
195+
completion = _new_schedule_nexus_completion(
196+
nexus_system.TEMPORAL_SYSTEM_ENDPOINT,
197+
_new_system_nexus_request_payload(),
198+
)
199+
visitor = _MarkingPayloadVisitor()
200+
201+
await PayloadVisitor().visit(visitor, completion)
202+
203+
schedule = completion.successful.commands[0].schedule_nexus_operation
204+
decoded = nexus_system.get_payload_converter().from_payload(schedule.input)
205+
assert isinstance(
206+
decoded, workflowservice_pb2.SignalWithStartWorkflowExecutionRequest
207+
)
208+
assert decoded.input.payloads[0].metadata["visited"] == b"true"
209+
assert "visited" not in schedule.input.metadata
210+
assert visitor.visited_payload_count == 1
211+
assert visitor.system_envelope_count == 1
212+
213+
214+
async def test_schedule_non_system_nexus_visits_input_as_regular_payload() -> None:
215+
completion = _new_schedule_nexus_completion(
216+
"not-the-system-endpoint",
217+
_new_system_nexus_request_payload(),
218+
)
219+
visitor = _MarkingPayloadVisitor()
220+
221+
await PayloadVisitor().visit(visitor, completion)
222+
223+
schedule = completion.successful.commands[0].schedule_nexus_operation
224+
assert schedule.input.metadata["visited"] == b"true"
225+
assert visitor.visited_payload_count == 1
226+
assert visitor.system_envelope_count == 0
227+
228+
142229
def _build_proto_sample(message_type: type[Message]) -> Message:
143230
message = message_type()
144231
_populate_proto_sample(message)

0 commit comments

Comments
 (0)