@@ -1179,6 +1179,7 @@ def _execute_orchestrator(
11791179 if (req .HasField ("orchestrationTraceContext" ) and req .orchestrationTraceContext .HasField ("spanID" ) and req .orchestrationTraceContext .spanID .value ):
11801180 persisted_orch_span_id = req .orchestrationTraceContext .spanID .value
11811181
1182+ result : ExecutionResults | None = None
11821183 try :
11831184 executor = _OrchestrationExecutor (
11841185 self ._registry , self ._logger ,
@@ -1192,30 +1193,6 @@ def _execute_orchestrator(
11921193 result = executor .execute (
11931194 instance_id , req .pastEvents , req .newEvents )
11941195
1195- # Determine completion status for span
1196- is_complete = False
1197- is_failed = False
1198- failure_details = None
1199- for action in result .actions :
1200- if action .HasField ("completeOrchestration" ):
1201- is_complete = True
1202- orch_status = action .completeOrchestration .orchestrationStatus
1203- if orch_status == pb .ORCHESTRATION_STATUS_FAILED :
1204- is_failed = True
1205- failure_details = action .completeOrchestration .failureDetails
1206-
1207- if is_complete and self ._emit_trace_spans :
1208- # Orchestration finished — emit a single span covering its lifetime
1209- tracing .emit_orchestration_span (
1210- orchestration_name ,
1211- instance_id ,
1212- start_time_ns ,
1213- is_failed ,
1214- failure_details = failure_details ,
1215- parent_trace_context = parent_trace_ctx ,
1216- orchestration_trace_context = result ._orchestration_trace_context , # pyright: ignore[reportPrivateUsage]
1217- )
1218-
12191196 # Include the span ID in the orchestration trace context
12201197 # so it persists across dispatches.
12211198 orch_span_id = None
@@ -1278,10 +1255,54 @@ def _execute_orchestrator(
12781255
12791256 try :
12801257 # Externalize any large payloads in the response
1281- if self ._payload_store is not None :
1282- payload_helpers .externalize_payloads (
1283- res , self ._payload_store , instance_id = instance_id ,
1258+ try :
1259+ if self ._payload_store is not None :
1260+ payload_helpers .externalize_payloads (
1261+ res , self ._payload_store , instance_id = instance_id ,
1262+ )
1263+ except pe .PayloadSizeExceededError as ex :
1264+ self ._logger .warning (
1265+ f"Orchestration '{ instance_id } ' exceeded the payload storage limit: { ex } "
12841266 )
1267+ # Use the original actions, not a partially externalized response.
1268+ releases = [
1269+ action for action in result .actions
1270+ if action .HasField ("sendEntityMessage" )
1271+ and action .sendEntityMessage .HasField ("entityUnlockSent" )
1272+ ] if result is not None else []
1273+ next_id = max ((action .id for action in res .actions ), default = 0 ) + 1
1274+ if result is not None :
1275+ for release in result .entity_lock_releases :
1276+ releases .append (pb .OrchestratorAction (id = next_id , sendEntityMessage = release ))
1277+ next_id += 1
1278+ releases .append (ph .new_complete_orchestration_action (
1279+ next_id ,
1280+ pb .ORCHESTRATION_STATUS_FAILED ,
1281+ None ,
1282+ pb .TaskFailureDetails (
1283+ errorType = ph .get_qualified_name (type (ex )),
1284+ errorMessage = str (ex )[:1024 ],
1285+ isNonRetriable = True ,
1286+ ),
1287+ ))
1288+ res .ClearField ("actions" )
1289+ res .actions .extend (releases )
1290+ res .ClearField ("customStatus" )
1291+ # Keep this fallback inline: it must not need the failing store.
1292+
1293+ if result is not None and self ._emit_trace_spans :
1294+ for action in res .actions :
1295+ if action .HasField ("completeOrchestration" ):
1296+ completion = action .completeOrchestration
1297+ tracing .emit_orchestration_span (
1298+ orchestration_name ,
1299+ instance_id ,
1300+ start_time_ns ,
1301+ completion .orchestrationStatus == pb .ORCHESTRATION_STATUS_FAILED ,
1302+ failure_details = completion .failureDetails if completion .HasField ("failureDetails" ) else None ,
1303+ parent_trace_context = parent_trace_ctx ,
1304+ orchestration_trace_context = result ._orchestration_trace_context , # pyright: ignore[reportPrivateUsage]
1305+ )
12851306 stub .CompleteOrchestratorTask (res )
12861307 except Exception as ex :
12871308 self ._logger .exception (
@@ -2194,14 +2215,17 @@ class ExecutionResults:
21942215 actions : list [pb .OrchestratorAction ]
21952216 encoded_custom_status : str | None
21962217 _orchestration_trace_context : pb .TraceContext | None
2218+ entity_lock_releases : list [pb .SendEntityMessageAction ]
21972219
21982220 def __init__ (
21992221 self , actions : list [pb .OrchestratorAction ], encoded_custom_status : str | None ,
22002222 orchestration_trace_context : pb .TraceContext | None = None ,
2223+ entity_lock_releases : list [pb .SendEntityMessageAction ] | None = None ,
22012224 ):
22022225 self .actions = actions
22032226 self .encoded_custom_status = encoded_custom_status
22042227 self ._orchestration_trace_context = orchestration_trace_context
2228+ self .entity_lock_releases = entity_lock_releases if entity_lock_releases is not None else []
22052229
22062230
22072231class _OrchestrationExecutor :
@@ -2347,6 +2371,8 @@ def execute(
23472371 return ExecutionResults (
23482372 actions = actions , encoded_custom_status = ctx ._encoded_custom_status , # pyright: ignore[reportPrivateUsage]
23492373 orchestration_trace_context = ctx ._orchestration_trace_context , # pyright: ignore[reportPrivateUsage]
2374+ # Snapshot before generator teardown can exit a critical section.
2375+ entity_lock_releases = list (ctx ._entity_context .get_lock_release_messages ()), # pyright: ignore[reportPrivateUsage]
23502376 )
23512377 finally :
23522378 # Close after building the response, so the orchestrator's finally
0 commit comments