d-hervas opened a new pull request, #73004:
URL: https://github.com/apache/airflow/pull/73004

   Structured Triggerer logging reused one queued Watchtower handler across all 
CloudWatch stream paths. Watchtower retains queue, sequence-token, and worker 
state for every path until that handler closes, while Airflow keeps the remote 
I/O object alive for the Triggerer process.
   
   This change owns one Watchtower handler per active structured-log path and 
closes/removes that handler when `upload(path)` signals completion. Other 
active paths remain live, a later reuse of the same path creates a fresh 
handler, and records racing with close cannot reopen the path during teardown. 
The legacy task-handler path keeps its current behavior.
   
   ### Expected behavior
   When `upload(path)` marks one trigger stream complete, all
   queued records for that stream are delivered and its Watchtower queue, 
worker,
   and sequence-token state are released. Other active streams must remain 
usable,
   and later reuse of the same path must create fresh state.
   
   ### Failure mode
   
   A long-lived Triggerer can process many distinct trigger log paths, 
including sequentially. With Watchtower's default queued delivery, the first 
record for each path creates a queue, sequence-token entry, and worker thread. 
Airflow calls `upload(path)` when the trigger finishes, but the current 
implementation only flushes the shared handler. `flush()` delivers pending 
records without stopping the worker or removing that stream's state. Since the 
shared handler remains reachable for the lifetime of the Triggerer, retained 
state grows with the number of completed streams. Eventually the Triggerer's 
RSS or thread count can reach its container limit and the process is terminated 
with `SIGKILL`/OOM.
   
   The retention can be reproduced without AWS credentials or API calls using 
Watchtower 3.4.0. This mirrors the current provider behavior of changing 
`log_stream_name` on one queued handler:
   
   ```python
   import logging
   from itertools import count
   
   import watchtower
   
   
   class FakeLogsClient:
       def __init__(self):
           self.tokens = count(1)
   
       def put_log_events(self, **kwargs):
           return {"nextSequenceToken": str(next(self.tokens))}
   
   
   handler = watchtower.CloudWatchLogHandler(
       log_group_name="reproducer",
       log_stream_name="unused",
       boto3_client=FakeLogsClient(),
       create_log_group=False,
       create_log_stream=False,
       use_queues=True,
       send_interval=60,
   )
   
   for job_id in range(300):
       handler.log_stream_name = (
           f"dag/task/attempt=1.log.trigger.{job_id}.log"
       )
       record = logging.LogRecord(
           "trigger",
           logging.INFO,
           "repro.py",
           1,
           "event",
           (),
           None,
       )
       handler.handle(record)
   
   handler.flush()
   
   print("queues:", len(handler.queues))
   print("sequence tokens:", len(handler.sequence_tokens))
   print("thread references:", len(handler.threads))
   print("live workers:", sum(thread.is_alive() for thread in handler.threads))
   
   handler.close()
   ```
   
   Output:
   
   ```text
   queues: 300
   sequence tokens: 300
   thread references: 300
   live workers: 300
   ```
   
   Calling `flush()` therefore does not bound retained state as streams 
complete.
   Calling `close()` stops all workers, but the current shared-handler design 
cannot do that for one completed stream without also closing other active 
streams. 
   
   This change gives each active path an independently closable handler.
   
   
   ### Related work
   
   #70635 adds read-side discovery of deferred-task trigger log streams so they 
remain visible after deferral. This PR addresses the separate write-side 
lifecycle of those streams and releases Watchtower's retained queues, 
sequence-token state, and worker threads when each stream completes.
   
   ### Validation
   
   All 39 CloudWatch task-handler tests pass on Python 3.10.21 and Python 
3.14.7. New tests cover active-stream isolation, repeated completion, 
exact-path reuse, 100 sequential processor-driven paths with no retained 
handlers, construction and close/log races, and final shutdown. Ruff, the 
Airflow Python 3.10 CI image's pinned mypy for both changed files, the full 
pre-commit `prek` stage, and `git diff --check` also pass.
   
   ---
   ##### Was generative AI tooling used to co-author this PR?
   
   - [x] Yes: OpenAI Codex


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