Skip to content

AIP-76: Hold Dag run until all upstream partitions arrive - #64571

Merged
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window
Jun 5, 2026
Merged

AIP-76: Hold Dag run until all upstream partitions arrive#64571
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window

Conversation

@Lee-W

@Lee-WLee-W commented Apr 1, 2026

Copy link
Copy Markdown
Member

Why

Closes: #59294

Why

Asset-partitioned Dags that aggregate many upstream slices into one downstream period (e.g., 60-minute-level events rolling up into one hourly Dag run) had no way to express that requirement — the scheduler would fire the downstream run as soon as any single upstream partition arrived.

This PR implements the rollup building block from AIP-76: a Window type that enumerates the full set of upstream partitions required for a downstream period, a RollupMapper that wires a source mapper to a window, and the scheduler logic to gate Dag runs until every required upstream key is present.

What

Partition mappers/windows

  • Add Window ABC and six concrete implementations (HourWindow, DayWindow, WeekWindow, MonthWindow, QuarterWindow, YearWindow) to both airflow-core and the Task SDK
  • Add RollupMapper that composes a source_mapper with a Window and exposes to_upstream(downstream_key) → frozenset[str]
  • Add decode_downstream / encode_upstream hooks to PartitionMapper and implement them in _BaseTemporalMapper; StartOfWeekMapper gets a regex-based override because %V is ambiguous with strptime.
  • Add week_start parameter to StartOfWeekMapper for non-Monday week starts

Scheduler

  • Rewrite _create_dagruns_for_partitioned_asset_dags to bulk-fetch serialized Dags and partition-key logs, removing N+1 queries, and cap per-tick work at MAX_PARTITION_DAG_RUNS_PER_TICK
  • Add _resolve_asset_partition_status / _check_rollup_asset_status to evaluate rollup satisfaction; non-rollup assets continue to satisfy immediately

Serialization

  • Add encode_window / decode_window and extend mapper encoder/decoder to round-trip RollupMapper and all Window subclasses

UI / API

  • Enrich next_run_assets endpoint with per-asset received_count, required_count, received_keys, required_keys, and is_rollup for partitioned Dags
  • Update AssetNode and AssetSchedule components to surface rollup progress (e.g. "12 / 24 received")
  • Add AssetProgressCell for inline progress in the Dags list

Was generative AI tooling used to co-author this PR?
  • Yes — Claude Sonnet 4.6

Generated-by: Claude Sonnet 4.6 following the guidelines

withDAG(
dag_id="daily_team_a_rollup",
schedule=PartitionedAssetTimetable(
assets=team_a_player_stats,
default_partition_mapper=RollupMapper(
source_mapper=StartOfDayMapper(),
window=DayWindow(),
),
),
catchup=False,
tags=["player-stats", "rollup"],
):
""" First rollup level: 24 hourly partitions of ``team_a_player_stats`` → one daily summary. ``StartOfDayMapper`` normalizes each upstream hourly timestamp (``%Y-%m-%dT%H:%M:%S``) to its day-start (``%Y-%m-%d``); ``DayWindow`` declares the downstream run needs all 24 hourly partitions before firing. Publishes ``daily_team_a`` so the monthly rollup below can consume it. """@task(outlets=[daily_team_a])defsummarise_team_a_day(dag_run=None):
"""Produce the full-day rollup once every hour has arrived."""ifTYPE_CHECKING:
assertdag_runprint(f"All 24 hourly partitions received. Day: {dag_run.partition_key}")
summarise_team_a_day()
imageimage
  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@boring-cyborgboring-cyborgBot added area:Scheduler including HA (high availability) scheduler area:task-sdk labels Apr 1, 2026
@Lee-WLee-W changed the title feat(AIP-76): windowfeat(AIP-76): implement to_upstreamApr 1, 2026
@kaxil
kaxil requested a review from CopilotApril 2, 2026 00:41

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements “rollup” support for partition mappers (AIP-76) by introducing a RollupMapper interface with to_upstream() and using it in the scheduler to wait for a complete set of upstream partition keys before creating partitioned asset-triggered DAG runs.

Changes:

  • Add RollupMapper base class (core + task SDK) with an abstract to_upstream() contract.
  • Implement to_upstream() for weekly and monthly temporal mappers (core + task SDK).
  • Update the scheduler’s partitioned-asset DAG-run creation logic to enforce rollup completeness when a mapper supports it.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
FileDescription
task-sdk/src/airflow/sdk/definitions/partition_mappers/base.pyIntroduces SDK-side RollupMapper abstraction.
task-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.pyAdds SDK to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/timetables/base.pyAdds get_partition_mapper() hook to the Timetable protocol.
airflow-core/src/airflow/partition_mappers/base.pyIntroduces core-side RollupMapper abstraction.
airflow-core/src/airflow/partition_mappers/temporal.pyAdds core to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/jobs/scheduler_job_runner.pyUses rollup mapper behavior to decide when partitioned asset-triggered DAG runs are ready.

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 2 times, most recently from e72dfa6 to e6d53f2CompareApril 7, 2026 09:57
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor
from __future__ importannotationsfromairflow.sdkimport (
DAG,
Asset,
CronPartitionTimetable,
PartitionedAssetTimetable,
WeeklyRollupMapper,
task,
)
daily_sales=Asset(uri="file://incoming/sales/daily.csv", name="daily_sales")
# Upstream Dag: produces one partition per day (key format: "2024-01-15T00:00:00")withDAG(
dag_id="ingest_daily_sales",
schedule=CronPartitionTimetable("0 0 * * *", timezone="UTC"),
):
@task(outlets=[daily_sales])defingest():
passingest()
# Downstream Dag: runs once all 7 daily partitions for a week have arrivedwithDAG(
dag_id="weekly_sales_report",
schedule=PartitionedAssetTimetable(
assets=daily_sales,
default_partition_mapper=WeeklyRollupMapper(),
),
catchup=False,
):
@taskdefgenerate_report(dag_run=None):
# dag_run.partition_key will be the week key, e.g. "2024-01-15 (W03)"print(dag_run.partition_key)
generate_report()

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 91c4ac3 to 93e82cbCompareApril 7, 2026 11:48
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor

The backend part is basically wrapped up, but the frontend and API side need some work. The UI is quite weired for these cases now

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 9934a73 to f52823cCompareApril 10, 2026 09:09
@kaxil
kaxil requested a review from CopilotApril 10, 2026 19:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (1)

airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py:1

  • Counting PartitionedAssetKeyLog.id can over-count when duplicate log rows exist for the same upstream partition key (e.g. retries/dup inserts), inflating total_received and potentially showing the run as satisfiable earlier than it should be. Consider counting distinct PartitionedAssetKeyLog.source_partition_key (and/or a distinct composite of (asset_id, source_partition_key)) to match the scheduler’s set-based satisfaction semantics.
# Licensed to the Apache Software Foundation (ASF) under one

Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/base.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/ui/src/components/AssetProgressCell.tsx Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 9 times, most recently from a64d06a to 9064515CompareApril 17, 2026 12:12
@Lee-W
Lee-W marked this pull request as ready for review April 20, 2026 06:32
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/common/partition_helpers.py Outdated
Comment threadairflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst
Comment threadairflow-core/src/airflow/partition_mappers/window.py
@dstandish

Copy link
Copy Markdown
Contributor

Hey @Lee-W

I was a little surprised to see so many lines for this change so I asked Claude to help me review whether there's any unnecessary complexity. Below are its findings. They seem plausible. What do you think?


The core design is clean (the Window types are tiny generators,
RollupMapper.to_upstream is a clear decode→expand→encode, and the scheduler gate comes down to one
expected.issubset(actual) check), and the test coverage on the scheduler paths is excellent. The
HA hardening in _create_dagruns_for_partitioned_asset_dags (bulk-fetch over the old N+1,
with_row_locks(skip_locked=True), the per-tick cap and deterministic order_by) all looks right.

One theme I'd like to resolve before merge: a meaningful chunk of complexity is self-induced,
concentrated in partition_mappers/temporal.py.

_compile_output_format_regex looks like more generality than the feature needs. Only two mappers
can't round-trip through strptime: StartOfWeekMapper (%V isn't strptime-parseable) and
StartOfQuarterMapper ({quarter} isn't a directive). The actual requirement is "recover Y/m/d" and
"recover Y/quarter". But the decode that motivated the whole compiler ends up discarding the token
it was built for:

StartOfWeekMapper.decode_downstream — %V is captured but never read

return datetime(int(match["Y"]), int(match["m"]), int(match["d"]))

Both decodes collapse to a dedicated per-mapper regex:

_WEEK_RE = re.compile(r"(?P\d{4})-(?P\d{2})-(?P\d{2})")
_QUARTER_RE = re.compile(r"(?P\d{4})-Q(?P[1-4])")

That drops the directive table, the {name} placeholder machinery, the placeholder_patterns arg,
and the five compile-time ValueError branches — plus the test class that only exists to exercise
that invented surface (test_rejects_adjacent_default_pattern_placeholders,
test_adjacent_placeholders_allowed_when_one_is_narrowed,
test_separator_between_default_placeholders_is_allowed). Roughly ~120 lines + tests, with
identical behavior for every shipped mapper. Could we go with the dedicated regexes for now and
add generality if/when a custom mapper actually needs it?

Follow-on: if the temporal decode simplifies, the base-class guard scaffolding in
partition_mappers/base.py — the init_subclass XOR check, expected_decoded_type, and the
runtime pairing check in RollupMapper.init — largely loses its purpose for the shipped
mappers, since it mostly exists to make the general decode/encode pair safe. Worth reassessing
whether it belongs now or arrives with the first real custom mapper.

Question (non-blocking): the audit-log path (_record_partition_audit_log /
_record_stale_apdr_audit_log) opens independent scoped=False sessions to survive an outer
rollback, plus a process-lifetime dedup set. For advisory Log rows where self.log.exception(...)
already records the same thing, is the rollback-survival guarantee worth the machinery for v1, or
would a plain log line do?

@Lee-W

Copy link
Copy Markdown
MemberAuthor

Hey @dstandish , thanks for reviewing. a few responses to the questions

Could we go with the dedicated regexes for now and add generality if/when a custom mapper actually needs it?

The regex compiler was added per @uranusjr's review: without it, a custom output_format forces the user to also override decode_downstream, which TP found surprising. Hardcoding _WEEK_RE / _QUARTER_RE saves the ~120 lines but is not that users friendly

is the rollback-survival guarantee worth the machinery for v1, or would a plain log line do?

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst

@uranusjruranusjr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find it somewhat awkward the UI API endpoints need to load the serialized dag to calculate data. But I guess it’s not too useful to overthink this unless there are known performance issues.

Should we add some links to Starlette for the format-parsing logic?

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/config_templates/config.yml Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
@dstandish

Copy link
Copy Markdown
Contributor

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

I don't really have a strong feeling about that. I tried to look for it in the code but could not find it.

And I don't stand in the way of merging this thing....

But... I can't help feeling that this PR is bigger than it should be. It's feels so big that it's kind of overwhelming to properly review. It spreads the reviewer's attention over a very wide area.

@Lee-W

Lee-W commented Jun 4, 2026

Copy link
Copy Markdown
MemberAuthor

Should we add some links to Starlette for the format-parsing logic?

yep, added a pointer to Starlette's compile_path in the _compile_output_format_regex docstring.

Lee-W added 9 commits June 4, 2026 23:26
- Introduces `RollupMapper(upstream_mapper=..., window=...)` so a partitioned
Dag run waits until the full set of upstream partitions for one downstream
period has arrived (e.g. 24 hourly events for a daily rollup).
- Ships 6 temporal `Window` built-ins (Hour / Day / Week / Month / Quarter /
Year); custom windows are rejected at serialization time.
- Surfaces frozen / mapper-error / partial-rollup state on the next-run-assets
UI; `pending_partition_count` and rollup-aware totals stay symmetric across
list/detail routes.
See `airflow-core/newsfragments/64571.significant.rst` for the full breakdown,
including the documented DST edge case for `DayWindow` + local-tz mappers.
…uler logs
The misconfigured-mapper and stale-APDR-cleanup paths persisted audit Log
rows on independent scoped=False sessions plus a per-process dedup set so
the rows survived an outer rollback. For advisory records this is heavier
than the problem warrants in v1 — the misconfig path already logs the
exception every tick, and stale cleanup now emits a structured info log.
Re-add the UI-visible audit rows if operators report needing them.
Add an ``is_rollup`` TypeGuard helper next to ``RollupMapper`` (mirroring
``is_mapped``) and use it in the partitioned-asset readiness check so the
mapper narrows to ``RollupMapper`` without a ``cast``.
…ion changes
Stale-cleanup previously dropped a pending AssetPartitionDagRun whenever the
Dag's serialized version changed, so any unrelated structural edit discarded
in-flight partition accumulation and could leave a rollup held forever.
Stamp a rollup-definition fingerprint (the serialized partition mappers of the
Dag's partitioned assets) on the APDR at creation and compare that against the
latest definition instead of the Dag version, so only a genuine mapper/window
change clears the run. Replaces the unreleased ``dag_version_id`` column on
AssetPartitionDagRun with ``rollup_fingerprint`` (migration 0119 amended).
The per-tick cap on pending AssetPartitionDagRun rows was exposed as the
``[scheduler] max_partition_dag_runs_to_create_per_loop`` setting, but it is a
performance safety bound (keeping the per-tick transaction from starving
executor heartbeats and regular scheduling), not a knob operators can
meaningfully tune. Drop the unreleased setting and keep the bound as a
module-level constant. The per-loop query LIMIT is retained.
Point the ``{name}`` placeholder handling at the Starlette routing prior art
it mirrors, so the parsing approach is discoverable.
The docs spellchecker rejects the coined abbreviation; spell it out as a
partition Dag run and drop the double spaces.
Spell out why a temporal upstream mapper is needed (it normalizes each
upstream key to the window's granularity), clarify that an identity mapper
paired with a temporal window fails as a type mismatch at construction, and
cross-reference RollupMapper / Window classes to the API docs.
…rint
The fingerprint tests built a core PartitionedAssetTimetable from Task SDK
Asset / RollupMapper objects, which tripped mypy arg-type checks. Construct it
with serialized assets and the core mapper/window types (matching the existing
test convention) imported at module top level; the serialized fingerprint is
unchanged.
@potiuk

Copy link
Copy Markdown
Member

@uranusjr's approval here is from Jun 2 (aa9f248), but there have been ~9 commits since — scheduler refactors, the partition-Dag-run clearing fix, mapper narrowing, etc. Worth a re-review so the green check reflects the current state before this merges. @uranusjr, could you re-confirm when you get a chance?


Drafted-by: Claude Code (Opus 4.8); reviewed by @potiuk before posting

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Schedulerincluding HA (high availability) schedulerarea:task-sdk

Projects

No open projects

Development

Successfully merging this pull request may close these issues.

Implement rollup (many-to-one partition mapper)

10 participants

@Lee-W@eladkal@dstandish@potiuk@ashb@uranusjr@kaxil@phanikumv@vatsrahul1001
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
AIP-76: Hold Dag run until all upstream partitions arrive by Lee-W · Pull Request #64571 · apache/airflow · GitHub
Skip to content

AIP-76: Hold Dag run until all upstream partitions arrive - #64571

Merged
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window
Jun 5, 2026
Merged

AIP-76: Hold Dag run until all upstream partitions arrive#64571
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window

Conversation

@Lee-W

@Lee-WLee-W commented Apr 1, 2026

Copy link
Copy Markdown
Member

Why

Closes: #59294

Why

Asset-partitioned Dags that aggregate many upstream slices into one downstream period (e.g., 60-minute-level events rolling up into one hourly Dag run) had no way to express that requirement — the scheduler would fire the downstream run as soon as any single upstream partition arrived.

This PR implements the rollup building block from AIP-76: a Window type that enumerates the full set of upstream partitions required for a downstream period, a RollupMapper that wires a source mapper to a window, and the scheduler logic to gate Dag runs until every required upstream key is present.

What

Partition mappers/windows

  • Add Window ABC and six concrete implementations (HourWindow, DayWindow, WeekWindow, MonthWindow, QuarterWindow, YearWindow) to both airflow-core and the Task SDK
  • Add RollupMapper that composes a source_mapper with a Window and exposes to_upstream(downstream_key) → frozenset[str]
  • Add decode_downstream / encode_upstream hooks to PartitionMapper and implement them in _BaseTemporalMapper; StartOfWeekMapper gets a regex-based override because %V is ambiguous with strptime.
  • Add week_start parameter to StartOfWeekMapper for non-Monday week starts

Scheduler

  • Rewrite _create_dagruns_for_partitioned_asset_dags to bulk-fetch serialized Dags and partition-key logs, removing N+1 queries, and cap per-tick work at MAX_PARTITION_DAG_RUNS_PER_TICK
  • Add _resolve_asset_partition_status / _check_rollup_asset_status to evaluate rollup satisfaction; non-rollup assets continue to satisfy immediately

Serialization

  • Add encode_window / decode_window and extend mapper encoder/decoder to round-trip RollupMapper and all Window subclasses

UI / API

  • Enrich next_run_assets endpoint with per-asset received_count, required_count, received_keys, required_keys, and is_rollup for partitioned Dags
  • Update AssetNode and AssetSchedule components to surface rollup progress (e.g. "12 / 24 received")
  • Add AssetProgressCell for inline progress in the Dags list

Was generative AI tooling used to co-author this PR?
  • Yes — Claude Sonnet 4.6

Generated-by: Claude Sonnet 4.6 following the guidelines

withDAG(
dag_id="daily_team_a_rollup",
schedule=PartitionedAssetTimetable(
assets=team_a_player_stats,
default_partition_mapper=RollupMapper(
source_mapper=StartOfDayMapper(),
window=DayWindow(),
),
),
catchup=False,
tags=["player-stats", "rollup"],
):
""" First rollup level: 24 hourly partitions of ``team_a_player_stats`` → one daily summary. ``StartOfDayMapper`` normalizes each upstream hourly timestamp (``%Y-%m-%dT%H:%M:%S``) to its day-start (``%Y-%m-%d``); ``DayWindow`` declares the downstream run needs all 24 hourly partitions before firing. Publishes ``daily_team_a`` so the monthly rollup below can consume it. """@task(outlets=[daily_team_a])defsummarise_team_a_day(dag_run=None):
"""Produce the full-day rollup once every hour has arrived."""ifTYPE_CHECKING:
assertdag_runprint(f"All 24 hourly partitions received. Day: {dag_run.partition_key}")
summarise_team_a_day()
imageimage
  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@boring-cyborgboring-cyborgBot added area:Scheduler including HA (high availability) scheduler area:task-sdk labels Apr 1, 2026
@Lee-WLee-W changed the title feat(AIP-76): windowfeat(AIP-76): implement to_upstreamApr 1, 2026
@kaxil
kaxil requested a review from CopilotApril 2, 2026 00:41

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements “rollup” support for partition mappers (AIP-76) by introducing a RollupMapper interface with to_upstream() and using it in the scheduler to wait for a complete set of upstream partition keys before creating partitioned asset-triggered DAG runs.

Changes:

  • Add RollupMapper base class (core + task SDK) with an abstract to_upstream() contract.
  • Implement to_upstream() for weekly and monthly temporal mappers (core + task SDK).
  • Update the scheduler’s partitioned-asset DAG-run creation logic to enforce rollup completeness when a mapper supports it.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
FileDescription
task-sdk/src/airflow/sdk/definitions/partition_mappers/base.pyIntroduces SDK-side RollupMapper abstraction.
task-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.pyAdds SDK to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/timetables/base.pyAdds get_partition_mapper() hook to the Timetable protocol.
airflow-core/src/airflow/partition_mappers/base.pyIntroduces core-side RollupMapper abstraction.
airflow-core/src/airflow/partition_mappers/temporal.pyAdds core to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/jobs/scheduler_job_runner.pyUses rollup mapper behavior to decide when partitioned asset-triggered DAG runs are ready.

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 2 times, most recently from e72dfa6 to e6d53f2CompareApril 7, 2026 09:57
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor
from __future__ importannotationsfromairflow.sdkimport (
DAG,
Asset,
CronPartitionTimetable,
PartitionedAssetTimetable,
WeeklyRollupMapper,
task,
)
daily_sales=Asset(uri="file://incoming/sales/daily.csv", name="daily_sales")
# Upstream Dag: produces one partition per day (key format: "2024-01-15T00:00:00")withDAG(
dag_id="ingest_daily_sales",
schedule=CronPartitionTimetable("0 0 * * *", timezone="UTC"),
):
@task(outlets=[daily_sales])defingest():
passingest()
# Downstream Dag: runs once all 7 daily partitions for a week have arrivedwithDAG(
dag_id="weekly_sales_report",
schedule=PartitionedAssetTimetable(
assets=daily_sales,
default_partition_mapper=WeeklyRollupMapper(),
),
catchup=False,
):
@taskdefgenerate_report(dag_run=None):
# dag_run.partition_key will be the week key, e.g. "2024-01-15 (W03)"print(dag_run.partition_key)
generate_report()

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 91c4ac3 to 93e82cbCompareApril 7, 2026 11:48
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor

The backend part is basically wrapped up, but the frontend and API side need some work. The UI is quite weired for these cases now

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 9934a73 to f52823cCompareApril 10, 2026 09:09
@kaxil
kaxil requested a review from CopilotApril 10, 2026 19:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (1)

airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py:1

  • Counting PartitionedAssetKeyLog.id can over-count when duplicate log rows exist for the same upstream partition key (e.g. retries/dup inserts), inflating total_received and potentially showing the run as satisfiable earlier than it should be. Consider counting distinct PartitionedAssetKeyLog.source_partition_key (and/or a distinct composite of (asset_id, source_partition_key)) to match the scheduler’s set-based satisfaction semantics.
# Licensed to the Apache Software Foundation (ASF) under one

Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/base.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/ui/src/components/AssetProgressCell.tsx Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 9 times, most recently from a64d06a to 9064515CompareApril 17, 2026 12:12
@Lee-W
Lee-W marked this pull request as ready for review April 20, 2026 06:32
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/common/partition_helpers.py Outdated
Comment threadairflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst
Comment threadairflow-core/src/airflow/partition_mappers/window.py
@dstandish

Copy link
Copy Markdown
Contributor

Hey @Lee-W

I was a little surprised to see so many lines for this change so I asked Claude to help me review whether there's any unnecessary complexity. Below are its findings. They seem plausible. What do you think?


The core design is clean (the Window types are tiny generators,
RollupMapper.to_upstream is a clear decode→expand→encode, and the scheduler gate comes down to one
expected.issubset(actual) check), and the test coverage on the scheduler paths is excellent. The
HA hardening in _create_dagruns_for_partitioned_asset_dags (bulk-fetch over the old N+1,
with_row_locks(skip_locked=True), the per-tick cap and deterministic order_by) all looks right.

One theme I'd like to resolve before merge: a meaningful chunk of complexity is self-induced,
concentrated in partition_mappers/temporal.py.

_compile_output_format_regex looks like more generality than the feature needs. Only two mappers
can't round-trip through strptime: StartOfWeekMapper (%V isn't strptime-parseable) and
StartOfQuarterMapper ({quarter} isn't a directive). The actual requirement is "recover Y/m/d" and
"recover Y/quarter". But the decode that motivated the whole compiler ends up discarding the token
it was built for:

StartOfWeekMapper.decode_downstream — %V is captured but never read

return datetime(int(match["Y"]), int(match["m"]), int(match["d"]))

Both decodes collapse to a dedicated per-mapper regex:

_WEEK_RE = re.compile(r"(?P\d{4})-(?P\d{2})-(?P\d{2})")
_QUARTER_RE = re.compile(r"(?P\d{4})-Q(?P[1-4])")

That drops the directive table, the {name} placeholder machinery, the placeholder_patterns arg,
and the five compile-time ValueError branches — plus the test class that only exists to exercise
that invented surface (test_rejects_adjacent_default_pattern_placeholders,
test_adjacent_placeholders_allowed_when_one_is_narrowed,
test_separator_between_default_placeholders_is_allowed). Roughly ~120 lines + tests, with
identical behavior for every shipped mapper. Could we go with the dedicated regexes for now and
add generality if/when a custom mapper actually needs it?

Follow-on: if the temporal decode simplifies, the base-class guard scaffolding in
partition_mappers/base.py — the init_subclass XOR check, expected_decoded_type, and the
runtime pairing check in RollupMapper.init — largely loses its purpose for the shipped
mappers, since it mostly exists to make the general decode/encode pair safe. Worth reassessing
whether it belongs now or arrives with the first real custom mapper.

Question (non-blocking): the audit-log path (_record_partition_audit_log /
_record_stale_apdr_audit_log) opens independent scoped=False sessions to survive an outer
rollback, plus a process-lifetime dedup set. For advisory Log rows where self.log.exception(...)
already records the same thing, is the rollback-survival guarantee worth the machinery for v1, or
would a plain log line do?

@Lee-W

Copy link
Copy Markdown
MemberAuthor

Hey @dstandish , thanks for reviewing. a few responses to the questions

Could we go with the dedicated regexes for now and add generality if/when a custom mapper actually needs it?

The regex compiler was added per @uranusjr's review: without it, a custom output_format forces the user to also override decode_downstream, which TP found surprising. Hardcoding _WEEK_RE / _QUARTER_RE saves the ~120 lines but is not that users friendly

is the rollback-survival guarantee worth the machinery for v1, or would a plain log line do?

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst

@uranusjruranusjr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find it somewhat awkward the UI API endpoints need to load the serialized dag to calculate data. But I guess it’s not too useful to overthink this unless there are known performance issues.

Should we add some links to Starlette for the format-parsing logic?

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/config_templates/config.yml Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
@dstandish

Copy link
Copy Markdown
Contributor

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

I don't really have a strong feeling about that. I tried to look for it in the code but could not find it.

And I don't stand in the way of merging this thing....

But... I can't help feeling that this PR is bigger than it should be. It's feels so big that it's kind of overwhelming to properly review. It spreads the reviewer's attention over a very wide area.

@Lee-W

Lee-W commented Jun 4, 2026

Copy link
Copy Markdown
MemberAuthor

Should we add some links to Starlette for the format-parsing logic?

yep, added a pointer to Starlette's compile_path in the _compile_output_format_regex docstring.

Lee-W added 9 commits June 4, 2026 23:26
- Introduces `RollupMapper(upstream_mapper=..., window=...)` so a partitioned
Dag run waits until the full set of upstream partitions for one downstream
period has arrived (e.g. 24 hourly events for a daily rollup).
- Ships 6 temporal `Window` built-ins (Hour / Day / Week / Month / Quarter /
Year); custom windows are rejected at serialization time.
- Surfaces frozen / mapper-error / partial-rollup state on the next-run-assets
UI; `pending_partition_count` and rollup-aware totals stay symmetric across
list/detail routes.
See `airflow-core/newsfragments/64571.significant.rst` for the full breakdown,
including the documented DST edge case for `DayWindow` + local-tz mappers.
…uler logs
The misconfigured-mapper and stale-APDR-cleanup paths persisted audit Log
rows on independent scoped=False sessions plus a per-process dedup set so
the rows survived an outer rollback. For advisory records this is heavier
than the problem warrants in v1 — the misconfig path already logs the
exception every tick, and stale cleanup now emits a structured info log.
Re-add the UI-visible audit rows if operators report needing them.
Add an ``is_rollup`` TypeGuard helper next to ``RollupMapper`` (mirroring
``is_mapped``) and use it in the partitioned-asset readiness check so the
mapper narrows to ``RollupMapper`` without a ``cast``.
…ion changes
Stale-cleanup previously dropped a pending AssetPartitionDagRun whenever the
Dag's serialized version changed, so any unrelated structural edit discarded
in-flight partition accumulation and could leave a rollup held forever.
Stamp a rollup-definition fingerprint (the serialized partition mappers of the
Dag's partitioned assets) on the APDR at creation and compare that against the
latest definition instead of the Dag version, so only a genuine mapper/window
change clears the run. Replaces the unreleased ``dag_version_id`` column on
AssetPartitionDagRun with ``rollup_fingerprint`` (migration 0119 amended).
The per-tick cap on pending AssetPartitionDagRun rows was exposed as the
``[scheduler] max_partition_dag_runs_to_create_per_loop`` setting, but it is a
performance safety bound (keeping the per-tick transaction from starving
executor heartbeats and regular scheduling), not a knob operators can
meaningfully tune. Drop the unreleased setting and keep the bound as a
module-level constant. The per-loop query LIMIT is retained.
Point the ``{name}`` placeholder handling at the Starlette routing prior art
it mirrors, so the parsing approach is discoverable.
The docs spellchecker rejects the coined abbreviation; spell it out as a
partition Dag run and drop the double spaces.
Spell out why a temporal upstream mapper is needed (it normalizes each
upstream key to the window's granularity), clarify that an identity mapper
paired with a temporal window fails as a type mismatch at construction, and
cross-reference RollupMapper / Window classes to the API docs.
…rint
The fingerprint tests built a core PartitionedAssetTimetable from Task SDK
Asset / RollupMapper objects, which tripped mypy arg-type checks. Construct it
with serialized assets and the core mapper/window types (matching the existing
test convention) imported at module top level; the serialized fingerprint is
unchanged.
@potiuk

Copy link
Copy Markdown
Member

@uranusjr's approval here is from Jun 2 (aa9f248), but there have been ~9 commits since — scheduler refactors, the partition-Dag-run clearing fix, mapper narrowing, etc. Worth a re-review so the green check reflects the current state before this merges. @uranusjr, could you re-confirm when you get a chance?


Drafted-by: Claude Code (Opus 4.8); reviewed by @potiuk before posting

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Schedulerincluding HA (high availability) schedulerarea:task-sdk

Projects

No open projects

Development

Successfully merging this pull request may close these issues.

Implement rollup (many-to-one partition mapper)

10 participants

@Lee-W@eladkal@dstandish@potiuk@ashb@uranusjr@kaxil@phanikumv@vatsrahul1001
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' AIP-76: Hold Dag run until all upstream partitions arrive by Lee-W · Pull Request #64571 · apache/airflow · GitHub
Skip to content

AIP-76: Hold Dag run until all upstream partitions arrive - #64571

Merged
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window
Jun 5, 2026
Merged

AIP-76: Hold Dag run until all upstream partitions arrive#64571
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window

Conversation

@Lee-W

@Lee-WLee-W commented Apr 1, 2026

Copy link
Copy Markdown
Member

Why

Closes: #59294

Why

Asset-partitioned Dags that aggregate many upstream slices into one downstream period (e.g., 60-minute-level events rolling up into one hourly Dag run) had no way to express that requirement — the scheduler would fire the downstream run as soon as any single upstream partition arrived.

This PR implements the rollup building block from AIP-76: a Window type that enumerates the full set of upstream partitions required for a downstream period, a RollupMapper that wires a source mapper to a window, and the scheduler logic to gate Dag runs until every required upstream key is present.

What

Partition mappers/windows

  • Add Window ABC and six concrete implementations (HourWindow, DayWindow, WeekWindow, MonthWindow, QuarterWindow, YearWindow) to both airflow-core and the Task SDK
  • Add RollupMapper that composes a source_mapper with a Window and exposes to_upstream(downstream_key) → frozenset[str]
  • Add decode_downstream / encode_upstream hooks to PartitionMapper and implement them in _BaseTemporalMapper; StartOfWeekMapper gets a regex-based override because %V is ambiguous with strptime.
  • Add week_start parameter to StartOfWeekMapper for non-Monday week starts

Scheduler

  • Rewrite _create_dagruns_for_partitioned_asset_dags to bulk-fetch serialized Dags and partition-key logs, removing N+1 queries, and cap per-tick work at MAX_PARTITION_DAG_RUNS_PER_TICK
  • Add _resolve_asset_partition_status / _check_rollup_asset_status to evaluate rollup satisfaction; non-rollup assets continue to satisfy immediately

Serialization

  • Add encode_window / decode_window and extend mapper encoder/decoder to round-trip RollupMapper and all Window subclasses

UI / API

  • Enrich next_run_assets endpoint with per-asset received_count, required_count, received_keys, required_keys, and is_rollup for partitioned Dags
  • Update AssetNode and AssetSchedule components to surface rollup progress (e.g. "12 / 24 received")
  • Add AssetProgressCell for inline progress in the Dags list

Was generative AI tooling used to co-author this PR?
  • Yes — Claude Sonnet 4.6

Generated-by: Claude Sonnet 4.6 following the guidelines

withDAG(
dag_id="daily_team_a_rollup",
schedule=PartitionedAssetTimetable(
assets=team_a_player_stats,
default_partition_mapper=RollupMapper(
source_mapper=StartOfDayMapper(),
window=DayWindow(),
),
),
catchup=False,
tags=["player-stats", "rollup"],
):
""" First rollup level: 24 hourly partitions of ``team_a_player_stats`` → one daily summary. ``StartOfDayMapper`` normalizes each upstream hourly timestamp (``%Y-%m-%dT%H:%M:%S``) to its day-start (``%Y-%m-%d``); ``DayWindow`` declares the downstream run needs all 24 hourly partitions before firing. Publishes ``daily_team_a`` so the monthly rollup below can consume it. """@task(outlets=[daily_team_a])defsummarise_team_a_day(dag_run=None):
"""Produce the full-day rollup once every hour has arrived."""ifTYPE_CHECKING:
assertdag_runprint(f"All 24 hourly partitions received. Day: {dag_run.partition_key}")
summarise_team_a_day()
imageimage
  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@boring-cyborgboring-cyborgBot added area:Scheduler including HA (high availability) scheduler area:task-sdk labels Apr 1, 2026
@Lee-WLee-W changed the title feat(AIP-76): windowfeat(AIP-76): implement to_upstreamApr 1, 2026
@kaxil
kaxil requested a review from CopilotApril 2, 2026 00:41

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements “rollup” support for partition mappers (AIP-76) by introducing a RollupMapper interface with to_upstream() and using it in the scheduler to wait for a complete set of upstream partition keys before creating partitioned asset-triggered DAG runs.

Changes:

  • Add RollupMapper base class (core + task SDK) with an abstract to_upstream() contract.
  • Implement to_upstream() for weekly and monthly temporal mappers (core + task SDK).
  • Update the scheduler’s partitioned-asset DAG-run creation logic to enforce rollup completeness when a mapper supports it.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
FileDescription
task-sdk/src/airflow/sdk/definitions/partition_mappers/base.pyIntroduces SDK-side RollupMapper abstraction.
task-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.pyAdds SDK to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/timetables/base.pyAdds get_partition_mapper() hook to the Timetable protocol.
airflow-core/src/airflow/partition_mappers/base.pyIntroduces core-side RollupMapper abstraction.
airflow-core/src/airflow/partition_mappers/temporal.pyAdds core to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/jobs/scheduler_job_runner.pyUses rollup mapper behavior to decide when partitioned asset-triggered DAG runs are ready.

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 2 times, most recently from e72dfa6 to e6d53f2CompareApril 7, 2026 09:57
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor
from __future__ importannotationsfromairflow.sdkimport (
DAG,
Asset,
CronPartitionTimetable,
PartitionedAssetTimetable,
WeeklyRollupMapper,
task,
)
daily_sales=Asset(uri="file://incoming/sales/daily.csv", name="daily_sales")
# Upstream Dag: produces one partition per day (key format: "2024-01-15T00:00:00")withDAG(
dag_id="ingest_daily_sales",
schedule=CronPartitionTimetable("0 0 * * *", timezone="UTC"),
):
@task(outlets=[daily_sales])defingest():
passingest()
# Downstream Dag: runs once all 7 daily partitions for a week have arrivedwithDAG(
dag_id="weekly_sales_report",
schedule=PartitionedAssetTimetable(
assets=daily_sales,
default_partition_mapper=WeeklyRollupMapper(),
),
catchup=False,
):
@taskdefgenerate_report(dag_run=None):
# dag_run.partition_key will be the week key, e.g. "2024-01-15 (W03)"print(dag_run.partition_key)
generate_report()

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 91c4ac3 to 93e82cbCompareApril 7, 2026 11:48
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor

The backend part is basically wrapped up, but the frontend and API side need some work. The UI is quite weired for these cases now

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 9934a73 to f52823cCompareApril 10, 2026 09:09
@kaxil
kaxil requested a review from CopilotApril 10, 2026 19:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (1)

airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py:1

  • Counting PartitionedAssetKeyLog.id can over-count when duplicate log rows exist for the same upstream partition key (e.g. retries/dup inserts), inflating total_received and potentially showing the run as satisfiable earlier than it should be. Consider counting distinct PartitionedAssetKeyLog.source_partition_key (and/or a distinct composite of (asset_id, source_partition_key)) to match the scheduler’s set-based satisfaction semantics.
# Licensed to the Apache Software Foundation (ASF) under one

Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/base.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/ui/src/components/AssetProgressCell.tsx Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 9 times, most recently from a64d06a to 9064515CompareApril 17, 2026 12:12
@Lee-W
Lee-W marked this pull request as ready for review April 20, 2026 06:32
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/common/partition_helpers.py Outdated
Comment threadairflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst
Comment threadairflow-core/src/airflow/partition_mappers/window.py
@dstandish

Copy link
Copy Markdown
Contributor

Hey @Lee-W

I was a little surprised to see so many lines for this change so I asked Claude to help me review whether there's any unnecessary complexity. Below are its findings. They seem plausible. What do you think?


The core design is clean (the Window types are tiny generators,
RollupMapper.to_upstream is a clear decode→expand→encode, and the scheduler gate comes down to one
expected.issubset(actual) check), and the test coverage on the scheduler paths is excellent. The
HA hardening in _create_dagruns_for_partitioned_asset_dags (bulk-fetch over the old N+1,
with_row_locks(skip_locked=True), the per-tick cap and deterministic order_by) all looks right.

One theme I'd like to resolve before merge: a meaningful chunk of complexity is self-induced,
concentrated in partition_mappers/temporal.py.

_compile_output_format_regex looks like more generality than the feature needs. Only two mappers
can't round-trip through strptime: StartOfWeekMapper (%V isn't strptime-parseable) and
StartOfQuarterMapper ({quarter} isn't a directive). The actual requirement is "recover Y/m/d" and
"recover Y/quarter". But the decode that motivated the whole compiler ends up discarding the token
it was built for:

StartOfWeekMapper.decode_downstream — %V is captured but never read

return datetime(int(match["Y"]), int(match["m"]), int(match["d"]))

Both decodes collapse to a dedicated per-mapper regex:

_WEEK_RE = re.compile(r"(?P\d{4})-(?P\d{2})-(?P\d{2})")
_QUARTER_RE = re.compile(r"(?P\d{4})-Q(?P[1-4])")

That drops the directive table, the {name} placeholder machinery, the placeholder_patterns arg,
and the five compile-time ValueError branches — plus the test class that only exists to exercise
that invented surface (test_rejects_adjacent_default_pattern_placeholders,
test_adjacent_placeholders_allowed_when_one_is_narrowed,
test_separator_between_default_placeholders_is_allowed). Roughly ~120 lines + tests, with
identical behavior for every shipped mapper. Could we go with the dedicated regexes for now and
add generality if/when a custom mapper actually needs it?

Follow-on: if the temporal decode simplifies, the base-class guard scaffolding in
partition_mappers/base.py — the init_subclass XOR check, expected_decoded_type, and the
runtime pairing check in RollupMapper.init — largely loses its purpose for the shipped
mappers, since it mostly exists to make the general decode/encode pair safe. Worth reassessing
whether it belongs now or arrives with the first real custom mapper.

Question (non-blocking): the audit-log path (_record_partition_audit_log /
_record_stale_apdr_audit_log) opens independent scoped=False sessions to survive an outer
rollback, plus a process-lifetime dedup set. For advisory Log rows where self.log.exception(...)
already records the same thing, is the rollback-survival guarantee worth the machinery for v1, or
would a plain log line do?

@Lee-W

Copy link
Copy Markdown
MemberAuthor

Hey @dstandish , thanks for reviewing. a few responses to the questions

Could we go with the dedicated regexes for now and add generality if/when a custom mapper actually needs it?

The regex compiler was added per @uranusjr's review: without it, a custom output_format forces the user to also override decode_downstream, which TP found surprising. Hardcoding _WEEK_RE / _QUARTER_RE saves the ~120 lines but is not that users friendly

is the rollback-survival guarantee worth the machinery for v1, or would a plain log line do?

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst

@uranusjruranusjr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find it somewhat awkward the UI API endpoints need to load the serialized dag to calculate data. But I guess it’s not too useful to overthink this unless there are known performance issues.

Should we add some links to Starlette for the format-parsing logic?

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/config_templates/config.yml Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
@dstandish

Copy link
Copy Markdown
Contributor

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

I don't really have a strong feeling about that. I tried to look for it in the code but could not find it.

And I don't stand in the way of merging this thing....

But... I can't help feeling that this PR is bigger than it should be. It's feels so big that it's kind of overwhelming to properly review. It spreads the reviewer's attention over a very wide area.

@Lee-W

Lee-W commented Jun 4, 2026

Copy link
Copy Markdown
MemberAuthor

Should we add some links to Starlette for the format-parsing logic?

yep, added a pointer to Starlette's compile_path in the _compile_output_format_regex docstring.

Lee-W added 9 commits June 4, 2026 23:26
- Introduces `RollupMapper(upstream_mapper=..., window=...)` so a partitioned
Dag run waits until the full set of upstream partitions for one downstream
period has arrived (e.g. 24 hourly events for a daily rollup).
- Ships 6 temporal `Window` built-ins (Hour / Day / Week / Month / Quarter /
Year); custom windows are rejected at serialization time.
- Surfaces frozen / mapper-error / partial-rollup state on the next-run-assets
UI; `pending_partition_count` and rollup-aware totals stay symmetric across
list/detail routes.
See `airflow-core/newsfragments/64571.significant.rst` for the full breakdown,
including the documented DST edge case for `DayWindow` + local-tz mappers.
…uler logs
The misconfigured-mapper and stale-APDR-cleanup paths persisted audit Log
rows on independent scoped=False sessions plus a per-process dedup set so
the rows survived an outer rollback. For advisory records this is heavier
than the problem warrants in v1 — the misconfig path already logs the
exception every tick, and stale cleanup now emits a structured info log.
Re-add the UI-visible audit rows if operators report needing them.
Add an ``is_rollup`` TypeGuard helper next to ``RollupMapper`` (mirroring
``is_mapped``) and use it in the partitioned-asset readiness check so the
mapper narrows to ``RollupMapper`` without a ``cast``.
…ion changes
Stale-cleanup previously dropped a pending AssetPartitionDagRun whenever the
Dag's serialized version changed, so any unrelated structural edit discarded
in-flight partition accumulation and could leave a rollup held forever.
Stamp a rollup-definition fingerprint (the serialized partition mappers of the
Dag's partitioned assets) on the APDR at creation and compare that against the
latest definition instead of the Dag version, so only a genuine mapper/window
change clears the run. Replaces the unreleased ``dag_version_id`` column on
AssetPartitionDagRun with ``rollup_fingerprint`` (migration 0119 amended).
The per-tick cap on pending AssetPartitionDagRun rows was exposed as the
``[scheduler] max_partition_dag_runs_to_create_per_loop`` setting, but it is a
performance safety bound (keeping the per-tick transaction from starving
executor heartbeats and regular scheduling), not a knob operators can
meaningfully tune. Drop the unreleased setting and keep the bound as a
module-level constant. The per-loop query LIMIT is retained.
Point the ``{name}`` placeholder handling at the Starlette routing prior art
it mirrors, so the parsing approach is discoverable.
The docs spellchecker rejects the coined abbreviation; spell it out as a
partition Dag run and drop the double spaces.
Spell out why a temporal upstream mapper is needed (it normalizes each
upstream key to the window's granularity), clarify that an identity mapper
paired with a temporal window fails as a type mismatch at construction, and
cross-reference RollupMapper / Window classes to the API docs.
…rint
The fingerprint tests built a core PartitionedAssetTimetable from Task SDK
Asset / RollupMapper objects, which tripped mypy arg-type checks. Construct it
with serialized assets and the core mapper/window types (matching the existing
test convention) imported at module top level; the serialized fingerprint is
unchanged.
@potiuk

Copy link
Copy Markdown
Member

@uranusjr's approval here is from Jun 2 (aa9f248), but there have been ~9 commits since — scheduler refactors, the partition-Dag-run clearing fix, mapper narrowing, etc. Worth a re-review so the green check reflects the current state before this merges. @uranusjr, could you re-confirm when you get a chance?


Drafted-by: Claude Code (Opus 4.8); reviewed by @potiuk before posting

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Schedulerincluding HA (high availability) schedulerarea:task-sdk

Projects

No open projects

Development

Successfully merging this pull request may close these issues.

Implement rollup (many-to-one partition mapper)

10 participants

@Lee-W@eladkal@dstandish@potiuk@ashb@uranusjr@kaxil@phanikumv@vatsrahul1001
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' AIP-76: Hold Dag run until all upstream partitions arrive by Lee-W · Pull Request #64571 · apache/airflow · GitHub
Skip to content

AIP-76: Hold Dag run until all upstream partitions arrive - #64571

Merged
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window
Jun 5, 2026
Merged

AIP-76: Hold Dag run until all upstream partitions arrive#64571
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window

Conversation

@Lee-W

@Lee-WLee-W commented Apr 1, 2026

Copy link
Copy Markdown
Member

Why

Closes: #59294

Why

Asset-partitioned Dags that aggregate many upstream slices into one downstream period (e.g., 60-minute-level events rolling up into one hourly Dag run) had no way to express that requirement — the scheduler would fire the downstream run as soon as any single upstream partition arrived.

This PR implements the rollup building block from AIP-76: a Window type that enumerates the full set of upstream partitions required for a downstream period, a RollupMapper that wires a source mapper to a window, and the scheduler logic to gate Dag runs until every required upstream key is present.

What

Partition mappers/windows

  • Add Window ABC and six concrete implementations (HourWindow, DayWindow, WeekWindow, MonthWindow, QuarterWindow, YearWindow) to both airflow-core and the Task SDK
  • Add RollupMapper that composes a source_mapper with a Window and exposes to_upstream(downstream_key) → frozenset[str]
  • Add decode_downstream / encode_upstream hooks to PartitionMapper and implement them in _BaseTemporalMapper; StartOfWeekMapper gets a regex-based override because %V is ambiguous with strptime.
  • Add week_start parameter to StartOfWeekMapper for non-Monday week starts

Scheduler

  • Rewrite _create_dagruns_for_partitioned_asset_dags to bulk-fetch serialized Dags and partition-key logs, removing N+1 queries, and cap per-tick work at MAX_PARTITION_DAG_RUNS_PER_TICK
  • Add _resolve_asset_partition_status / _check_rollup_asset_status to evaluate rollup satisfaction; non-rollup assets continue to satisfy immediately

Serialization

  • Add encode_window / decode_window and extend mapper encoder/decoder to round-trip RollupMapper and all Window subclasses

UI / API

  • Enrich next_run_assets endpoint with per-asset received_count, required_count, received_keys, required_keys, and is_rollup for partitioned Dags
  • Update AssetNode and AssetSchedule components to surface rollup progress (e.g. "12 / 24 received")
  • Add AssetProgressCell for inline progress in the Dags list

Was generative AI tooling used to co-author this PR?
  • Yes — Claude Sonnet 4.6

Generated-by: Claude Sonnet 4.6 following the guidelines

withDAG(
dag_id="daily_team_a_rollup",
schedule=PartitionedAssetTimetable(
assets=team_a_player_stats,
default_partition_mapper=RollupMapper(
source_mapper=StartOfDayMapper(),
window=DayWindow(),
),
),
catchup=False,
tags=["player-stats", "rollup"],
):
""" First rollup level: 24 hourly partitions of ``team_a_player_stats`` → one daily summary. ``StartOfDayMapper`` normalizes each upstream hourly timestamp (``%Y-%m-%dT%H:%M:%S``) to its day-start (``%Y-%m-%d``); ``DayWindow`` declares the downstream run needs all 24 hourly partitions before firing. Publishes ``daily_team_a`` so the monthly rollup below can consume it. """@task(outlets=[daily_team_a])defsummarise_team_a_day(dag_run=None):
"""Produce the full-day rollup once every hour has arrived."""ifTYPE_CHECKING:
assertdag_runprint(f"All 24 hourly partitions received. Day: {dag_run.partition_key}")
summarise_team_a_day()
imageimage
  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@boring-cyborgboring-cyborgBot added area:Scheduler including HA (high availability) scheduler area:task-sdk labels Apr 1, 2026
@Lee-WLee-W changed the title feat(AIP-76): windowfeat(AIP-76): implement to_upstreamApr 1, 2026
@kaxil
kaxil requested a review from CopilotApril 2, 2026 00:41

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements “rollup” support for partition mappers (AIP-76) by introducing a RollupMapper interface with to_upstream() and using it in the scheduler to wait for a complete set of upstream partition keys before creating partitioned asset-triggered DAG runs.

Changes:

  • Add RollupMapper base class (core + task SDK) with an abstract to_upstream() contract.
  • Implement to_upstream() for weekly and monthly temporal mappers (core + task SDK).
  • Update the scheduler’s partitioned-asset DAG-run creation logic to enforce rollup completeness when a mapper supports it.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
FileDescription
task-sdk/src/airflow/sdk/definitions/partition_mappers/base.pyIntroduces SDK-side RollupMapper abstraction.
task-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.pyAdds SDK to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/timetables/base.pyAdds get_partition_mapper() hook to the Timetable protocol.
airflow-core/src/airflow/partition_mappers/base.pyIntroduces core-side RollupMapper abstraction.
airflow-core/src/airflow/partition_mappers/temporal.pyAdds core to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/jobs/scheduler_job_runner.pyUses rollup mapper behavior to decide when partitioned asset-triggered DAG runs are ready.

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 2 times, most recently from e72dfa6 to e6d53f2CompareApril 7, 2026 09:57
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor
from __future__ importannotationsfromairflow.sdkimport (
DAG,
Asset,
CronPartitionTimetable,
PartitionedAssetTimetable,
WeeklyRollupMapper,
task,
)
daily_sales=Asset(uri="file://incoming/sales/daily.csv", name="daily_sales")
# Upstream Dag: produces one partition per day (key format: "2024-01-15T00:00:00")withDAG(
dag_id="ingest_daily_sales",
schedule=CronPartitionTimetable("0 0 * * *", timezone="UTC"),
):
@task(outlets=[daily_sales])defingest():
passingest()
# Downstream Dag: runs once all 7 daily partitions for a week have arrivedwithDAG(
dag_id="weekly_sales_report",
schedule=PartitionedAssetTimetable(
assets=daily_sales,
default_partition_mapper=WeeklyRollupMapper(),
),
catchup=False,
):
@taskdefgenerate_report(dag_run=None):
# dag_run.partition_key will be the week key, e.g. "2024-01-15 (W03)"print(dag_run.partition_key)
generate_report()

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 91c4ac3 to 93e82cbCompareApril 7, 2026 11:48
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor

The backend part is basically wrapped up, but the frontend and API side need some work. The UI is quite weired for these cases now

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 9934a73 to f52823cCompareApril 10, 2026 09:09
@kaxil
kaxil requested a review from CopilotApril 10, 2026 19:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (1)

airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py:1

  • Counting PartitionedAssetKeyLog.id can over-count when duplicate log rows exist for the same upstream partition key (e.g. retries/dup inserts), inflating total_received and potentially showing the run as satisfiable earlier than it should be. Consider counting distinct PartitionedAssetKeyLog.source_partition_key (and/or a distinct composite of (asset_id, source_partition_key)) to match the scheduler’s set-based satisfaction semantics.
# Licensed to the Apache Software Foundation (ASF) under one

Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/base.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/ui/src/components/AssetProgressCell.tsx Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 9 times, most recently from a64d06a to 9064515CompareApril 17, 2026 12:12
@Lee-W
Lee-W marked this pull request as ready for review April 20, 2026 06:32
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/common/partition_helpers.py Outdated
Comment threadairflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst
Comment threadairflow-core/src/airflow/partition_mappers/window.py
@dstandish

Copy link
Copy Markdown
Contributor

Hey @Lee-W

I was a little surprised to see so many lines for this change so I asked Claude to help me review whether there's any unnecessary complexity. Below are its findings. They seem plausible. What do you think?


The core design is clean (the Window types are tiny generators,
RollupMapper.to_upstream is a clear decode→expand→encode, and the scheduler gate comes down to one
expected.issubset(actual) check), and the test coverage on the scheduler paths is excellent. The
HA hardening in _create_dagruns_for_partitioned_asset_dags (bulk-fetch over the old N+1,
with_row_locks(skip_locked=True), the per-tick cap and deterministic order_by) all looks right.

One theme I'd like to resolve before merge: a meaningful chunk of complexity is self-induced,
concentrated in partition_mappers/temporal.py.

_compile_output_format_regex looks like more generality than the feature needs. Only two mappers
can't round-trip through strptime: StartOfWeekMapper (%V isn't strptime-parseable) and
StartOfQuarterMapper ({quarter} isn't a directive). The actual requirement is "recover Y/m/d" and
"recover Y/quarter". But the decode that motivated the whole compiler ends up discarding the token
it was built for:

StartOfWeekMapper.decode_downstream — %V is captured but never read

return datetime(int(match["Y"]), int(match["m"]), int(match["d"]))

Both decodes collapse to a dedicated per-mapper regex:

_WEEK_RE = re.compile(r"(?P\d{4})-(?P\d{2})-(?P\d{2})")
_QUARTER_RE = re.compile(r"(?P\d{4})-Q(?P[1-4])")

That drops the directive table, the {name} placeholder machinery, the placeholder_patterns arg,
and the five compile-time ValueError branches — plus the test class that only exists to exercise
that invented surface (test_rejects_adjacent_default_pattern_placeholders,
test_adjacent_placeholders_allowed_when_one_is_narrowed,
test_separator_between_default_placeholders_is_allowed). Roughly ~120 lines + tests, with
identical behavior for every shipped mapper. Could we go with the dedicated regexes for now and
add generality if/when a custom mapper actually needs it?

Follow-on: if the temporal decode simplifies, the base-class guard scaffolding in
partition_mappers/base.py — the init_subclass XOR check, expected_decoded_type, and the
runtime pairing check in RollupMapper.init — largely loses its purpose for the shipped
mappers, since it mostly exists to make the general decode/encode pair safe. Worth reassessing
whether it belongs now or arrives with the first real custom mapper.

Question (non-blocking): the audit-log path (_record_partition_audit_log /
_record_stale_apdr_audit_log) opens independent scoped=False sessions to survive an outer
rollback, plus a process-lifetime dedup set. For advisory Log rows where self.log.exception(...)
already records the same thing, is the rollback-survival guarantee worth the machinery for v1, or
would a plain log line do?

@Lee-W

Copy link
Copy Markdown
MemberAuthor

Hey @dstandish , thanks for reviewing. a few responses to the questions

Could we go with the dedicated regexes for now and add generality if/when a custom mapper actually needs it?

The regex compiler was added per @uranusjr's review: without it, a custom output_format forces the user to also override decode_downstream, which TP found surprising. Hardcoding _WEEK_RE / _QUARTER_RE saves the ~120 lines but is not that users friendly

is the rollback-survival guarantee worth the machinery for v1, or would a plain log line do?

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst

@uranusjruranusjr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find it somewhat awkward the UI API endpoints need to load the serialized dag to calculate data. But I guess it’s not too useful to overthink this unless there are known performance issues.

Should we add some links to Starlette for the format-parsing logic?

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/config_templates/config.yml Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
@dstandish

Copy link
Copy Markdown
Contributor

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

I don't really have a strong feeling about that. I tried to look for it in the code but could not find it.

And I don't stand in the way of merging this thing....

But... I can't help feeling that this PR is bigger than it should be. It's feels so big that it's kind of overwhelming to properly review. It spreads the reviewer's attention over a very wide area.

@Lee-W

Lee-W commented Jun 4, 2026

Copy link
Copy Markdown
MemberAuthor

Should we add some links to Starlette for the format-parsing logic?

yep, added a pointer to Starlette's compile_path in the _compile_output_format_regex docstring.

Lee-W added 9 commits June 4, 2026 23:26
- Introduces `RollupMapper(upstream_mapper=..., window=...)` so a partitioned
Dag run waits until the full set of upstream partitions for one downstream
period has arrived (e.g. 24 hourly events for a daily rollup).
- Ships 6 temporal `Window` built-ins (Hour / Day / Week / Month / Quarter /
Year); custom windows are rejected at serialization time.
- Surfaces frozen / mapper-error / partial-rollup state on the next-run-assets
UI; `pending_partition_count` and rollup-aware totals stay symmetric across
list/detail routes.
See `airflow-core/newsfragments/64571.significant.rst` for the full breakdown,
including the documented DST edge case for `DayWindow` + local-tz mappers.
…uler logs
The misconfigured-mapper and stale-APDR-cleanup paths persisted audit Log
rows on independent scoped=False sessions plus a per-process dedup set so
the rows survived an outer rollback. For advisory records this is heavier
than the problem warrants in v1 — the misconfig path already logs the
exception every tick, and stale cleanup now emits a structured info log.
Re-add the UI-visible audit rows if operators report needing them.
Add an ``is_rollup`` TypeGuard helper next to ``RollupMapper`` (mirroring
``is_mapped``) and use it in the partitioned-asset readiness check so the
mapper narrows to ``RollupMapper`` without a ``cast``.
…ion changes
Stale-cleanup previously dropped a pending AssetPartitionDagRun whenever the
Dag's serialized version changed, so any unrelated structural edit discarded
in-flight partition accumulation and could leave a rollup held forever.
Stamp a rollup-definition fingerprint (the serialized partition mappers of the
Dag's partitioned assets) on the APDR at creation and compare that against the
latest definition instead of the Dag version, so only a genuine mapper/window
change clears the run. Replaces the unreleased ``dag_version_id`` column on
AssetPartitionDagRun with ``rollup_fingerprint`` (migration 0119 amended).
The per-tick cap on pending AssetPartitionDagRun rows was exposed as the
``[scheduler] max_partition_dag_runs_to_create_per_loop`` setting, but it is a
performance safety bound (keeping the per-tick transaction from starving
executor heartbeats and regular scheduling), not a knob operators can
meaningfully tune. Drop the unreleased setting and keep the bound as a
module-level constant. The per-loop query LIMIT is retained.
Point the ``{name}`` placeholder handling at the Starlette routing prior art
it mirrors, so the parsing approach is discoverable.
The docs spellchecker rejects the coined abbreviation; spell it out as a
partition Dag run and drop the double spaces.
Spell out why a temporal upstream mapper is needed (it normalizes each
upstream key to the window's granularity), clarify that an identity mapper
paired with a temporal window fails as a type mismatch at construction, and
cross-reference RollupMapper / Window classes to the API docs.
…rint
The fingerprint tests built a core PartitionedAssetTimetable from Task SDK
Asset / RollupMapper objects, which tripped mypy arg-type checks. Construct it
with serialized assets and the core mapper/window types (matching the existing
test convention) imported at module top level; the serialized fingerprint is
unchanged.
@potiuk

Copy link
Copy Markdown
Member

@uranusjr's approval here is from Jun 2 (aa9f248), but there have been ~9 commits since — scheduler refactors, the partition-Dag-run clearing fix, mapper narrowing, etc. Worth a re-review so the green check reflects the current state before this merges. @uranusjr, could you re-confirm when you get a chance?


Drafted-by: Claude Code (Opus 4.8); reviewed by @potiuk before posting

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Schedulerincluding HA (high availability) schedulerarea:task-sdk

Projects

No open projects

Development

Successfully merging this pull request may close these issues.

Implement rollup (many-to-one partition mapper)

10 participants

@Lee-W@eladkal@dstandish@potiuk@ashb@uranusjr@kaxil@phanikumv@vatsrahul1001
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' AIP-76: Hold Dag run until all upstream partitions arrive by Lee-W · Pull Request #64571 · apache/airflow · GitHub
Skip to content

AIP-76: Hold Dag run until all upstream partitions arrive - #64571

Merged
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window
Jun 5, 2026
Merged

AIP-76: Hold Dag run until all upstream partitions arrive#64571
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window

Conversation

@Lee-W

@Lee-WLee-W commented Apr 1, 2026

Copy link
Copy Markdown
Member

Why

Closes: #59294

Why

Asset-partitioned Dags that aggregate many upstream slices into one downstream period (e.g., 60-minute-level events rolling up into one hourly Dag run) had no way to express that requirement — the scheduler would fire the downstream run as soon as any single upstream partition arrived.

This PR implements the rollup building block from AIP-76: a Window type that enumerates the full set of upstream partitions required for a downstream period, a RollupMapper that wires a source mapper to a window, and the scheduler logic to gate Dag runs until every required upstream key is present.

What

Partition mappers/windows

  • Add Window ABC and six concrete implementations (HourWindow, DayWindow, WeekWindow, MonthWindow, QuarterWindow, YearWindow) to both airflow-core and the Task SDK
  • Add RollupMapper that composes a source_mapper with a Window and exposes to_upstream(downstream_key) → frozenset[str]
  • Add decode_downstream / encode_upstream hooks to PartitionMapper and implement them in _BaseTemporalMapper; StartOfWeekMapper gets a regex-based override because %V is ambiguous with strptime.
  • Add week_start parameter to StartOfWeekMapper for non-Monday week starts

Scheduler

  • Rewrite _create_dagruns_for_partitioned_asset_dags to bulk-fetch serialized Dags and partition-key logs, removing N+1 queries, and cap per-tick work at MAX_PARTITION_DAG_RUNS_PER_TICK
  • Add _resolve_asset_partition_status / _check_rollup_asset_status to evaluate rollup satisfaction; non-rollup assets continue to satisfy immediately

Serialization

  • Add encode_window / decode_window and extend mapper encoder/decoder to round-trip RollupMapper and all Window subclasses

UI / API

  • Enrich next_run_assets endpoint with per-asset received_count, required_count, received_keys, required_keys, and is_rollup for partitioned Dags
  • Update AssetNode and AssetSchedule components to surface rollup progress (e.g. "12 / 24 received")
  • Add AssetProgressCell for inline progress in the Dags list

Was generative AI tooling used to co-author this PR?
  • Yes — Claude Sonnet 4.6

Generated-by: Claude Sonnet 4.6 following the guidelines

withDAG(
dag_id="daily_team_a_rollup",
schedule=PartitionedAssetTimetable(
assets=team_a_player_stats,
default_partition_mapper=RollupMapper(
source_mapper=StartOfDayMapper(),
window=DayWindow(),
),
),
catchup=False,
tags=["player-stats", "rollup"],
):
""" First rollup level: 24 hourly partitions of ``team_a_player_stats`` → one daily summary. ``StartOfDayMapper`` normalizes each upstream hourly timestamp (``%Y-%m-%dT%H:%M:%S``) to its day-start (``%Y-%m-%d``); ``DayWindow`` declares the downstream run needs all 24 hourly partitions before firing. Publishes ``daily_team_a`` so the monthly rollup below can consume it. """@task(outlets=[daily_team_a])defsummarise_team_a_day(dag_run=None):
"""Produce the full-day rollup once every hour has arrived."""ifTYPE_CHECKING:
assertdag_runprint(f"All 24 hourly partitions received. Day: {dag_run.partition_key}")
summarise_team_a_day()
imageimage
  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@boring-cyborgboring-cyborgBot added area:Scheduler including HA (high availability) scheduler area:task-sdk labels Apr 1, 2026
@Lee-WLee-W changed the title feat(AIP-76): windowfeat(AIP-76): implement to_upstreamApr 1, 2026
@kaxil
kaxil requested a review from CopilotApril 2, 2026 00:41

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements “rollup” support for partition mappers (AIP-76) by introducing a RollupMapper interface with to_upstream() and using it in the scheduler to wait for a complete set of upstream partition keys before creating partitioned asset-triggered DAG runs.

Changes:

  • Add RollupMapper base class (core + task SDK) with an abstract to_upstream() contract.
  • Implement to_upstream() for weekly and monthly temporal mappers (core + task SDK).
  • Update the scheduler’s partitioned-asset DAG-run creation logic to enforce rollup completeness when a mapper supports it.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
FileDescription
task-sdk/src/airflow/sdk/definitions/partition_mappers/base.pyIntroduces SDK-side RollupMapper abstraction.
task-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.pyAdds SDK to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/timetables/base.pyAdds get_partition_mapper() hook to the Timetable protocol.
airflow-core/src/airflow/partition_mappers/base.pyIntroduces core-side RollupMapper abstraction.
airflow-core/src/airflow/partition_mappers/temporal.pyAdds core to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/jobs/scheduler_job_runner.pyUses rollup mapper behavior to decide when partitioned asset-triggered DAG runs are ready.

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 2 times, most recently from e72dfa6 to e6d53f2CompareApril 7, 2026 09:57
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor
from __future__ importannotationsfromairflow.sdkimport (
DAG,
Asset,
CronPartitionTimetable,
PartitionedAssetTimetable,
WeeklyRollupMapper,
task,
)
daily_sales=Asset(uri="file://incoming/sales/daily.csv", name="daily_sales")
# Upstream Dag: produces one partition per day (key format: "2024-01-15T00:00:00")withDAG(
dag_id="ingest_daily_sales",
schedule=CronPartitionTimetable("0 0 * * *", timezone="UTC"),
):
@task(outlets=[daily_sales])defingest():
passingest()
# Downstream Dag: runs once all 7 daily partitions for a week have arrivedwithDAG(
dag_id="weekly_sales_report",
schedule=PartitionedAssetTimetable(
assets=daily_sales,
default_partition_mapper=WeeklyRollupMapper(),
),
catchup=False,
):
@taskdefgenerate_report(dag_run=None):
# dag_run.partition_key will be the week key, e.g. "2024-01-15 (W03)"print(dag_run.partition_key)
generate_report()

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 91c4ac3 to 93e82cbCompareApril 7, 2026 11:48
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor

The backend part is basically wrapped up, but the frontend and API side need some work. The UI is quite weired for these cases now

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 9934a73 to f52823cCompareApril 10, 2026 09:09
@kaxil
kaxil requested a review from CopilotApril 10, 2026 19:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (1)

airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py:1

  • Counting PartitionedAssetKeyLog.id can over-count when duplicate log rows exist for the same upstream partition key (e.g. retries/dup inserts), inflating total_received and potentially showing the run as satisfiable earlier than it should be. Consider counting distinct PartitionedAssetKeyLog.source_partition_key (and/or a distinct composite of (asset_id, source_partition_key)) to match the scheduler’s set-based satisfaction semantics.
# Licensed to the Apache Software Foundation (ASF) under one

Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/base.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/ui/src/components/AssetProgressCell.tsx Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 9 times, most recently from a64d06a to 9064515CompareApril 17, 2026 12:12
@Lee-W
Lee-W marked this pull request as ready for review April 20, 2026 06:32
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/common/partition_helpers.py Outdated
Comment threadairflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst
Comment threadairflow-core/src/airflow/partition_mappers/window.py
@dstandish

Copy link
Copy Markdown
Contributor

Hey @Lee-W

I was a little surprised to see so many lines for this change so I asked Claude to help me review whether there's any unnecessary complexity. Below are its findings. They seem plausible. What do you think?


The core design is clean (the Window types are tiny generators,
RollupMapper.to_upstream is a clear decode→expand→encode, and the scheduler gate comes down to one
expected.issubset(actual) check), and the test coverage on the scheduler paths is excellent. The
HA hardening in _create_dagruns_for_partitioned_asset_dags (bulk-fetch over the old N+1,
with_row_locks(skip_locked=True), the per-tick cap and deterministic order_by) all looks right.

One theme I'd like to resolve before merge: a meaningful chunk of complexity is self-induced,
concentrated in partition_mappers/temporal.py.

_compile_output_format_regex looks like more generality than the feature needs. Only two mappers
can't round-trip through strptime: StartOfWeekMapper (%V isn't strptime-parseable) and
StartOfQuarterMapper ({quarter} isn't a directive). The actual requirement is "recover Y/m/d" and
"recover Y/quarter". But the decode that motivated the whole compiler ends up discarding the token
it was built for:

StartOfWeekMapper.decode_downstream — %V is captured but never read

return datetime(int(match["Y"]), int(match["m"]), int(match["d"]))

Both decodes collapse to a dedicated per-mapper regex:

_WEEK_RE = re.compile(r"(?P\d{4})-(?P\d{2})-(?P\d{2})")
_QUARTER_RE = re.compile(r"(?P\d{4})-Q(?P[1-4])")

That drops the directive table, the {name} placeholder machinery, the placeholder_patterns arg,
and the five compile-time ValueError branches — plus the test class that only exists to exercise
that invented surface (test_rejects_adjacent_default_pattern_placeholders,
test_adjacent_placeholders_allowed_when_one_is_narrowed,
test_separator_between_default_placeholders_is_allowed). Roughly ~120 lines + tests, with
identical behavior for every shipped mapper. Could we go with the dedicated regexes for now and
add generality if/when a custom mapper actually needs it?

Follow-on: if the temporal decode simplifies, the base-class guard scaffolding in
partition_mappers/base.py — the init_subclass XOR check, expected_decoded_type, and the
runtime pairing check in RollupMapper.init — largely loses its purpose for the shipped
mappers, since it mostly exists to make the general decode/encode pair safe. Worth reassessing
whether it belongs now or arrives with the first real custom mapper.

Question (non-blocking): the audit-log path (_record_partition_audit_log /
_record_stale_apdr_audit_log) opens independent scoped=False sessions to survive an outer
rollback, plus a process-lifetime dedup set. For advisory Log rows where self.log.exception(...)
already records the same thing, is the rollback-survival guarantee worth the machinery for v1, or
would a plain log line do?

@Lee-W

Copy link
Copy Markdown
MemberAuthor

Hey @dstandish , thanks for reviewing. a few responses to the questions

Could we go with the dedicated regexes for now and add generality if/when a custom mapper actually needs it?

The regex compiler was added per @uranusjr's review: without it, a custom output_format forces the user to also override decode_downstream, which TP found surprising. Hardcoding _WEEK_RE / _QUARTER_RE saves the ~120 lines but is not that users friendly

is the rollback-survival guarantee worth the machinery for v1, or would a plain log line do?

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst

@uranusjruranusjr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find it somewhat awkward the UI API endpoints need to load the serialized dag to calculate data. But I guess it’s not too useful to overthink this unless there are known performance issues.

Should we add some links to Starlette for the format-parsing logic?

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/config_templates/config.yml Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
@dstandish

Copy link
Copy Markdown
Contributor

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

I don't really have a strong feeling about that. I tried to look for it in the code but could not find it.

And I don't stand in the way of merging this thing....

But... I can't help feeling that this PR is bigger than it should be. It's feels so big that it's kind of overwhelming to properly review. It spreads the reviewer's attention over a very wide area.

@Lee-W

Lee-W commented Jun 4, 2026

Copy link
Copy Markdown
MemberAuthor

Should we add some links to Starlette for the format-parsing logic?

yep, added a pointer to Starlette's compile_path in the _compile_output_format_regex docstring.

Lee-W added 9 commits June 4, 2026 23:26
- Introduces `RollupMapper(upstream_mapper=..., window=...)` so a partitioned
Dag run waits until the full set of upstream partitions for one downstream
period has arrived (e.g. 24 hourly events for a daily rollup).
- Ships 6 temporal `Window` built-ins (Hour / Day / Week / Month / Quarter /
Year); custom windows are rejected at serialization time.
- Surfaces frozen / mapper-error / partial-rollup state on the next-run-assets
UI; `pending_partition_count` and rollup-aware totals stay symmetric across
list/detail routes.
See `airflow-core/newsfragments/64571.significant.rst` for the full breakdown,
including the documented DST edge case for `DayWindow` + local-tz mappers.
…uler logs
The misconfigured-mapper and stale-APDR-cleanup paths persisted audit Log
rows on independent scoped=False sessions plus a per-process dedup set so
the rows survived an outer rollback. For advisory records this is heavier
than the problem warrants in v1 — the misconfig path already logs the
exception every tick, and stale cleanup now emits a structured info log.
Re-add the UI-visible audit rows if operators report needing them.
Add an ``is_rollup`` TypeGuard helper next to ``RollupMapper`` (mirroring
``is_mapped``) and use it in the partitioned-asset readiness check so the
mapper narrows to ``RollupMapper`` without a ``cast``.
…ion changes
Stale-cleanup previously dropped a pending AssetPartitionDagRun whenever the
Dag's serialized version changed, so any unrelated structural edit discarded
in-flight partition accumulation and could leave a rollup held forever.
Stamp a rollup-definition fingerprint (the serialized partition mappers of the
Dag's partitioned assets) on the APDR at creation and compare that against the
latest definition instead of the Dag version, so only a genuine mapper/window
change clears the run. Replaces the unreleased ``dag_version_id`` column on
AssetPartitionDagRun with ``rollup_fingerprint`` (migration 0119 amended).
The per-tick cap on pending AssetPartitionDagRun rows was exposed as the
``[scheduler] max_partition_dag_runs_to_create_per_loop`` setting, but it is a
performance safety bound (keeping the per-tick transaction from starving
executor heartbeats and regular scheduling), not a knob operators can
meaningfully tune. Drop the unreleased setting and keep the bound as a
module-level constant. The per-loop query LIMIT is retained.
Point the ``{name}`` placeholder handling at the Starlette routing prior art
it mirrors, so the parsing approach is discoverable.
The docs spellchecker rejects the coined abbreviation; spell it out as a
partition Dag run and drop the double spaces.
Spell out why a temporal upstream mapper is needed (it normalizes each
upstream key to the window's granularity), clarify that an identity mapper
paired with a temporal window fails as a type mismatch at construction, and
cross-reference RollupMapper / Window classes to the API docs.
…rint
The fingerprint tests built a core PartitionedAssetTimetable from Task SDK
Asset / RollupMapper objects, which tripped mypy arg-type checks. Construct it
with serialized assets and the core mapper/window types (matching the existing
test convention) imported at module top level; the serialized fingerprint is
unchanged.
@potiuk

Copy link
Copy Markdown
Member

@uranusjr's approval here is from Jun 2 (aa9f248), but there have been ~9 commits since — scheduler refactors, the partition-Dag-run clearing fix, mapper narrowing, etc. Worth a re-review so the green check reflects the current state before this merges. @uranusjr, could you re-confirm when you get a chance?


Drafted-by: Claude Code (Opus 4.8); reviewed by @potiuk before posting

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Schedulerincluding HA (high availability) schedulerarea:task-sdk

Projects

No open projects

Development

Successfully merging this pull request may close these issues.

Implement rollup (many-to-one partition mapper)

10 participants

@Lee-W@eladkal@dstandish@potiuk@ashb@uranusjr@kaxil@phanikumv@vatsrahul1001
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' AIP-76: Hold Dag run until all upstream partitions arrive by Lee-W · Pull Request #64571 · apache/airflow · GitHub
Skip to content

AIP-76: Hold Dag run until all upstream partitions arrive - #64571

Merged
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window
Jun 5, 2026
Merged

AIP-76: Hold Dag run until all upstream partitions arrive#64571
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window

Conversation

@Lee-W

@Lee-WLee-W commented Apr 1, 2026

Copy link
Copy Markdown
Member

Why

Closes: #59294

Why

Asset-partitioned Dags that aggregate many upstream slices into one downstream period (e.g., 60-minute-level events rolling up into one hourly Dag run) had no way to express that requirement — the scheduler would fire the downstream run as soon as any single upstream partition arrived.

This PR implements the rollup building block from AIP-76: a Window type that enumerates the full set of upstream partitions required for a downstream period, a RollupMapper that wires a source mapper to a window, and the scheduler logic to gate Dag runs until every required upstream key is present.

What

Partition mappers/windows

  • Add Window ABC and six concrete implementations (HourWindow, DayWindow, WeekWindow, MonthWindow, QuarterWindow, YearWindow) to both airflow-core and the Task SDK
  • Add RollupMapper that composes a source_mapper with a Window and exposes to_upstream(downstream_key) → frozenset[str]
  • Add decode_downstream / encode_upstream hooks to PartitionMapper and implement them in _BaseTemporalMapper; StartOfWeekMapper gets a regex-based override because %V is ambiguous with strptime.
  • Add week_start parameter to StartOfWeekMapper for non-Monday week starts

Scheduler

  • Rewrite _create_dagruns_for_partitioned_asset_dags to bulk-fetch serialized Dags and partition-key logs, removing N+1 queries, and cap per-tick work at MAX_PARTITION_DAG_RUNS_PER_TICK
  • Add _resolve_asset_partition_status / _check_rollup_asset_status to evaluate rollup satisfaction; non-rollup assets continue to satisfy immediately

Serialization

  • Add encode_window / decode_window and extend mapper encoder/decoder to round-trip RollupMapper and all Window subclasses

UI / API

  • Enrich next_run_assets endpoint with per-asset received_count, required_count, received_keys, required_keys, and is_rollup for partitioned Dags
  • Update AssetNode and AssetSchedule components to surface rollup progress (e.g. "12 / 24 received")
  • Add AssetProgressCell for inline progress in the Dags list

Was generative AI tooling used to co-author this PR?
  • Yes — Claude Sonnet 4.6

Generated-by: Claude Sonnet 4.6 following the guidelines

withDAG(
dag_id="daily_team_a_rollup",
schedule=PartitionedAssetTimetable(
assets=team_a_player_stats,
default_partition_mapper=RollupMapper(
source_mapper=StartOfDayMapper(),
window=DayWindow(),
),
),
catchup=False,
tags=["player-stats", "rollup"],
):
""" First rollup level: 24 hourly partitions of ``team_a_player_stats`` → one daily summary. ``StartOfDayMapper`` normalizes each upstream hourly timestamp (``%Y-%m-%dT%H:%M:%S``) to its day-start (``%Y-%m-%d``); ``DayWindow`` declares the downstream run needs all 24 hourly partitions before firing. Publishes ``daily_team_a`` so the monthly rollup below can consume it. """@task(outlets=[daily_team_a])defsummarise_team_a_day(dag_run=None):
"""Produce the full-day rollup once every hour has arrived."""ifTYPE_CHECKING:
assertdag_runprint(f"All 24 hourly partitions received. Day: {dag_run.partition_key}")
summarise_team_a_day()
imageimage
  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@boring-cyborgboring-cyborgBot added area:Scheduler including HA (high availability) scheduler area:task-sdk labels Apr 1, 2026
@Lee-WLee-W changed the title feat(AIP-76): windowfeat(AIP-76): implement to_upstreamApr 1, 2026
@kaxil
kaxil requested a review from CopilotApril 2, 2026 00:41

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements “rollup” support for partition mappers (AIP-76) by introducing a RollupMapper interface with to_upstream() and using it in the scheduler to wait for a complete set of upstream partition keys before creating partitioned asset-triggered DAG runs.

Changes:

  • Add RollupMapper base class (core + task SDK) with an abstract to_upstream() contract.
  • Implement to_upstream() for weekly and monthly temporal mappers (core + task SDK).
  • Update the scheduler’s partitioned-asset DAG-run creation logic to enforce rollup completeness when a mapper supports it.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
FileDescription
task-sdk/src/airflow/sdk/definitions/partition_mappers/base.pyIntroduces SDK-side RollupMapper abstraction.
task-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.pyAdds SDK to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/timetables/base.pyAdds get_partition_mapper() hook to the Timetable protocol.
airflow-core/src/airflow/partition_mappers/base.pyIntroduces core-side RollupMapper abstraction.
airflow-core/src/airflow/partition_mappers/temporal.pyAdds core to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/jobs/scheduler_job_runner.pyUses rollup mapper behavior to decide when partitioned asset-triggered DAG runs are ready.

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 2 times, most recently from e72dfa6 to e6d53f2CompareApril 7, 2026 09:57
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor
from __future__ importannotationsfromairflow.sdkimport (
DAG,
Asset,
CronPartitionTimetable,
PartitionedAssetTimetable,
WeeklyRollupMapper,
task,
)
daily_sales=Asset(uri="file://incoming/sales/daily.csv", name="daily_sales")
# Upstream Dag: produces one partition per day (key format: "2024-01-15T00:00:00")withDAG(
dag_id="ingest_daily_sales",
schedule=CronPartitionTimetable("0 0 * * *", timezone="UTC"),
):
@task(outlets=[daily_sales])defingest():
passingest()
# Downstream Dag: runs once all 7 daily partitions for a week have arrivedwithDAG(
dag_id="weekly_sales_report",
schedule=PartitionedAssetTimetable(
assets=daily_sales,
default_partition_mapper=WeeklyRollupMapper(),
),
catchup=False,
):
@taskdefgenerate_report(dag_run=None):
# dag_run.partition_key will be the week key, e.g. "2024-01-15 (W03)"print(dag_run.partition_key)
generate_report()

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 91c4ac3 to 93e82cbCompareApril 7, 2026 11:48
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor

The backend part is basically wrapped up, but the frontend and API side need some work. The UI is quite weired for these cases now

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 9934a73 to f52823cCompareApril 10, 2026 09:09
@kaxil
kaxil requested a review from CopilotApril 10, 2026 19:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (1)

airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py:1

  • Counting PartitionedAssetKeyLog.id can over-count when duplicate log rows exist for the same upstream partition key (e.g. retries/dup inserts), inflating total_received and potentially showing the run as satisfiable earlier than it should be. Consider counting distinct PartitionedAssetKeyLog.source_partition_key (and/or a distinct composite of (asset_id, source_partition_key)) to match the scheduler’s set-based satisfaction semantics.
# Licensed to the Apache Software Foundation (ASF) under one

Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/base.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/ui/src/components/AssetProgressCell.tsx Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 9 times, most recently from a64d06a to 9064515CompareApril 17, 2026 12:12
@Lee-W
Lee-W marked this pull request as ready for review April 20, 2026 06:32
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/common/partition_helpers.py Outdated
Comment threadairflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst
Comment threadairflow-core/src/airflow/partition_mappers/window.py
@dstandish

Copy link
Copy Markdown
Contributor

Hey @Lee-W

I was a little surprised to see so many lines for this change so I asked Claude to help me review whether there's any unnecessary complexity. Below are its findings. They seem plausible. What do you think?


The core design is clean (the Window types are tiny generators,
RollupMapper.to_upstream is a clear decode→expand→encode, and the scheduler gate comes down to one
expected.issubset(actual) check), and the test coverage on the scheduler paths is excellent. The
HA hardening in _create_dagruns_for_partitioned_asset_dags (bulk-fetch over the old N+1,
with_row_locks(skip_locked=True), the per-tick cap and deterministic order_by) all looks right.

One theme I'd like to resolve before merge: a meaningful chunk of complexity is self-induced,
concentrated in partition_mappers/temporal.py.

_compile_output_format_regex looks like more generality than the feature needs. Only two mappers
can't round-trip through strptime: StartOfWeekMapper (%V isn't strptime-parseable) and
StartOfQuarterMapper ({quarter} isn't a directive). The actual requirement is "recover Y/m/d" and
"recover Y/quarter". But the decode that motivated the whole compiler ends up discarding the token
it was built for:

StartOfWeekMapper.decode_downstream — %V is captured but never read

return datetime(int(match["Y"]), int(match["m"]), int(match["d"]))

Both decodes collapse to a dedicated per-mapper regex:

_WEEK_RE = re.compile(r"(?P\d{4})-(?P\d{2})-(?P\d{2})")
_QUARTER_RE = re.compile(r"(?P\d{4})-Q(?P[1-4])")

That drops the directive table, the {name} placeholder machinery, the placeholder_patterns arg,
and the five compile-time ValueError branches — plus the test class that only exists to exercise
that invented surface (test_rejects_adjacent_default_pattern_placeholders,
test_adjacent_placeholders_allowed_when_one_is_narrowed,
test_separator_between_default_placeholders_is_allowed). Roughly ~120 lines + tests, with
identical behavior for every shipped mapper. Could we go with the dedicated regexes for now and
add generality if/when a custom mapper actually needs it?

Follow-on: if the temporal decode simplifies, the base-class guard scaffolding in
partition_mappers/base.py — the init_subclass XOR check, expected_decoded_type, and the
runtime pairing check in RollupMapper.init — largely loses its purpose for the shipped
mappers, since it mostly exists to make the general decode/encode pair safe. Worth reassessing
whether it belongs now or arrives with the first real custom mapper.

Question (non-blocking): the audit-log path (_record_partition_audit_log /
_record_stale_apdr_audit_log) opens independent scoped=False sessions to survive an outer
rollback, plus a process-lifetime dedup set. For advisory Log rows where self.log.exception(...)
already records the same thing, is the rollback-survival guarantee worth the machinery for v1, or
would a plain log line do?

@Lee-W

Copy link
Copy Markdown
MemberAuthor

Hey @dstandish , thanks for reviewing. a few responses to the questions

Could we go with the dedicated regexes for now and add generality if/when a custom mapper actually needs it?

The regex compiler was added per @uranusjr's review: without it, a custom output_format forces the user to also override decode_downstream, which TP found surprising. Hardcoding _WEEK_RE / _QUARTER_RE saves the ~120 lines but is not that users friendly

is the rollback-survival guarantee worth the machinery for v1, or would a plain log line do?

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst

@uranusjruranusjr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find it somewhat awkward the UI API endpoints need to load the serialized dag to calculate data. But I guess it’s not too useful to overthink this unless there are known performance issues.

Should we add some links to Starlette for the format-parsing logic?

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/config_templates/config.yml Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
@dstandish

Copy link
Copy Markdown
Contributor

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

I don't really have a strong feeling about that. I tried to look for it in the code but could not find it.

And I don't stand in the way of merging this thing....

But... I can't help feeling that this PR is bigger than it should be. It's feels so big that it's kind of overwhelming to properly review. It spreads the reviewer's attention over a very wide area.

@Lee-W

Lee-W commented Jun 4, 2026

Copy link
Copy Markdown
MemberAuthor

Should we add some links to Starlette for the format-parsing logic?

yep, added a pointer to Starlette's compile_path in the _compile_output_format_regex docstring.

Lee-W added 9 commits June 4, 2026 23:26
- Introduces `RollupMapper(upstream_mapper=..., window=...)` so a partitioned
Dag run waits until the full set of upstream partitions for one downstream
period has arrived (e.g. 24 hourly events for a daily rollup).
- Ships 6 temporal `Window` built-ins (Hour / Day / Week / Month / Quarter /
Year); custom windows are rejected at serialization time.
- Surfaces frozen / mapper-error / partial-rollup state on the next-run-assets
UI; `pending_partition_count` and rollup-aware totals stay symmetric across
list/detail routes.
See `airflow-core/newsfragments/64571.significant.rst` for the full breakdown,
including the documented DST edge case for `DayWindow` + local-tz mappers.
…uler logs
The misconfigured-mapper and stale-APDR-cleanup paths persisted audit Log
rows on independent scoped=False sessions plus a per-process dedup set so
the rows survived an outer rollback. For advisory records this is heavier
than the problem warrants in v1 — the misconfig path already logs the
exception every tick, and stale cleanup now emits a structured info log.
Re-add the UI-visible audit rows if operators report needing them.
Add an ``is_rollup`` TypeGuard helper next to ``RollupMapper`` (mirroring
``is_mapped``) and use it in the partitioned-asset readiness check so the
mapper narrows to ``RollupMapper`` without a ``cast``.
…ion changes
Stale-cleanup previously dropped a pending AssetPartitionDagRun whenever the
Dag's serialized version changed, so any unrelated structural edit discarded
in-flight partition accumulation and could leave a rollup held forever.
Stamp a rollup-definition fingerprint (the serialized partition mappers of the
Dag's partitioned assets) on the APDR at creation and compare that against the
latest definition instead of the Dag version, so only a genuine mapper/window
change clears the run. Replaces the unreleased ``dag_version_id`` column on
AssetPartitionDagRun with ``rollup_fingerprint`` (migration 0119 amended).
The per-tick cap on pending AssetPartitionDagRun rows was exposed as the
``[scheduler] max_partition_dag_runs_to_create_per_loop`` setting, but it is a
performance safety bound (keeping the per-tick transaction from starving
executor heartbeats and regular scheduling), not a knob operators can
meaningfully tune. Drop the unreleased setting and keep the bound as a
module-level constant. The per-loop query LIMIT is retained.
Point the ``{name}`` placeholder handling at the Starlette routing prior art
it mirrors, so the parsing approach is discoverable.
The docs spellchecker rejects the coined abbreviation; spell it out as a
partition Dag run and drop the double spaces.
Spell out why a temporal upstream mapper is needed (it normalizes each
upstream key to the window's granularity), clarify that an identity mapper
paired with a temporal window fails as a type mismatch at construction, and
cross-reference RollupMapper / Window classes to the API docs.
…rint
The fingerprint tests built a core PartitionedAssetTimetable from Task SDK
Asset / RollupMapper objects, which tripped mypy arg-type checks. Construct it
with serialized assets and the core mapper/window types (matching the existing
test convention) imported at module top level; the serialized fingerprint is
unchanged.
@potiuk

Copy link
Copy Markdown
Member

@uranusjr's approval here is from Jun 2 (aa9f248), but there have been ~9 commits since — scheduler refactors, the partition-Dag-run clearing fix, mapper narrowing, etc. Worth a re-review so the green check reflects the current state before this merges. @uranusjr, could you re-confirm when you get a chance?


Drafted-by: Claude Code (Opus 4.8); reviewed by @potiuk before posting

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Schedulerincluding HA (high availability) schedulerarea:task-sdk

Projects

No open projects

Development

Successfully merging this pull request may close these issues.

Implement rollup (many-to-one partition mapper)

10 participants

@Lee-W@eladkal@dstandish@potiuk@ashb@uranusjr@kaxil@phanikumv@vatsrahul1001
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' AIP-76: Hold Dag run until all upstream partitions arrive by Lee-W · Pull Request #64571 · apache/airflow · GitHub
Skip to content

AIP-76: Hold Dag run until all upstream partitions arrive - #64571

Merged
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window
Jun 5, 2026
Merged

AIP-76: Hold Dag run until all upstream partitions arrive#64571
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window

Conversation

@Lee-W

@Lee-WLee-W commented Apr 1, 2026

Copy link
Copy Markdown
Member

Why

Closes: #59294

Why

Asset-partitioned Dags that aggregate many upstream slices into one downstream period (e.g., 60-minute-level events rolling up into one hourly Dag run) had no way to express that requirement — the scheduler would fire the downstream run as soon as any single upstream partition arrived.

This PR implements the rollup building block from AIP-76: a Window type that enumerates the full set of upstream partitions required for a downstream period, a RollupMapper that wires a source mapper to a window, and the scheduler logic to gate Dag runs until every required upstream key is present.

What

Partition mappers/windows

  • Add Window ABC and six concrete implementations (HourWindow, DayWindow, WeekWindow, MonthWindow, QuarterWindow, YearWindow) to both airflow-core and the Task SDK
  • Add RollupMapper that composes a source_mapper with a Window and exposes to_upstream(downstream_key) → frozenset[str]
  • Add decode_downstream / encode_upstream hooks to PartitionMapper and implement them in _BaseTemporalMapper; StartOfWeekMapper gets a regex-based override because %V is ambiguous with strptime.
  • Add week_start parameter to StartOfWeekMapper for non-Monday week starts

Scheduler

  • Rewrite _create_dagruns_for_partitioned_asset_dags to bulk-fetch serialized Dags and partition-key logs, removing N+1 queries, and cap per-tick work at MAX_PARTITION_DAG_RUNS_PER_TICK
  • Add _resolve_asset_partition_status / _check_rollup_asset_status to evaluate rollup satisfaction; non-rollup assets continue to satisfy immediately

Serialization

  • Add encode_window / decode_window and extend mapper encoder/decoder to round-trip RollupMapper and all Window subclasses

UI / API

  • Enrich next_run_assets endpoint with per-asset received_count, required_count, received_keys, required_keys, and is_rollup for partitioned Dags
  • Update AssetNode and AssetSchedule components to surface rollup progress (e.g. "12 / 24 received")
  • Add AssetProgressCell for inline progress in the Dags list

Was generative AI tooling used to co-author this PR?
  • Yes — Claude Sonnet 4.6

Generated-by: Claude Sonnet 4.6 following the guidelines

withDAG(
dag_id="daily_team_a_rollup",
schedule=PartitionedAssetTimetable(
assets=team_a_player_stats,
default_partition_mapper=RollupMapper(
source_mapper=StartOfDayMapper(),
window=DayWindow(),
),
),
catchup=False,
tags=["player-stats", "rollup"],
):
""" First rollup level: 24 hourly partitions of ``team_a_player_stats`` → one daily summary. ``StartOfDayMapper`` normalizes each upstream hourly timestamp (``%Y-%m-%dT%H:%M:%S``) to its day-start (``%Y-%m-%d``); ``DayWindow`` declares the downstream run needs all 24 hourly partitions before firing. Publishes ``daily_team_a`` so the monthly rollup below can consume it. """@task(outlets=[daily_team_a])defsummarise_team_a_day(dag_run=None):
"""Produce the full-day rollup once every hour has arrived."""ifTYPE_CHECKING:
assertdag_runprint(f"All 24 hourly partitions received. Day: {dag_run.partition_key}")
summarise_team_a_day()
imageimage
  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@boring-cyborgboring-cyborgBot added area:Scheduler including HA (high availability) scheduler area:task-sdk labels Apr 1, 2026
@Lee-WLee-W changed the title feat(AIP-76): windowfeat(AIP-76): implement to_upstreamApr 1, 2026
@kaxil
kaxil requested a review from CopilotApril 2, 2026 00:41

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements “rollup” support for partition mappers (AIP-76) by introducing a RollupMapper interface with to_upstream() and using it in the scheduler to wait for a complete set of upstream partition keys before creating partitioned asset-triggered DAG runs.

Changes:

  • Add RollupMapper base class (core + task SDK) with an abstract to_upstream() contract.
  • Implement to_upstream() for weekly and monthly temporal mappers (core + task SDK).
  • Update the scheduler’s partitioned-asset DAG-run creation logic to enforce rollup completeness when a mapper supports it.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
FileDescription
task-sdk/src/airflow/sdk/definitions/partition_mappers/base.pyIntroduces SDK-side RollupMapper abstraction.
task-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.pyAdds SDK to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/timetables/base.pyAdds get_partition_mapper() hook to the Timetable protocol.
airflow-core/src/airflow/partition_mappers/base.pyIntroduces core-side RollupMapper abstraction.
airflow-core/src/airflow/partition_mappers/temporal.pyAdds core to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/jobs/scheduler_job_runner.pyUses rollup mapper behavior to decide when partitioned asset-triggered DAG runs are ready.

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 2 times, most recently from e72dfa6 to e6d53f2CompareApril 7, 2026 09:57
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor
from __future__ importannotationsfromairflow.sdkimport (
DAG,
Asset,
CronPartitionTimetable,
PartitionedAssetTimetable,
WeeklyRollupMapper,
task,
)
daily_sales=Asset(uri="file://incoming/sales/daily.csv", name="daily_sales")
# Upstream Dag: produces one partition per day (key format: "2024-01-15T00:00:00")withDAG(
dag_id="ingest_daily_sales",
schedule=CronPartitionTimetable("0 0 * * *", timezone="UTC"),
):
@task(outlets=[daily_sales])defingest():
passingest()
# Downstream Dag: runs once all 7 daily partitions for a week have arrivedwithDAG(
dag_id="weekly_sales_report",
schedule=PartitionedAssetTimetable(
assets=daily_sales,
default_partition_mapper=WeeklyRollupMapper(),
),
catchup=False,
):
@taskdefgenerate_report(dag_run=None):
# dag_run.partition_key will be the week key, e.g. "2024-01-15 (W03)"print(dag_run.partition_key)
generate_report()

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 91c4ac3 to 93e82cbCompareApril 7, 2026 11:48
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor

The backend part is basically wrapped up, but the frontend and API side need some work. The UI is quite weired for these cases now

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 9934a73 to f52823cCompareApril 10, 2026 09:09
@kaxil
kaxil requested a review from CopilotApril 10, 2026 19:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (1)

airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py:1

  • Counting PartitionedAssetKeyLog.id can over-count when duplicate log rows exist for the same upstream partition key (e.g. retries/dup inserts), inflating total_received and potentially showing the run as satisfiable earlier than it should be. Consider counting distinct PartitionedAssetKeyLog.source_partition_key (and/or a distinct composite of (asset_id, source_partition_key)) to match the scheduler’s set-based satisfaction semantics.
# Licensed to the Apache Software Foundation (ASF) under one

Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/base.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/ui/src/components/AssetProgressCell.tsx Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 9 times, most recently from a64d06a to 9064515CompareApril 17, 2026 12:12
@Lee-W
Lee-W marked this pull request as ready for review April 20, 2026 06:32
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/common/partition_helpers.py Outdated
Comment threadairflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst
Comment threadairflow-core/src/airflow/partition_mappers/window.py
@dstandish

Copy link
Copy Markdown
Contributor

Hey @Lee-W

I was a little surprised to see so many lines for this change so I asked Claude to help me review whether there's any unnecessary complexity. Below are its findings. They seem plausible. What do you think?


The core design is clean (the Window types are tiny generators,
RollupMapper.to_upstream is a clear decode→expand→encode, and the scheduler gate comes down to one
expected.issubset(actual) check), and the test coverage on the scheduler paths is excellent. The
HA hardening in _create_dagruns_for_partitioned_asset_dags (bulk-fetch over the old N+1,
with_row_locks(skip_locked=True), the per-tick cap and deterministic order_by) all looks right.

One theme I'd like to resolve before merge: a meaningful chunk of complexity is self-induced,
concentrated in partition_mappers/temporal.py.

_compile_output_format_regex looks like more generality than the feature needs. Only two mappers
can't round-trip through strptime: StartOfWeekMapper (%V isn't strptime-parseable) and
StartOfQuarterMapper ({quarter} isn't a directive). The actual requirement is "recover Y/m/d" and
"recover Y/quarter". But the decode that motivated the whole compiler ends up discarding the token
it was built for:

StartOfWeekMapper.decode_downstream — %V is captured but never read

return datetime(int(match["Y"]), int(match["m"]), int(match["d"]))

Both decodes collapse to a dedicated per-mapper regex:

_WEEK_RE = re.compile(r"(?P\d{4})-(?P\d{2})-(?P\d{2})")
_QUARTER_RE = re.compile(r"(?P\d{4})-Q(?P[1-4])")

That drops the directive table, the {name} placeholder machinery, the placeholder_patterns arg,
and the five compile-time ValueError branches — plus the test class that only exists to exercise
that invented surface (test_rejects_adjacent_default_pattern_placeholders,
test_adjacent_placeholders_allowed_when_one_is_narrowed,
test_separator_between_default_placeholders_is_allowed). Roughly ~120 lines + tests, with
identical behavior for every shipped mapper. Could we go with the dedicated regexes for now and
add generality if/when a custom mapper actually needs it?

Follow-on: if the temporal decode simplifies, the base-class guard scaffolding in
partition_mappers/base.py — the init_subclass XOR check, expected_decoded_type, and the
runtime pairing check in RollupMapper.init — largely loses its purpose for the shipped
mappers, since it mostly exists to make the general decode/encode pair safe. Worth reassessing
whether it belongs now or arrives with the first real custom mapper.

Question (non-blocking): the audit-log path (_record_partition_audit_log /
_record_stale_apdr_audit_log) opens independent scoped=False sessions to survive an outer
rollback, plus a process-lifetime dedup set. For advisory Log rows where self.log.exception(...)
already records the same thing, is the rollback-survival guarantee worth the machinery for v1, or
would a plain log line do?

@Lee-W

Copy link
Copy Markdown
MemberAuthor

Hey @dstandish , thanks for reviewing. a few responses to the questions

Could we go with the dedicated regexes for now and add generality if/when a custom mapper actually needs it?

The regex compiler was added per @uranusjr's review: without it, a custom output_format forces the user to also override decode_downstream, which TP found surprising. Hardcoding _WEEK_RE / _QUARTER_RE saves the ~120 lines but is not that users friendly

is the rollback-survival guarantee worth the machinery for v1, or would a plain log line do?

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst

@uranusjruranusjr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find it somewhat awkward the UI API endpoints need to load the serialized dag to calculate data. But I guess it’s not too useful to overthink this unless there are known performance issues.

Should we add some links to Starlette for the format-parsing logic?

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/config_templates/config.yml Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
@dstandish

Copy link
Copy Markdown
Contributor

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

I don't really have a strong feeling about that. I tried to look for it in the code but could not find it.

And I don't stand in the way of merging this thing....

But... I can't help feeling that this PR is bigger than it should be. It's feels so big that it's kind of overwhelming to properly review. It spreads the reviewer's attention over a very wide area.

@Lee-W

Lee-W commented Jun 4, 2026

Copy link
Copy Markdown
MemberAuthor

Should we add some links to Starlette for the format-parsing logic?

yep, added a pointer to Starlette's compile_path in the _compile_output_format_regex docstring.

Lee-W added 9 commits June 4, 2026 23:26
- Introduces `RollupMapper(upstream_mapper=..., window=...)` so a partitioned
Dag run waits until the full set of upstream partitions for one downstream
period has arrived (e.g. 24 hourly events for a daily rollup).
- Ships 6 temporal `Window` built-ins (Hour / Day / Week / Month / Quarter /
Year); custom windows are rejected at serialization time.
- Surfaces frozen / mapper-error / partial-rollup state on the next-run-assets
UI; `pending_partition_count` and rollup-aware totals stay symmetric across
list/detail routes.
See `airflow-core/newsfragments/64571.significant.rst` for the full breakdown,
including the documented DST edge case for `DayWindow` + local-tz mappers.
…uler logs
The misconfigured-mapper and stale-APDR-cleanup paths persisted audit Log
rows on independent scoped=False sessions plus a per-process dedup set so
the rows survived an outer rollback. For advisory records this is heavier
than the problem warrants in v1 — the misconfig path already logs the
exception every tick, and stale cleanup now emits a structured info log.
Re-add the UI-visible audit rows if operators report needing them.
Add an ``is_rollup`` TypeGuard helper next to ``RollupMapper`` (mirroring
``is_mapped``) and use it in the partitioned-asset readiness check so the
mapper narrows to ``RollupMapper`` without a ``cast``.
…ion changes
Stale-cleanup previously dropped a pending AssetPartitionDagRun whenever the
Dag's serialized version changed, so any unrelated structural edit discarded
in-flight partition accumulation and could leave a rollup held forever.
Stamp a rollup-definition fingerprint (the serialized partition mappers of the
Dag's partitioned assets) on the APDR at creation and compare that against the
latest definition instead of the Dag version, so only a genuine mapper/window
change clears the run. Replaces the unreleased ``dag_version_id`` column on
AssetPartitionDagRun with ``rollup_fingerprint`` (migration 0119 amended).
The per-tick cap on pending AssetPartitionDagRun rows was exposed as the
``[scheduler] max_partition_dag_runs_to_create_per_loop`` setting, but it is a
performance safety bound (keeping the per-tick transaction from starving
executor heartbeats and regular scheduling), not a knob operators can
meaningfully tune. Drop the unreleased setting and keep the bound as a
module-level constant. The per-loop query LIMIT is retained.
Point the ``{name}`` placeholder handling at the Starlette routing prior art
it mirrors, so the parsing approach is discoverable.
The docs spellchecker rejects the coined abbreviation; spell it out as a
partition Dag run and drop the double spaces.
Spell out why a temporal upstream mapper is needed (it normalizes each
upstream key to the window's granularity), clarify that an identity mapper
paired with a temporal window fails as a type mismatch at construction, and
cross-reference RollupMapper / Window classes to the API docs.
…rint
The fingerprint tests built a core PartitionedAssetTimetable from Task SDK
Asset / RollupMapper objects, which tripped mypy arg-type checks. Construct it
with serialized assets and the core mapper/window types (matching the existing
test convention) imported at module top level; the serialized fingerprint is
unchanged.
@potiuk

Copy link
Copy Markdown
Member

@uranusjr's approval here is from Jun 2 (aa9f248), but there have been ~9 commits since — scheduler refactors, the partition-Dag-run clearing fix, mapper narrowing, etc. Worth a re-review so the green check reflects the current state before this merges. @uranusjr, could you re-confirm when you get a chance?


Drafted-by: Claude Code (Opus 4.8); reviewed by @potiuk before posting

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Schedulerincluding HA (high availability) schedulerarea:task-sdk

Projects

No open projects

Development

Successfully merging this pull request may close these issues.

Implement rollup (many-to-one partition mapper)

10 participants

@Lee-W@eladkal@dstandish@potiuk@ashb@uranusjr@kaxil@phanikumv@vatsrahul1001
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); AIP-76: Hold Dag run until all upstream partitions arrive by Lee-W · Pull Request #64571 · apache/airflow · GitHub
Skip to content

AIP-76: Hold Dag run until all upstream partitions arrive - #64571

Merged
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window
Jun 5, 2026
Merged

AIP-76: Hold Dag run until all upstream partitions arrive#64571
Lee-W merged 9 commits into
apache:mainfrom
astronomer:asset-partition-window

Conversation

@Lee-W

@Lee-WLee-W commented Apr 1, 2026

Copy link
Copy Markdown
Member

Why

Closes: #59294

Why

Asset-partitioned Dags that aggregate many upstream slices into one downstream period (e.g., 60-minute-level events rolling up into one hourly Dag run) had no way to express that requirement — the scheduler would fire the downstream run as soon as any single upstream partition arrived.

This PR implements the rollup building block from AIP-76: a Window type that enumerates the full set of upstream partitions required for a downstream period, a RollupMapper that wires a source mapper to a window, and the scheduler logic to gate Dag runs until every required upstream key is present.

What

Partition mappers/windows

  • Add Window ABC and six concrete implementations (HourWindow, DayWindow, WeekWindow, MonthWindow, QuarterWindow, YearWindow) to both airflow-core and the Task SDK
  • Add RollupMapper that composes a source_mapper with a Window and exposes to_upstream(downstream_key) → frozenset[str]
  • Add decode_downstream / encode_upstream hooks to PartitionMapper and implement them in _BaseTemporalMapper; StartOfWeekMapper gets a regex-based override because %V is ambiguous with strptime.
  • Add week_start parameter to StartOfWeekMapper for non-Monday week starts

Scheduler

  • Rewrite _create_dagruns_for_partitioned_asset_dags to bulk-fetch serialized Dags and partition-key logs, removing N+1 queries, and cap per-tick work at MAX_PARTITION_DAG_RUNS_PER_TICK
  • Add _resolve_asset_partition_status / _check_rollup_asset_status to evaluate rollup satisfaction; non-rollup assets continue to satisfy immediately

Serialization

  • Add encode_window / decode_window and extend mapper encoder/decoder to round-trip RollupMapper and all Window subclasses

UI / API

  • Enrich next_run_assets endpoint with per-asset received_count, required_count, received_keys, required_keys, and is_rollup for partitioned Dags
  • Update AssetNode and AssetSchedule components to surface rollup progress (e.g. "12 / 24 received")
  • Add AssetProgressCell for inline progress in the Dags list

Was generative AI tooling used to co-author this PR?
  • Yes — Claude Sonnet 4.6

Generated-by: Claude Sonnet 4.6 following the guidelines

withDAG(
dag_id="daily_team_a_rollup",
schedule=PartitionedAssetTimetable(
assets=team_a_player_stats,
default_partition_mapper=RollupMapper(
source_mapper=StartOfDayMapper(),
window=DayWindow(),
),
),
catchup=False,
tags=["player-stats", "rollup"],
):
""" First rollup level: 24 hourly partitions of ``team_a_player_stats`` → one daily summary. ``StartOfDayMapper`` normalizes each upstream hourly timestamp (``%Y-%m-%dT%H:%M:%S``) to its day-start (``%Y-%m-%d``); ``DayWindow`` declares the downstream run needs all 24 hourly partitions before firing. Publishes ``daily_team_a`` so the monthly rollup below can consume it. """@task(outlets=[daily_team_a])defsummarise_team_a_day(dag_run=None):
"""Produce the full-day rollup once every hour has arrived."""ifTYPE_CHECKING:
assertdag_runprint(f"All 24 hourly partitions received. Day: {dag_run.partition_key}")
summarise_team_a_day()
imageimage
  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

@boring-cyborgboring-cyborgBot added area:Scheduler including HA (high availability) scheduler area:task-sdk labels Apr 1, 2026
@Lee-WLee-W changed the title feat(AIP-76): windowfeat(AIP-76): implement to_upstreamApr 1, 2026
@kaxil
kaxil requested a review from CopilotApril 2, 2026 00:41

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements “rollup” support for partition mappers (AIP-76) by introducing a RollupMapper interface with to_upstream() and using it in the scheduler to wait for a complete set of upstream partition keys before creating partitioned asset-triggered DAG runs.

Changes:

  • Add RollupMapper base class (core + task SDK) with an abstract to_upstream() contract.
  • Implement to_upstream() for weekly and monthly temporal mappers (core + task SDK).
  • Update the scheduler’s partitioned-asset DAG-run creation logic to enforce rollup completeness when a mapper supports it.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 11 comments.

Show a summary per file
FileDescription
task-sdk/src/airflow/sdk/definitions/partition_mappers/base.pyIntroduces SDK-side RollupMapper abstraction.
task-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.pyAdds SDK to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/timetables/base.pyAdds get_partition_mapper() hook to the Timetable protocol.
airflow-core/src/airflow/partition_mappers/base.pyIntroduces core-side RollupMapper abstraction.
airflow-core/src/airflow/partition_mappers/temporal.pyAdds core to_upstream() for week/month temporal rollups.
airflow-core/src/airflow/jobs/scheduler_job_runner.pyUses rollup mapper behavior to decide when partitioned asset-triggered DAG runs are ready.

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 2 times, most recently from e72dfa6 to e6d53f2CompareApril 7, 2026 09:57
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor
from __future__ importannotationsfromairflow.sdkimport (
DAG,
Asset,
CronPartitionTimetable,
PartitionedAssetTimetable,
WeeklyRollupMapper,
task,
)
daily_sales=Asset(uri="file://incoming/sales/daily.csv", name="daily_sales")
# Upstream Dag: produces one partition per day (key format: "2024-01-15T00:00:00")withDAG(
dag_id="ingest_daily_sales",
schedule=CronPartitionTimetable("0 0 * * *", timezone="UTC"),
):
@task(outlets=[daily_sales])defingest():
passingest()
# Downstream Dag: runs once all 7 daily partitions for a week have arrivedwithDAG(
dag_id="weekly_sales_report",
schedule=PartitionedAssetTimetable(
assets=daily_sales,
default_partition_mapper=WeeklyRollupMapper(),
),
catchup=False,
):
@taskdefgenerate_report(dag_run=None):
# dag_run.partition_key will be the week key, e.g. "2024-01-15 (W03)"print(dag_run.partition_key)
generate_report()

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 91c4ac3 to 93e82cbCompareApril 7, 2026 11:48
@Lee-W

Lee-W commented Apr 7, 2026

Copy link
Copy Markdown
MemberAuthor

The backend part is basically wrapped up, but the frontend and API side need some work. The UI is quite weired for these cases now

@Lee-W
Lee-Wforce-pushed the asset-partition-window branch from 9934a73 to f52823cCompareApril 10, 2026 09:09
@kaxil
kaxil requested a review from CopilotApril 10, 2026 19:55

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 21 changed files in this pull request and generated 13 comments.

Comments suppressed due to low confidence (1)

airflow-core/src/airflow/api_fastapi/core_api/routes/ui/partitioned_dag_runs.py:1

  • Counting PartitionedAssetKeyLog.id can over-count when duplicate log rows exist for the same upstream partition key (e.g. retries/dup inserts), inflating total_received and potentially showing the run as satisfiable earlier than it should be. Consider counting distinct PartitionedAssetKeyLog.source_partition_key (and/or a distinct composite of (asset_id, source_partition_key)) to match the scheduler’s set-based satisfaction semantics.
# Licensed to the Apache Software Foundation (ASF) under one

Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/temporal.py Outdated
Comment threadtask-sdk/src/airflow/sdk/definitions/partition_mappers/base.py Outdated
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/partition_mappers/temporal.py
Comment threadairflow-core/src/airflow/ui/src/components/AssetProgressCell.tsx Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/tests/unit/partition_mappers/test_temporal.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py
@Lee-W
Lee-Wforce-pushed the asset-partition-window branch 9 times, most recently from a64d06a to 9064515CompareApril 17, 2026 12:12
@Lee-W
Lee-W marked this pull request as ready for review April 20, 2026 06:32
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/core_api/routes/ui/assets.py Outdated
Comment threadairflow-core/src/airflow/api_fastapi/common/partition_helpers.py Outdated
Comment threadairflow-core/src/airflow/ui/src/components/AssetExpression/AssetNode.tsx Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst
Comment threadairflow-core/src/airflow/partition_mappers/window.py
@dstandish

Copy link
Copy Markdown
Contributor

Hey @Lee-W

I was a little surprised to see so many lines for this change so I asked Claude to help me review whether there's any unnecessary complexity. Below are its findings. They seem plausible. What do you think?


The core design is clean (the Window types are tiny generators,
RollupMapper.to_upstream is a clear decode→expand→encode, and the scheduler gate comes down to one
expected.issubset(actual) check), and the test coverage on the scheduler paths is excellent. The
HA hardening in _create_dagruns_for_partitioned_asset_dags (bulk-fetch over the old N+1,
with_row_locks(skip_locked=True), the per-tick cap and deterministic order_by) all looks right.

One theme I'd like to resolve before merge: a meaningful chunk of complexity is self-induced,
concentrated in partition_mappers/temporal.py.

_compile_output_format_regex looks like more generality than the feature needs. Only two mappers
can't round-trip through strptime: StartOfWeekMapper (%V isn't strptime-parseable) and
StartOfQuarterMapper ({quarter} isn't a directive). The actual requirement is "recover Y/m/d" and
"recover Y/quarter". But the decode that motivated the whole compiler ends up discarding the token
it was built for:

StartOfWeekMapper.decode_downstream — %V is captured but never read

return datetime(int(match["Y"]), int(match["m"]), int(match["d"]))

Both decodes collapse to a dedicated per-mapper regex:

_WEEK_RE = re.compile(r"(?P\d{4})-(?P\d{2})-(?P\d{2})")
_QUARTER_RE = re.compile(r"(?P\d{4})-Q(?P[1-4])")

That drops the directive table, the {name} placeholder machinery, the placeholder_patterns arg,
and the five compile-time ValueError branches — plus the test class that only exists to exercise
that invented surface (test_rejects_adjacent_default_pattern_placeholders,
test_adjacent_placeholders_allowed_when_one_is_narrowed,
test_separator_between_default_placeholders_is_allowed). Roughly ~120 lines + tests, with
identical behavior for every shipped mapper. Could we go with the dedicated regexes for now and
add generality if/when a custom mapper actually needs it?

Follow-on: if the temporal decode simplifies, the base-class guard scaffolding in
partition_mappers/base.py — the init_subclass XOR check, expected_decoded_type, and the
runtime pairing check in RollupMapper.init — largely loses its purpose for the shipped
mappers, since it mostly exists to make the general decode/encode pair safe. Worth reassessing
whether it belongs now or arrives with the first real custom mapper.

Question (non-blocking): the audit-log path (_record_partition_audit_log /
_record_stale_apdr_audit_log) opens independent scoped=False sessions to survive an outer
rollback, plus a process-lifetime dedup set. For advisory Log rows where self.log.exception(...)
already records the same thing, is the rollback-survival guarantee worth the machinery for v1, or
would a plain log line do?

@Lee-W

Copy link
Copy Markdown
MemberAuthor

Hey @dstandish , thanks for reviewing. a few responses to the questions

Could we go with the dedicated regexes for now and add generality if/when a custom mapper actually needs it?

The regex compiler was added per @uranusjr's review: without it, a custom output_format forces the user to also override decode_downstream, which TP found surprising. Hardcoding _WEEK_RE / _QUARTER_RE saves the ~120 lines but is not that users friendly

is the rollback-survival guarantee worth the machinery for v1, or would a plain log line do?

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/newsfragments/64571.significant.rst

@uranusjruranusjr left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I still find it somewhat awkward the UI API endpoints need to load the serialized dag to calculate data. But I guess it’s not too useful to overthink this unless there are known performance issues.

Should we add some links to Starlette for the format-parsing logic?

Comment threadairflow-core/src/airflow/jobs/scheduler_job_runner.py Outdated
Comment threadairflow-core/src/airflow/config_templates/config.yml Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst Outdated
Comment threadairflow-core/docs/authoring-and-scheduling/assets.rst
@dstandish

Copy link
Copy Markdown
Contributor

Agree it might not worth the machinery for v1. The only reason I added it was for better discoverability. I can drop both to plain self.log and add it back if someone asks it. WDYT?

I don't really have a strong feeling about that. I tried to look for it in the code but could not find it.

And I don't stand in the way of merging this thing....

But... I can't help feeling that this PR is bigger than it should be. It's feels so big that it's kind of overwhelming to properly review. It spreads the reviewer's attention over a very wide area.

@Lee-W

Lee-W commented Jun 4, 2026

Copy link
Copy Markdown
MemberAuthor

Should we add some links to Starlette for the format-parsing logic?

yep, added a pointer to Starlette's compile_path in the _compile_output_format_regex docstring.

Lee-W added 9 commits June 4, 2026 23:26
- Introduces `RollupMapper(upstream_mapper=..., window=...)` so a partitioned
Dag run waits until the full set of upstream partitions for one downstream
period has arrived (e.g. 24 hourly events for a daily rollup).
- Ships 6 temporal `Window` built-ins (Hour / Day / Week / Month / Quarter /
Year); custom windows are rejected at serialization time.
- Surfaces frozen / mapper-error / partial-rollup state on the next-run-assets
UI; `pending_partition_count` and rollup-aware totals stay symmetric across
list/detail routes.
See `airflow-core/newsfragments/64571.significant.rst` for the full breakdown,
including the documented DST edge case for `DayWindow` + local-tz mappers.
…uler logs
The misconfigured-mapper and stale-APDR-cleanup paths persisted audit Log
rows on independent scoped=False sessions plus a per-process dedup set so
the rows survived an outer rollback. For advisory records this is heavier
than the problem warrants in v1 — the misconfig path already logs the
exception every tick, and stale cleanup now emits a structured info log.
Re-add the UI-visible audit rows if operators report needing them.
Add an ``is_rollup`` TypeGuard helper next to ``RollupMapper`` (mirroring
``is_mapped``) and use it in the partitioned-asset readiness check so the
mapper narrows to ``RollupMapper`` without a ``cast``.
…ion changes
Stale-cleanup previously dropped a pending AssetPartitionDagRun whenever the
Dag's serialized version changed, so any unrelated structural edit discarded
in-flight partition accumulation and could leave a rollup held forever.
Stamp a rollup-definition fingerprint (the serialized partition mappers of the
Dag's partitioned assets) on the APDR at creation and compare that against the
latest definition instead of the Dag version, so only a genuine mapper/window
change clears the run. Replaces the unreleased ``dag_version_id`` column on
AssetPartitionDagRun with ``rollup_fingerprint`` (migration 0119 amended).
The per-tick cap on pending AssetPartitionDagRun rows was exposed as the
``[scheduler] max_partition_dag_runs_to_create_per_loop`` setting, but it is a
performance safety bound (keeping the per-tick transaction from starving
executor heartbeats and regular scheduling), not a knob operators can
meaningfully tune. Drop the unreleased setting and keep the bound as a
module-level constant. The per-loop query LIMIT is retained.
Point the ``{name}`` placeholder handling at the Starlette routing prior art
it mirrors, so the parsing approach is discoverable.
The docs spellchecker rejects the coined abbreviation; spell it out as a
partition Dag run and drop the double spaces.
Spell out why a temporal upstream mapper is needed (it normalizes each
upstream key to the window's granularity), clarify that an identity mapper
paired with a temporal window fails as a type mismatch at construction, and
cross-reference RollupMapper / Window classes to the API docs.
…rint
The fingerprint tests built a core PartitionedAssetTimetable from Task SDK
Asset / RollupMapper objects, which tripped mypy arg-type checks. Construct it
with serialized assets and the core mapper/window types (matching the existing
test convention) imported at module top level; the serialized fingerprint is
unchanged.
@potiuk

Copy link
Copy Markdown
Member

@uranusjr's approval here is from Jun 2 (aa9f248), but there have been ~9 commits since — scheduler refactors, the partition-Dag-run clearing fix, mapper narrowing, etc. Worth a re-review so the green check reflects the current state before this merges. @uranusjr, could you re-confirm when you get a chance?


Drafted-by: Claude Code (Opus 4.8); reviewed by @potiuk before posting

Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:Schedulerincluding HA (high availability) schedulerarea:task-sdk

Projects

No open projects

Development

Successfully merging this pull request may close these issues.

Implement rollup (many-to-one partition mapper)

10 participants

@Lee-W@eladkal@dstandish@potiuk@ashb@uranusjr@kaxil@phanikumv@vatsrahul1001