{{ message }}
Repository navigation
Let partitioned Dag runs fire on a partial upstream window with wait_policy #66848
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
0cc1aad
feat(AIP-76): add rollup trigger policies for partitioned Dag runs
Lee-W a64a6b1
refactor(partition_mappers): drop B008 noqa on RollupMapper wait_poli…
Lee-W 63c9083
refactor(timetables): drop unrelated PartitionMapper import churn
Lee-W 13f2cbf
refactor(partition_mappers): use attrs for WaitPolicy value classes
Lee-W 2c00aeb
refactor(partition_mappers): stop exposing WaitPolicy base to Dag aut…
Lee-W 0c91453
docs(partition_mappers): tighten WaitPolicy comments and mark base pr…
Lee-W 2922bcb
refactor(serialization): delegate WaitPolicy serialization to the pol…
Lee-W 7ad068a
refactor(partition_mappers): dispatch rollup satisfaction via WaitPolicy
Lee-W b4d089e
refactor(partition_mappers): return structured rollup satisfaction fr…
Lee-W File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| Add ``wait_policy`` to ``RollupMapper`` — ``WaitForAll()`` (default) fires when all keys arrive; ``MinimumCount(n)`` fires on partial windows. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
174 changes: 174 additions & 0 deletions
174
airflow-core/src/airflow/partition_mappers/wait_policy.py
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,174 @@ | ||
| # Licensed to the Apache Software Foundation (ASF) under one | ||
| # or more contributor license agreements. See the NOTICE file | ||
| # distributed with this work for additional information | ||
| # regarding copyright ownership. The ASF licenses this file | ||
| # to you under the Apache License, Version 2.0 (the | ||
| # "License"); you may not use this file except in compliance | ||
| # with the License. You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, | ||
| # software distributed under the License is distributed on an | ||
| # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY | ||
| # KIND, either express or implied. See the License for the | ||
| # specific language governing permissions and limitations | ||
| # under the License. | ||
| from __future__ import annotations | ||
|
|
||
| from typing import TYPE_CHECKING, Any | ||
|
|
||
| import attrs | ||
|
|
||
| if TYPE_CHECKING: | ||
| from collections.abc import Set | ||
|
|
||
|
|
||
| @attrs.define(frozen=True) | ||
| class PartitionSatisfaction: | ||
| """ | ||
| Structured result returned by :meth:`WaitPolicy.is_satisfied_by_keys`. | ||
|
|
||
| :param satisfied: ``True`` if the matched key set meets the policy's firing threshold. | ||
| :param unreachable: ``True`` if the policy threshold can never be met given the window's | ||
| cardinality, regardless of how many upstream events arrive. | ||
| :param unreachable_reason: Human-readable explanation of why the policy is unreachable, | ||
| constructed atomically by the policy from its own repr and the window's cardinality. | ||
| Non-``None`` if and only if ``unreachable`` is ``True``; the scheduler may forward | ||
| this string directly to :meth:`~logging.Logger.warning` without further formatting. | ||
| """ | ||
|
|
||
| satisfied: bool | ||
| unreachable: bool | ||
| unreachable_reason: str | None | ||
|
|
||
| def __attrs_post_init__(self) -> None: | ||
| if self.unreachable and self.unreachable_reason is None: | ||
| raise ValueError("unreachable_reason must be set when unreachable is True") | ||
| if not self.unreachable and self.unreachable_reason is not None: | ||
| raise ValueError("unreachable_reason must be None when unreachable is False") | ||
|
|
||
|
|
||
| class WaitPolicy: | ||
| """ | ||
| An object the scheduler asks whether a partitioned Dag run should fire. | ||
|
|
||
| Concrete policies are ``WaitForAll`` and ``MinimumCount``. The scheduler | ||
| calls only :meth:`is_satisfied_by_keys`, which returns a :class:`PartitionSatisfaction` | ||
| carrying both the satisfaction result and the unreachability flag. | ||
| :meth:`is_satisfied` and :meth:`is_unreachable` are internal collaboration | ||
| points used by policy implementations; they are not called directly by the | ||
| scheduler. | ||
|
|
||
| :meta private: | ||
| """ | ||
|
|
||
| def is_satisfied(self, matched: int, expected: int) -> bool: | ||
| raise NotImplementedError | ||
|
|
||
| def is_satisfied_by_keys(self, *, matched: Set[str], expected: Set[str]) -> PartitionSatisfaction: | ||
|
Lee-W marked this conversation as resolved.
|
||
| """ | ||
| Return a :class:`PartitionSatisfaction` for the given key sets. | ||
|
|
||
| The base default converts sets to counts, then calls :meth:`is_satisfied` | ||
| and :meth:`is_unreachable` — both using ``len(expected)`` as the cardinality. | ||
| Override to avoid materialising the full intersection (see ``WaitForAll``). | ||
| """ | ||
| cardinality = len(expected) | ||
| unreachable = self.is_unreachable(cardinality) | ||
| return PartitionSatisfaction( | ||
| satisfied=self.is_satisfied(matched=len(matched & expected), expected=cardinality), | ||
| unreachable=unreachable, | ||
| unreachable_reason=( | ||
| f"wait policy {self!r} can never be satisfied given the window's cardinality {cardinality}" | ||
| if unreachable | ||
| else None | ||
| ), | ||
| ) | ||
|
|
||
| def is_unreachable(self, expected: int) -> bool: | ||
| raise NotImplementedError | ||
|
|
||
| def serialize(self) -> dict[str, Any]: | ||
| raise NotImplementedError | ||
|
|
||
| @classmethod | ||
| def deserialize(cls, data: dict[str, Any]) -> WaitPolicy: | ||
| raise NotImplementedError | ||
|
|
||
|
|
||
| @attrs.define(frozen=True) | ||
| class WaitForAll(WaitPolicy): | ||
| """ | ||
| Fires only when every expected upstream key has arrived (``matched == expected``). | ||
|
|
||
| An empty window (both zero) is vacuously satisfied and never unreachable. | ||
| """ | ||
|
|
||
| def is_satisfied(self, matched: int, expected: int) -> bool: | ||
| return matched == expected | ||
|
|
||
| def is_satisfied_by_keys(self, *, matched: Set[str], expected: Set[str]) -> PartitionSatisfaction: | ||
| # Short-circuits on the first missing key; avoids materializing the full intersection. | ||
| return PartitionSatisfaction( | ||
| satisfied=(expected <= matched), unreachable=False, unreachable_reason=None | ||
| ) | ||
|
|
||
| def is_unreachable(self, expected: int) -> bool: | ||
| return False | ||
|
|
||
| def serialize(self) -> dict[str, Any]: | ||
| return {} | ||
|
|
||
| @classmethod | ||
| def deserialize(cls, data: dict[str, Any]) -> WaitForAll: | ||
| return cls() | ||
|
|
||
|
|
||
| @attrs.define(frozen=True) | ||
| class MinimumCount(WaitPolicy): | ||
| """ | ||
| Fires once a minimum number of upstream keys have arrived. | ||
|
|
||
| ``n > 0``: fires when ``matched >= n`` (absolute lower bound). | ||
|
|
||
| ``n < 0``: fires when ``matched >= max(0, expected + n)`` — i.e. at most | ||
| ``-n`` keys are still missing. The clamp ensures negative offsets never | ||
| produce a negative effective threshold, keeping the empty-window case | ||
| vacuously satisfied. | ||
|
|
||
| ``n == 0`` is rejected at construction because it is degenerate: zero | ||
| would always fire, even on empty ticks. | ||
|
|
||
| ``is_unreachable(expected)`` returns ``True`` when ``n > 0`` and | ||
| ``n > expected``, meaning the threshold can never be met regardless of | ||
| how many upstream events arrive. Negative ``n`` is bounded by ``expected`` | ||
| after the clamp, so it is never unreachable. | ||
| """ | ||
|
|
||
| n: int = attrs.field() | ||
|
|
||
| @n.validator | ||
| def _validate_n(self, attribute: attrs.Attribute, value: int) -> None: | ||
| if value == 0: | ||
| raise ValueError( | ||
| "MinimumCount(0) is degenerate: n=0 would always fire, even on empty windows. " | ||
| "Use WaitForAll() to require every key, or MinimumCount(n) with n != 0." | ||
| ) | ||
|
|
||
| def is_satisfied(self, matched: int, expected: int) -> bool: | ||
| if self.n > 0: | ||
| return matched >= self.n | ||
| return matched >= max(0, expected + self.n) | ||
|
|
||
| def is_unreachable(self, expected: int) -> bool: | ||
| if self.n > 0: | ||
| return self.n > expected | ||
| return False | ||
|
|
||
| def serialize(self) -> dict[str, Any]: | ||
| return {"n": self.n} | ||
|
|
||
| @classmethod | ||
| def deserialize(cls, data: dict[str, Any]) -> MinimumCount: | ||
| return cls(data["n"]) | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
You can’t perform that action at this time.

Uh oh!
There was an error while loading. Please reload this page.