buhuipao opened a new pull request, #76423:
URL: https://github.com/apache/airflow/pull/76423

   The triggerer supervisor reads the runner's sockets once per loop pass: one 
4 KiB `recv` on the log socket and at most one request frame. The rest of the 
pass is database work. The runner writes its logs with a blocking write from 
inside its event loop. When triggers log faster than about 4 KiB times the loop 
passes per second, the log socket fills up and the runner's event loop stops 
inside that write. No trigger runs, no state sync reaches the supervisor, and 
after `runner_health_check_threshold` the supervisor skips its heartbeat 
("TriggerRunner subprocess event loop appears deadlocked"). The triggers then 
move to the other triggerers, which pushes the same load onto them.
   
   It can also deadlock. The supervisor sends each reply with a blocking 
`sendall()`. A `TriggerStateSync` that carries many new triggers, for example 
right after a reassignment, is larger than the socket buffer. If the runner is 
blocked in a log write at that moment, it never reads the reply, and the 
supervisor never reads the logs. Both processes wait until something kills the 
triggerer. Stack dumps from both sides of a stuck run:
   
   ```
   supervisor: run_once -> _handle_request -> send_msg -> sendall
   runner:     run_trigger -> DateTimeTrigger.run -> log.info -> 
BytesLogger.msg -> write
   ```
   
   This change:
   
   * drains all runner output that is already waiting on each loop pass, up to 
one second, instead of a single 4 KiB read;
   * sends replies to the runner without blocking, and reads the runner's 
output sockets while a reply cannot be written yet.
   
   I reproduced this on Linux with the real `TriggerRunnerSupervisor` and 
runner, with only the database calls replaced by 20 ms sleeps: 1,000 
`DateTimeTrigger`s at INFO level, each re-armed 60 s after it fires.
   
   | | Before (5 runs) | After (5 runs) |
   |---|---|---|
   | Runs with a runner silence over 30 s | 5 (longest 52–128 s) | 0 (longest 
1.4 s) |
   | Runs that deadlocked | 2 | 0 |
   | Trigger fire latency p50 / p99 | 16–20 s / ~51 s | 0.9–1.2 s / 1.7–1.9 s |
   
   With the fix, two more runs that also assigned 1,000 extra triggers at once 
60 s in (like a reassignment) stayed healthy: longest silence 5.1–5.3 s, p99 
fire latency 4.1–4.9 s.
   
   Reading the output sockets during a send means an event from the caller's 
earlier `select()` can point at a socket that is already empty. The triggerer's 
output sockets are therefore switched to non-blocking, and 
`_service_subprocess` treats `BlockingIOError` from a handler as "nothing to 
read yet". For the same reason `make_buffered_socket_reader` now forgets the 
trailing fragment after sending it at EOF, so a second EOF does not send it 
again. The test fixtures that captured replies through `stdin.sendall` now 
capture them through `stdin.send`.
   
   The synchronous remote log upload for finished triggers in `_handle_request` 
also slows the loop down. It is left out of this PR, because moving it to the 
background changes when a trigger's log file can be reused by the next deferral 
of the same task try.
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   - [X] Yes — Claude Code (Opus 5.5)
   
   Generated-by: Claude Code (Opus 5.5) following [the 
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
   


-- 
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