Skip to content

Detect supervisor-subprocess trigger count mismatch in Triggerer - #68792

Open
JH0917 wants to merge 3 commits into
apache:mainfrom
JH0917:detect-unhandled-triggers
Open

Detect supervisor-subprocess trigger count mismatch in Triggerer#68792
JH0917 wants to merge 3 commits into
apache:mainfrom
JH0917:detect-unhandled-triggers

Conversation

@JH0917

@JH0917 JH0917 commented Jun 21, 2026

Copy link
Copy Markdown

In rare cases the Triggerer subprocess can lose track of a trigger — it
is assigned in the supervisor's running_triggers but no coroutine is
actually running. The affected task instance stays in deferred
permanently.

Changes:

  1. Supervisor compares len(running_triggers) against the subprocess's
    actual trigger count each sync cycle. If they diverge, it shuts down
    so the orchestrator can restart and recover stuck tasks.

  2. Creation failures are included in finished_ids so the supervisor
    removes them from running_triggers naturally (prevents false-positive
    shutdown).

related: #63913


Was generative AI tooling used to co-author this PR?
  • Yes — Claude Code (Opus 4.6)

Generated-by: Claude Code (Opus 4.6) following the guidelines

@boring-cyborg

boring-cyborg Bot commented Jun 21, 2026

Copy link
Copy Markdown

Congratulations on your first Pull Request and welcome to the Apache Airflow community! If you have any issues or are unsure about any anything please check our Contributors' Guide
Here are some useful points:

  • Pay attention to the quality of your code (ruff, mypy and type annotations). Our prek-hooks will help you with that.
  • In case of a new feature add useful documentation (in docstrings or in docs/ directory). Adding a new operator? Check this short guide Consider adding an example Dag that shows how users should use it.
  • Consider using Breeze environment for testing locally, it's a heavy docker but it ships with a working Airflow and a lot of integrations.
  • Be patient and persistent. It might take some time to get a review or get the final approval from Committers.
  • Please follow ASF Code of Conduct for all communication including (but not limited to) comments on Pull Requests, Mailing list and Slack.
  • Be sure to read the Airflow Coding style.
  • Always keep your Pull Requests rebased, otherwise your build might fail due to changes not related to your commits.
    Apache Airflow is a community-driven project and together we are making it better 🚀.
    In case of doubts contact the developers at:
    Mailing List: dev@airflow.apache.org
    Slack: https://s.apache.org/airflow-slack

@JH0917
JH0917 force-pushed the detect-unhandled-triggers branch 2 times, most recently from 2a820f1 to 8c3adff Compare June 22, 2026 08:19
@JH0917

JH0917 commented Jun 25, 2026

Copy link
Copy Markdown
Author

@dstandish @hussein-awala Could you approve the CI run? Thanks!

@JH0917

JH0917 commented Jul 1, 2026

Copy link
Copy Markdown
Author

@potiuk @dstandish @hussein-awala Friendly reminder - could someone approve the CI run? Thanks!

@potiuk potiuk added the ready for maintainer review Set after triaging when all criteria pass. label Jul 8, 2026
@JH0917
JH0917 force-pushed the detect-unhandled-triggers branch from 57f0d9b to 295071e Compare July 12, 2026 05:52
@JH0917

JH0917 commented Jul 13, 2026

Copy link
Copy Markdown
Author

Force-pushed with a fix for a false-positive path in the mismatch check.

@potiuk potiuk left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — detecting supervisor/subprocess drift is worth doing, and I checked the placement: the call sits between running_triggers.discard(id) for finished triggers (line 594) and running_triggers.update(...) for to_create (line 621), which is exactly the window your docstring describes. Test coverage is good too.

What I'd push back on is the remedy. self.stop = True feeds should_stop() and exits the supervisor run loop, so a single count disagreement takes down the whole Triggerer and every trigger running on it. That's a very large hammer for a condition the code itself describes as only valid inside a narrow window. Detail inline.

As with my other reviews today: these are design questions more than defects, my review was AI-assisted, and I'd rather you formed your own view than took mine as settled. Other maintainers who know the Triggerer supervisor protocol should weigh in before this lands.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

expected,
num_running,
)
self.stop = True

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This exits the supervisor loop via should_stop(), terminating the Triggerer and every trigger it is running. The tasks get picked up by another Triggerer eventually, so it's recoverable — but it's a disruptive, whole-process response to what may be a transient accounting difference.

The docstring concedes the invariant is only valid "between finished-removal and to_create-addition", which is a timing-dependent window: num_running is computed in the subprocess when it builds the message, and compared against supervisor state some time later. I'd want to see the argument for why no legitimate interleaving can produce a one-off difference before wiring it to a shutdown.

Softer options that still surface the bug: log at ERROR and emit a metric, or require N consecutive mismatches before stopping so a single transient blip doesn't cost an outage. If shutdown really is the right call, a comment explaining why the window is airtight would help the next reader.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks -- the shutdown is gone. The check now recovers instead: dropping the stranded ids from running_triggers / cancelling_triggers makes them fall out of known_trigger_ids in update_triggers, so the next loop rebuilds their workloads and nothing else running is disturbed. I added the ERROR log and the triggers.state_mismatch counter you suggested.

On the timing window: both sides only change at the message boundary, and the only code that adds a trigger runs in the runner's main loop -- which is blocked waiting for the reply while the supervisor compares. So the snapshot can't go stale and one disagreement is enough. That's why I skipped the N-consecutive debounce.

Drafted-by: Claude Code (Opus 5); reviewed by @JH0917 before posting

"""
expected = len(self.running_triggers)
if expected != num_running:
log.error(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Uses the module-level log, but _handle_request already receives a bound log: FilteringBoundLogger that carries the request context. Passing that through would keep this message correlated with the rest of the request's logging.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I can see that log only carries logger_name="supervisor" -- nothing is bound to it anywhere on the path. So I left it as is: request context rides on contextvars and shows up either way, and the difference would be that this line gets tagged [supervisor] while the rest of the class logs as [airflow.jobs.triggerer_job_runner]. Happy to be told I'm misreading it.

Drafted-by: Claude Code (Opus 5); reviewed by @JH0917 before posting

tb = format_exception(type(exc), exc, exc.__traceback__) if exc else None
failures_to_send.append((trigger_id, tb))
if trigger_id not in self.triggers:
finished_ids.append(trigger_id)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mutating the caller's list in place. I traced it and it's harmless today — finished_ids is freshly built in cleanup_finished_triggers() and isn't read again after sync_state_to_supervisor returns — so this is style, not a bug.

Still, process_trigger_events reads as a pure "build the message" function, and a caller that later reused its list would get a surprise. Building the combined list locally and leaving the parameter untouched keeps that property.


Drafted-by: Claude Code (Opus 5); reviewed by @potiuk before posting

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed -- builds a local list now, parameter is untouched, added regression test too. Thanks for pointing that out.

JH0917 and others added 3 commits August 2, 2026 16:13
Remove the incorrect difference_update on msg.failures (which would
break for serialization failures where the trigger is still running).

Instead, include creation-failure IDs in finished_ids so the supervisor
naturally removes them from running_triggers via the existing finished
processing path.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@JH0917
JH0917 force-pushed the detect-unhandled-triggers branch from 295071e to d0f5f2d Compare August 2, 2026 07:21
@JH0917
JH0917 requested review from amoghrajesh and ashb as code owners August 2, 2026 07:21
@eladkal eladkal added this to the Airflow 3.3.2 milestone Aug 3, 2026
@eladkal eladkal added type:bug-fix Changelog: Bug Fixes backport-to-v3-3-test Backport to v3-3-test labels Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Triggerer backport-to-v3-3-test Backport to v3-3-test ready for maintainer review Set after triaging when all criteria pass. type:bug-fix Changelog: Bug Fixes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants