seanghaeli opened a new issue, #75352:
URL: https://github.com/apache/airflow/issues/75352

   ### Apache Airflow version
   
   main (development)
   
   ### What happened
   
   When two assets declare a watcher with the same trigger class and arguments, 
the Dag processor stores a single trigger linked to both assets. The triggerer 
then hands that trigger an asset state store covering every linked asset, and 
the store's `get`/`set` shortcuts (and `aget`/`aset`) refuse when there is more 
than one:
   
   ```
   ValueError: Task has 2 concrete inlets and outlets — use 
context['asset_state_store'][MY_ASSET] to specify which
   ```
   
   The trigger can't follow that advice either: it isn't told which assets it 
watches, and the accessor has no public way to list them. So a shared watcher 
trigger has no supported way to persist state.
   
   Today this means:
   
   - `KinesisTrigger` and `IcebergTableSnapshotTrigger` catch the `ValueError` 
and keep their cursor in memory only, so for a shared trigger every triggerer 
restart re-reads from the initial position (Kinesis) or re-emits the current 
snapshot (Iceberg).
   - A trigger that doesn't catch it crashes on its first read and is restarted 
every couple of seconds without ever firing. That's what `S3KeyUpdateTrigger` 
in #74293 does as of now.
   - Every new watcher that wants restart-safe state has to copy the same 
workaround.
   
   ### What you think should happen instead
   
   A watcher trigger should be able to persist state whether it serves one 
asset or several.
   
   ### How to reproduce
   
   ```python
   from airflow.providers.amazon.aws.triggers.s3 import S3KeyUpdateTrigger  # 
any watcher trigger that calls store.get
   from airflow.sdk import DAG, Asset, AssetWatcher
   
   def t():
       return S3KeyUpdateTrigger(bucket_name="b", bucket_key="k.txt")
   
   x = Asset("asset_x", watchers=[AssetWatcher(name="wx", trigger=t())])
   y = Asset("asset_y", watchers=[AssetWatcher(name="wy", trigger=t())])
   
   with DAG("two_assets", schedule=[x, y]):
       ...
   ```
   
   There is one `trigger` row linked to `asset_x` and `asset_y`, and 
`store.get(...)` raises on the trigger's first step.
   
   ### Anything else
   
   Where it comes from:
   
   - `airflow-core/src/airflow/dag_processing/collection.py`, 
`add_asset_trigger_references`: watchers are deduplicated by 
`BaseEventTrigger.hash(classpath, kwargs)`.
   - `airflow-core/src/airflow/jobs/triggerer_job_runner.py`: the trigger gets 
an accessor built from every asset linked to it.
   - `task-sdk/src/airflow/sdk/execution_time/context.py`: 
`AssetStateStoreAccessors._single_accessor` raises when there is more than one.
   
   Some possible directions:
   
   1. Give event triggers a store that works when shared, e.g. `set` writes to 
every watched asset and `get` returns the value only if they all agree.
   2. Let the trigger see which assets it watches, so it can pick.
   3. Don't share a trigger across assets for watchers that use the state store.
   
   At the very least, documenting this for trigger authors would stop new 
triggers from crash-looping.
   
   ### Are you willing to submit PR?
   
   - [X] Yes I am willing to submit a PR!
   
   ---
   Drafted-by: Claude Code (Opus 5.5)
   


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