66import concurrent .futures
77import contextvars
88import threading
9- from collections .abc import Callable , Mapping , Sequence
9+ from collections .abc import Awaitable , Callable , Mapping , Sequence
1010from dataclasses import dataclass
1111from datetime import datetime , timezone
1212from functools import reduce
3030import temporalio .common
3131import temporalio .converter
3232import temporalio .nexus
33+ from temporalio .bridge ._visitor import PayloadVisitor
34+ from temporalio .bridge ._visitor_functions import PayloadSequence , VisitorFunctions
3335from temporalio .bridge .worker import PollShutdownError
3436from temporalio .exceptions import (
3537 ApplicationError ,
@@ -216,6 +218,19 @@ async def _complete_task(
216218 ):
217219 await asyncio .shield (self ._bridge_worker ().complete_nexus_task (completion ))
218220
221+ async def _encode_completion (
222+ self , completion : temporalio .bridge .proto .nexus .NexusTaskCompletion
223+ ) -> None :
224+ """Apply the payload codec then external storage to the completion's payloads."""
225+ dc = self ._data_converter
226+ await PayloadVisitor (skip_search_attributes = True , skip_headers = True ).visit (
227+ _PayloadTransformVisitor (dc ._encode_payload_sequence ), completion
228+ )
229+ await PayloadVisitor (skip_search_attributes = True ).visit (
230+ _PayloadTransformVisitor (dc ._external_store_payload_sequence ),
231+ completion ,
232+ )
233+
219234 # TODO(nexus-preview): stack trace pruning. See sdk-typescript NexusHandler.execute
220235 # "Any call up to this function and including this one will be trimmed out of stack traces.""
221236
@@ -260,6 +275,14 @@ async def _handle_cancel_operation_task(
260275 try :
261276 try :
262277 await self ._handler .cancel_operation (ctx , request .operation_token )
278+ completion = temporalio .bridge .proto .nexus .NexusTaskCompletion (
279+ task_token = task_token ,
280+ completed = temporalio .api .nexus .v1 .Response (
281+ cancel_operation = temporalio .api .nexus .v1 .CancelOperationResponse ()
282+ ),
283+ )
284+ # No-op but keeps the cancel covered if it ever carries a payload.
285+ await self ._encode_completion (completion )
263286 except asyncio .CancelledError :
264287 completion = temporalio .bridge .proto .nexus .NexusTaskCompletion (
265288 task_token = task_token ,
@@ -271,16 +294,12 @@ async def _handle_cancel_operation_task(
271294 completion = temporalio .bridge .proto .nexus .NexusTaskCompletion (
272295 task_token = task_token ,
273296 )
274- await self ._data_converter .encode_failure (
275- handler_error , completion .failure
276- )
277- else :
278- completion = temporalio .bridge .proto .nexus .NexusTaskCompletion (
279- task_token = task_token ,
280- completed = temporalio .api .nexus .v1 .Response (
281- cancel_operation = temporalio .api .nexus .v1 .CancelOperationResponse ()
282- ),
297+ self ._data_converter .failure_converter .to_failure (
298+ handler_error ,
299+ self ._data_converter .payload_converter ,
300+ completion .failure ,
283301 )
302+ await self ._encode_completion (completion )
284303 await self ._complete_task (completion )
285304 except Exception :
286305 logger .exception ("Failed to send Nexus task completion" )
@@ -315,6 +334,13 @@ async def _handle_start_operation_task(
315334 request_deadline ,
316335 endpoint ,
317336 )
337+ completion = temporalio .bridge .proto .nexus .NexusTaskCompletion (
338+ task_token = task_token ,
339+ completed = temporalio .api .nexus .v1 .Response (
340+ start_operation = start_response
341+ ),
342+ )
343+ await self ._encode_completion (completion )
318344 except asyncio .CancelledError :
319345 completion = temporalio .bridge .proto .nexus .NexusTaskCompletion (
320346 task_token = task_token ,
@@ -326,19 +352,15 @@ async def _handle_start_operation_task(
326352 task_token = task_token ,
327353 )
328354 handler_error = _exception_to_handler_error (err )
329- await self ._data_converter .encode_failure (
330- handler_error , completion .failure
355+ self ._data_converter .failure_converter .to_failure (
356+ handler_error ,
357+ self ._data_converter .payload_converter ,
358+ completion .failure ,
331359 )
332360
333361 if isinstance (err , concurrent .futures .BrokenExecutor ):
334362 self ._fail_worker_exception_queue .put_nowait (err )
335- else :
336- completion = temporalio .bridge .proto .nexus .NexusTaskCompletion (
337- task_token = task_token ,
338- completed = temporalio .api .nexus .v1 .Response (
339- start_operation = start_response
340- ),
341- )
363+ await self ._encode_completion (completion )
342364
343365 await self ._complete_task (completion )
344366 except Exception :
@@ -417,7 +439,9 @@ async def _start_operation(
417439 )
418440 )
419441 elif isinstance (result , nexusrpc .handler .StartOperationResultSync ):
420- [payload ] = await self ._data_converter .encode ([result .value ])
442+ [payload ] = self ._data_converter .payload_converter .to_payloads (
443+ [result .value ]
444+ )
421445 return temporalio .api .nexus .v1 .StartOperationResponse (
422446 sync_success = temporalio .api .nexus .v1 .StartOperationResponse .Sync (
423447 payload = payload ,
@@ -446,10 +470,41 @@ async def _start_operation(
446470 ) from err .__cause__
447471 except FailureError as new_err :
448472 response = temporalio .api .nexus .v1 .StartOperationResponse ()
449- await self ._data_converter .encode_failure (new_err , response .failure )
473+ self ._data_converter .failure_converter .to_failure (
474+ new_err ,
475+ self ._data_converter .payload_converter ,
476+ response .failure ,
477+ )
450478 return response
451479
452480
481+ class _PayloadTransformVisitor (VisitorFunctions ):
482+ """Adapts a payload-sequence transform for use with :class:`PayloadVisitor`."""
483+
484+ def __init__ (
485+ self ,
486+ f : Callable [
487+ [Sequence [temporalio .api .common .v1 .Payload ]],
488+ Awaitable [list [temporalio .api .common .v1 .Payload ]],
489+ ],
490+ ) -> None :
491+ self ._f = f
492+
493+ async def visit_payload (self , payload : temporalio .api .common .v1 .Payload ) -> None :
494+ new_payload = (await self ._f ([payload ]))[0 ]
495+ if new_payload is not payload :
496+ payload .CopyFrom (new_payload )
497+
498+ async def visit_payloads (self , payloads : PayloadSequence ) -> None :
499+ if len (payloads ) == 0 :
500+ return
501+ new_payloads = await self ._f (payloads )
502+ if new_payloads is payloads :
503+ return
504+ del payloads [:]
505+ payloads .extend (new_payloads )
506+
507+
453508@dataclass
454509class _DummyPayloadSerializer :
455510 data_converter : temporalio .converter .DataConverter
@@ -465,18 +520,35 @@ async def deserialize(
465520 content : nexusrpc .Content , # type:ignore[reportUnusedParameter]
466521 as_type : type [Any ] | None = None ,
467522 ) -> Any :
468- payload = self .payload
469- if self .data_converter .payload_codec :
470- try :
471- [payload ] = await self .data_converter .payload_codec .decode ([payload ])
472- except Exception as err :
473- raise nexusrpc .HandlerError (
474- "Payload codec failed to decode Nexus operation input" ,
475- type = nexusrpc .HandlerErrorType .INTERNAL ,
476- ) from err
523+ dc = self .data_converter
524+ # The visitor mutates in place, so work on a copy to leave the request
525+ # payload untouched.
526+ payload = temporalio .api .common .v1 .Payload ()
527+ payload .CopyFrom (self .payload )
528+ try :
529+ await PayloadVisitor (skip_search_attributes = True ).visit (
530+ _PayloadTransformVisitor (dc ._external_retrieve_payload_sequence ),
531+ payload ,
532+ )
533+ except Exception as err :
534+ raise nexusrpc .HandlerError (
535+ "Failed to retrieve Nexus operation input from external storage" ,
536+ type = nexusrpc .HandlerErrorType .INTERNAL ,
537+ retryable_override = True ,
538+ ) from err
539+
540+ try :
541+ await PayloadVisitor (skip_search_attributes = True , skip_headers = True ).visit (
542+ _PayloadTransformVisitor (dc ._decode_payload_sequence ), payload
543+ )
544+ except Exception as err :
545+ raise nexusrpc .HandlerError (
546+ "Payload codec failed to decode Nexus operation input" ,
547+ type = nexusrpc .HandlerErrorType .INTERNAL ,
548+ ) from err
477549
478550 try :
479- [input ] = self . data_converter .payload_converter .from_payloads (
551+ [input ] = dc .payload_converter .from_payloads (
480552 [payload ],
481553 type_hints = [as_type ] if as_type else None ,
482554 )
0 commit comments