ramitkataria commented on code in PR #75253:
URL: https://github.com/apache/airflow/pull/75253#discussion_r4449070205


##########
task-sdk/src/airflow/sdk/execution_time/callback_supervisor.py:
##########
@@ -489,9 +496,23 @@ def supervise_callback(
                 exit_code=exit_code,
                 duration=end - start,
             )
-            if exit_code != 0:
-                raise RuntimeError(f"Callback subprocess exited with code 
{exit_code}")
-            return exit_code
+            if exit_code == 0:
+                terminal_state = CallbackTerminalState.SUCCESS
+            else:
+                terminal_output = f"Callback subprocess exited with code 
{exit_code}"
+        except Exception as e:
+            terminal_output = f"Callback supervisor error: {type(e).__name__}: 
{e}"
+            log.exception("Callback supervisor error", workload_id=id)
+            raise
         finally:
+            try:
+                client.callbacks.finish(UUID(id), state=terminal_state, 
output=terminal_output)

Review Comment:
   The execution token from `/run` seems to only live for `[execution_api] 
jwt_expiration_time` (600s by default), and since the callback supervisor 
doesn't heartbeat, I don't think anything refreshes it while the callback runs. 
So a callback that runs longer than ~10 min without reading a Connection or 
Variable would get a 403 here, which isn't retried and gets swallowed by the 
`except`. On executors with no executor-event fallback, that would leave the 
row `RUNNING`. Maybe the supervisor could refresh the token periodically? 



##########
airflow-core/src/airflow/jobs/scheduler_job_runner.py:
##########
@@ -1538,14 +1539,52 @@ def process_executor_events(
                 )
                 continue
 
+            # The supervisor reports the outcome through PATCH 
/callbacks/{id}/state, this is
+            # in case a supervisor dies before reporting.
+            if callback.state not in (CallbackState.QUEUED, 
CallbackState.RUNNING):
+                cls.logger().debug(
+                    "Callback %s is already %s; ignoring executor event with 
state %s",
+                    callback_id,
+                    callback.state,
+                    state,
+                )
+                continue
+
+            new_values: dict[str, Any] = {"state": state}
+            if state == CallbackState.FAILED:
+                new_values["output"] = str(info) if info else "Execution 
failed"
+
+            # The QUEUED/RUNNING check above reads an ORM snapshot, and the 
supervisor can report a
+            # terminal state through the API before this write lands. 
Repeating it here stops a
+            # terminal row moving.
+            updated = cast(
+                "CursorResult",
+                session.execute(
+                    update(Callback)
+                    .where(
+                        Callback.id == callback_uuid,
+                        Callback.state.in_((CallbackState.QUEUED, 
CallbackState.RUNNING)),

Review Comment:
   Could a duplicate delivery (e.g. Celery `acks_late` redelivery) end up 
overriding a real success here? The second supervisor's `/run` would get a 409 
and fail, this `UPDATE` would write FAILED since the row is `RUNNING`, and then 
the original supervisor's `finish(SUCCESS)` gets a 409 and its SUCCESS event is 
ignored. Before this change the later SUCCESS would have overwritten it. Not 
sure what the cleanest fix is, since this path still needs to fail a `RUNNING` 
row whose supervisor died, so mostly flagging it in case it's worth handling or 
noting.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to