cmarteepants commented on PR #65103:
URL: https://github.com/apache/airflow/pull/65103#issuecomment-4284323692
+1 on the direction of making watcher-side triggers asset-aware. AIP 103
doesn't really land without something like this, and the watermarking case is
the clearest motivation for why the trigger needs access to the asset's
attributes at runtime. That part of the problem is real and has to be solved.
Where I'd push back on is flipping the declaration relationship to get
there. Vincent and TP's concern about fragmenting the "one way to declare
watchers" model is valid, and I think we can give the trigger access to the
asset's attributes without inverting `Asset(watchers=[...])`.
Before getting to the shape, I want to raise a second case that's currently
hindering event-driven scheduling adoption, because I think both should be
solved together.
Today's model gives every asset its own watcher, and every watcher its own
trigger. With `MessageQueueTrigger`, that means each asset polls the queue
independently. In practice, a single SQS queue or Kafka topic usually carries
events for many logical assets. So users with 25 assets on one queue end up
with 25 trigger instances polling the same queue and filtering out messages
they don't care about. That's more polling than they need, plus coordination
concerns (visibility timeouts, independent consumer offsets etc.). Once
customers hit this, they usually drop off and move the fan-out logic into a
lambda or other server less compute that calls Airflow's REST API instead.
So there are two things the next iteration of the watcher should address:
1. **Watermark / per resource** (Jake's case): The trigger needs to read the
asset's state via AIP 103, e.g `max(updated)` scoped to one specific asset.
2. **Fan-In** (not solved today): One poller against a shared queue should
be able to produce events for many assets without N independent triggers.
The interesting thing is that the building blocks for both already exist in
the codebase, they just aren't composed. Kafka's `MessageQueueTrigger` already
takes an `apply_function` that evaluates each message and decides whether to
fire. If we (a) standardize that filter concept at the watcher level so it's
uniform across providers, and (b) let the scheduler deduplicate watchers whose
triggers share the same underlying config into a single poller, fan-in works
without changing the declaration pattern at all. Something like:
```python
from airflow.sdk import Asset, AssetWatcher
from airflow.providers.common.messaging.triggers import MessageQueueTrigger
# One shared queue config
shared_queue = dict(
scheme="sqs",
sqs_queue="https://sqs.us-east-1.amazonaws.com/.../orders-events",
)
message_queue_trigger = MessageQueueTrigger(**shared_queue)
orders = Asset(
"orders",
watchers=[AssetWatcher(
name="orders_watcher",
trigger=message_queue_trigger,
match=lambda msg: msg.attributes.get("event_type") ==
"order_created",
)],
)
refunds = Asset(
"refunds",
watchers=[AssetWatcher(
name="refunds_watcher",
trigger=message_queue_trigger,
match=lambda msg: msg.attributes.get("event_type") ==
"refund_issued",
)],
)
shipments = Asset(
"shipments",
watchers=[AssetWatcher(
name="shipments_watcher",
trigger=message_queue_trigger,
match=lambda msg: msg.attributes.get("event_type") ==
"shipment_dispatched",
)],
)
# Watermark case (same declaration pattern, no match needed)
# The watcher runtime passes the owning asset into the trigger so it can read
# its state via AIP-103.
users_table = Asset(
"users_table",
watchers=[AssetWatcher(
name="users_watermark",
trigger=TableWatermarkTrigger(table="users"),
# **context,
)],
)
```
What this gives us:
- **Declaration pattern preserved.** `Asset(watchers=[...])` stays.
- **Fan-in handled at runtime.** When Airflow sees multiple watchers with
equivalent trigger configs, it runs one poller and dispatches each message to
whichever watcher's `match` accepts it. Authors don't see or manage this.
- **Jake's watermark case still works.** The watcher runtime has always
known which asset it owns (it's declared right there on
`Asset(watchers=[...])`). The piece Jake needs to add is plumbing that asset
through to the trigger so it can read state. That plumbing doesn't require the
asset to be declared on the watcher; it just requires the runtime to hand it
over at trigger invocation time.
- **`apply_function` gets generalized.** The existing Kafka filter concept
becomes a first-class watcher feature available across all providers, which
also addresses #50369 thread about patterns for custom watchers.
I think the real question for this PR is whether the asset-access mechanism
Jake is adding can be implemented as "runtime passes the owning asset into the
trigger" rather than "user declares the asset on the watcher." If yes, we get
AIP-103 support without touching the declaration model, and we have a clean
path to solving fan-in next without a second breaking change. Happy to spin up
a separate issue on the fan-in piece if that's easier.
--
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]