1+ import logging
12from asyncio import CancelledError , InvalidStateError , Task
2- from typing import Optional
3+ from typing import Optional , Type
4+
35from cadence ._internal .workflow .deterministic_event_loop import DeterministicEventLoop
46from cadence .api .v1 .common_pb2 import Payload
57from cadence .data_converter import DataConverter
68from cadence .error import WorkflowFailure
79from cadence .workflow import WorkflowDefinition
810
11+ logger = logging .getLogger (__name__ )
12+
913
1014class WorkflowInstance :
1115 def __init__ (
@@ -29,6 +33,72 @@ def start(self, payload: Payload):
2933 )
3034 self ._task = self ._loop .create_task (run_method (* workflow_input ))
3135
36+ def handle_signal (
37+ self , signal_name : str , payload : Payload , event_id : int
38+ ) -> None :
39+ """Handle an incoming signal by invoking the registered signal handler.
40+
41+ Looks up the signal definition by name, decodes the payload using the
42+ data converter and parameter type hints, and invokes the handler on the
43+ workflow instance. Async handlers are scheduled as tasks on the
44+ deterministic event loop so they execute during run_once().
45+
46+ Args:
47+ signal_name: The name of the signal to handle.
48+ payload: The encoded signal input payload.
49+ event_id: The history event ID (used for logging context).
50+ """
51+ # Guard: reject signals after workflow completion (matches Java client)
52+ if self .is_done ():
53+ logger .warning (
54+ "Signal received after workflow is completed, ignoring" ,
55+ extra = {"signal_name" : signal_name , "event_id" : event_id },
56+ )
57+ return
58+
59+ signal_def = self ._definition .signals .get (signal_name )
60+ if signal_def is None :
61+ logger .warning (
62+ "Received signal with no registered handler, ignoring" ,
63+ extra = {"signal_name" : signal_name , "event_id" : event_id },
64+ )
65+ return
66+
67+ # Decode payload using parameter type hints from the signal definition.
68+ # Deserialization errors are caught and logged rather than crashing the
69+ # decision (matches Java client DataConverterException handling).
70+ type_hints : list [Type | None ] = [p .type_hint for p in signal_def .params ]
71+ try :
72+ if type_hints :
73+ decoded_args = self ._data_converter .from_data (payload , type_hints )
74+ else :
75+ decoded_args = []
76+ except Exception :
77+ logger .error (
78+ "Failed to deserialize signal payload, dropping signal" ,
79+ extra = {"signal_name" : signal_name , "event_id" : event_id },
80+ exc_info = True ,
81+ )
82+ return
83+
84+ # Invoke the handler on the workflow instance.
85+ # signal_def._wrapped is the unbound class method, so we pass
86+ # self._instance as the first argument.
87+ # Handler invocation errors are caught and logged rather than crashing
88+ # the decision task (matches Java client InvocationTargetException handling).
89+ try :
90+ if signal_def .is_async :
91+ coro = signal_def (self ._instance , * decoded_args )
92+ self ._loop .create_task (coro )
93+ else :
94+ signal_def (self ._instance , * decoded_args )
95+ except Exception :
96+ logger .error (
97+ "Signal handler raised an exception" ,
98+ extra = {"signal_name" : signal_name , "event_id" : event_id },
99+ exc_info = True ,
100+ )
101+
32102 def run_once (self ):
33103 self ._loop .run_until_yield ()
34104
0 commit comments