This is an automated email from the ASF dual-hosted git repository.
bbovenzi pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git
The following commit(s) were added to refs/heads/main by this push:
new 8e36d6d6efd Show whether a calendar day's run was a backfill (#72782)
8e36d6d6efd is described below
commit 8e36d6d6efdb1811570e5915deed404635171ccf
Author: Software Developer <[email protected]>
AuthorDate: Wed Sep 30 16:53:54 2026 +0200
Show whether a calendar day's run was a backfill (#72782)
* show backfill state on calendar view
* addressing PR remarks.
* addressing PR remarks.
---
.../api_fastapi/core_api/datamodels/ui/calendar.py | 1 +
.../api_fastapi/core_api/openapi/_private_ui.yaml | 4 +
.../api_fastapi/core_api/services/ui/calendar.py | 10 +-
.../airflow/ui/openapi-gen/requests/schemas.gen.ts | 5 +
.../airflow/ui/openapi-gen/requests/types.gen.ts | 1 +
.../src/airflow/ui/public/i18n/locales/en/dag.json | 1 +
.../ui/src/pages/Dag/Calendar/CalendarCell.tsx | 31 ++-
.../ui/src/pages/Dag/Calendar/CalendarLegend.tsx | 11 +
.../ui/src/pages/Dag/Calendar/CalendarTooltip.tsx | 14 +-
.../src/pages/Dag/Calendar/HourlyCalendarView.tsx | 10 +-
.../src/pages/Dag/Calendar/calendarUtils.test.ts | 22 ++
.../ui/src/pages/Dag/Calendar/calendarUtils.ts | 4 +
.../src/airflow/ui/src/pages/Dag/Calendar/types.ts | 1 +
.../core_api/routes/ui/test_calendar.py | 239 ++++++++++++++++++---
14 files changed, 319 insertions(+), 35 deletions(-)
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/calendar.py
b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/calendar.py
index 09f5062a497..4dd75711b7f 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/calendar.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/datamodels/ui/calendar.py
@@ -35,6 +35,7 @@ class CalendarTimeRangeResponse(BaseModel):
"planned",
]
count: int
+ is_backfill: bool = False
class CalendarTimeRangeCollectionResponse(BaseModel):
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
index 66032afd609..3c96ebf967f 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
+++ b/airflow-core/src/airflow/api_fastapi/core_api/openapi/_private_ui.yaml
@@ -2851,6 +2851,10 @@ components:
count:
type: integer
title: Count
+ is_backfill:
+ type: boolean
+ title: Is Backfill
+ default: false
type: object
required:
- date
diff --git
a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
index c6853cafb83..13d6170b92b 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/calendar.py
@@ -40,6 +40,7 @@ from airflow.serialization.definitions.dag import
SerializedDAG
from airflow.timetables._cron import CronMixin
from airflow.timetables.base import DagRunInfo, DataInterval, TimeRestriction
from airflow.utils.sqlalchemy import get_dialect_name
+from airflow.utils.types import DagRunType
log = structlog.get_logger(logger_name=__name__)
@@ -101,10 +102,16 @@ class CalendarService:
effective_date = sa.func.coalesce(DagRun.partition_date,
DagRun.logical_date)
time_expression = self._get_time_truncation_expression(effective_date,
granularity, dialect)
+ is_backfill_expression = sa.case(
+ (DagRun.run_type == DagRunType.BACKFILL_JOB, sa.literal(True)),
+ else_=sa.literal(False),
+ )
+
select_stmt = (
sa.select(
time_expression.label("datetime"),
DagRun.state,
+ is_backfill_expression.label("is_backfill"),
sa.func.max(DagRun.data_interval_start).label("data_interval_start"),
sa.func.max(DagRun.data_interval_end).label("data_interval_end"),
sa.func.max(DagRun.run_after).label("run_after"),
@@ -112,7 +119,7 @@ class CalendarService:
sa.func.count("*").label("count"),
)
.where(DagRun.dag_id == dag_id)
- .group_by(time_expression, DagRun.state)
+ .group_by(time_expression, DagRun.state, is_backfill_expression)
.order_by(time_expression.asc())
)
@@ -125,6 +132,7 @@ class CalendarService:
date=ds.datetime,
state=ds.state,
count=int(ds._mapping["count"]),
+ is_backfill=bool(ds._mapping["is_backfill"]),
)
for ds in dag_states
]
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
index 729d8a5ad80..d5943c54077 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/schemas.gen.ts
@@ -9854,6 +9854,11 @@ export const $CalendarTimeRangeResponse = {
count: {
type: 'integer',
title: 'Count'
+ },
+ is_backfill: {
+ type: 'boolean',
+ title: 'Is Backfill',
+ default: false
}
},
type: 'object',
diff --git a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
index 4017f33daee..441d1e1eb55 100644
--- a/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
+++ b/airflow-core/src/airflow/ui/openapi-gen/requests/types.gen.ts
@@ -2517,6 +2517,7 @@ export type CalendarTimeRangeResponse = {
date: string;
state: 'queued' | 'running' | 'success' | 'failed' | 'planned';
count: number;
+ is_backfill?: boolean;
};
export type state = 'queued' | 'running' | 'success' | 'failed' | 'planned';
diff --git a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
index feb8c81c88e..62ecc2d5c24 100644
--- a/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
+++ b/airflow-core/src/airflow/ui/public/i18n/locales/en/dag.json
@@ -6,6 +6,7 @@
"title": "Dependencies Blocking Task From Getting Scheduled"
},
"calendar": {
+ "backfill": "Backfill",
"daily": "Daily",
"hourly": "Hourly",
"legend": {
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx
index 1244e20b907..73d3df9b6b5 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarCell.tsx
@@ -21,6 +21,8 @@ import { FiAlertTriangle, FiClock } from "react-icons/fi";
import { RouterLink, Tooltip } from "src/system-components";
+import { RunTypeIcon } from "src/components/RunTypeIcon";
+
import { CalendarTooltip } from "./CalendarTooltip";
import type { CalendarCellData, CalendarColorMode } from "./types";
@@ -63,7 +65,11 @@ export const CalendarCell = ({
const runStates = cellData
? Object.entries(cellData.counts)
.filter(
- ([key, value]) => key !== "total" && value > 0 && (viewMode ===
"failed" ? key === "failed" : true),
+ ([key, value]) =>
+ key !== "total" &&
+ key !== "backfill" &&
+ value > 0 &&
+ (viewMode === "failed" ? key === "failed" : true),
)
.map(([key]) => key)
: [];
@@ -95,6 +101,27 @@ export const CalendarCell = ({
</Box>
) : undefined;
+ const hasBackfill = (cellData?.counts.backfill ?? 0) > 0;
+ const backfillIndicator = hasBackfill ? (
+ <Box
+ alignItems="center"
+ color="white"
+ data-testid="backfill-indicator"
+ display="flex"
+ filter="drop-shadow(0 0 1px rgba(0, 0, 0, 0.7))"
+ fontSize="9px"
+ height="100%"
+ justifyContent="center"
+ left="0"
+ lineHeight={1}
+ position="absolute"
+ top="0"
+ width="100%"
+ >
+ <RunTypeIcon runType="backfill" />
+ </Box>
+ ) : undefined;
+
const cellBox = isMixedState ? (
<Box
_hover={hasData ? { transform: "scale(1.1)" } : {}}
@@ -125,6 +152,7 @@ export const CalendarCell = ({
width="100%"
/>
{deadlineIndicator}
+ {backfillIndicator}
</Box>
) : (
<Box
@@ -142,6 +170,7 @@ export const CalendarCell = ({
width="14px"
>
{deadlineIndicator}
+ {backfillIndicator}
</Box>
);
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx
index c1a6f0d4b01..7c546be4370 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarLegend.tsx
@@ -22,6 +22,8 @@ import { FiAlertTriangle, FiClock } from "react-icons/fi";
import { Tooltip } from "src/system-components";
+import { RunTypeIcon } from "src/components/RunTypeIcon";
+
import { LegendIcon } from "./LegendIcon";
import { PLANNED_COLOR } from "./calendarUtils";
import type { CalendarScale, CalendarColorMode } from "./types";
@@ -135,6 +137,15 @@ export const CalendarLegend = ({ hasDeadlines = false,
scale, vertical = false,
{translate("dag:calendar.legend.mixed")}
</Text>
</HStack>
+
+ <HStack gap={2}>
+ <Box fontSize="14px" lineHeight={1}>
+ <RunTypeIcon runType="backfill" />
+ </Box>
+ <Text color="fg.muted" fontSize="xs">
+ {translate("dag:calendar.backfill")}
+ </Text>
+ </HStack>
</HStack>
</Box>
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
index 1d026032655..8ca36b11109 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/CalendarTooltip.tsx
@@ -20,6 +20,8 @@ import { Box, HStack, Text, VStack } from "@chakra-ui/react";
import { useTranslation } from "react-i18next";
import { FiAlertTriangle, FiClock } from "react-icons/fi";
+import { RunTypeIcon } from "src/components/RunTypeIcon";
+
import type { CalendarCellData, CalendarColorMode } from "./types";
const SQUARE_SIZE = "12px";
@@ -57,7 +59,7 @@ export const CalendarTooltip = ({ cellData, viewMode =
"total" }: Props) => {
// In failed mode, only show failed runs; in total mode, show all non-zero
states
const states = Object.entries(counts)
.filter(([key, value]) => {
- if (key === "total") {
+ if (key === "total" || key === "backfill") {
return false;
}
if (value === 0) {
@@ -97,6 +99,16 @@ export const CalendarTooltip = ({ cellData, viewMode =
"total" }: Props) => {
</Text>
</HStack>
))}
+ {counts.backfill > 0 && (
+ <HStack data-testid="calendar-tooltip-backfill" gap={3}>
+ <Box fontSize={SQUARE_SIZE} lineHeight={1}>
+ <RunTypeIcon runType="backfill" />
+ </Box>
+ <Text fontSize="xs">
+ {counts.backfill} {translate("dag:calendar.backfill")}
+ </Text>
+ </HStack>
+ )}
</VStack>
) : null}
{hasDeadlines ? (
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/HourlyCalendarView.tsx
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/HourlyCalendarView.tsx
index 5f584591b7a..503f9c531e6 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/HourlyCalendarView.tsx
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/HourlyCalendarView.tsx
@@ -175,7 +175,15 @@ export const HourlyCalendarView = ({
const hourData = day.hours.find((hourItem) => hourItem.hour
=== hour);
if (!hourData) {
- const emptyCounts = { failed: 0, planned: 0, queued: 0,
running: 0, success: 0, total: 0 };
+ const emptyCounts = {
+ backfill: 0,
+ failed: 0,
+ planned: 0,
+ queued: 0,
+ running: 0,
+ success: 0,
+ total: 0,
+ };
const emptyData = {
counts: emptyCounts,
date: `${dayjs(day.day).format("MMM DD")},
${hour.toString().padStart(2, "0")}:00`,
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts
index 738ebf698cc..fd73589dced 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.test.ts
@@ -35,6 +35,7 @@ const DEFAULT_FAILED_COLOR = { _dark: "red.700", _light:
"red.400" };
const DEFAULT_RUNNING_COLOR = { _dark: "cyan.700", _light: "cyan.400" };
const EMPTY_COUNTS: RunCounts = {
+ backfill: 0,
failed: 0,
planned: 0,
queued: 0,
@@ -50,9 +51,15 @@ const run = (
): CalendarTimeRangeResponse => ({
count,
date,
+ is_backfill: false,
state,
});
+const backfillRun = (
+ state: CalendarTimeRangeResponse["state"],
+ count: number,
+): CalendarTimeRangeResponse => ({ ...run(state, count), is_backfill: true });
+
describe("calculateRunCounts", () => {
it("counts each calendar state and includes all states in total", () => {
expect(
@@ -64,6 +71,7 @@ describe("calculateRunCounts", () => {
run("planned", 5),
]),
).toEqual({
+ backfill: 0,
failed: 1,
planned: 5,
queued: 4,
@@ -72,6 +80,20 @@ describe("calculateRunCounts", () => {
total: 15,
});
});
+
+ it("counts backfill runs separately from their state", () => {
+ expect(
+ calculateRunCounts([backfillRun("success", 1), backfillRun("failed", 2),
run("success", 3)]),
+ ).toEqual({
+ backfill: 3,
+ failed: 2,
+ planned: 0,
+ queued: 0,
+ running: 0,
+ success: 4,
+ total: 6,
+ });
+ });
});
describe("calculateDataBounds", () => {
diff --git
a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts
index 58a956c28f7..05acf1080ee 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/calendarUtils.ts
@@ -129,6 +129,7 @@ export const buildDeadlineDateMap = (
export const calculateRunCounts = (runs: Array<CalendarTimeRangeResponse>):
RunCounts => {
const counts: { [K in keyof RunCounts]: number } = {
+ backfill: 0,
failed: 0,
planned: 0,
queued: 0,
@@ -143,6 +144,9 @@ export const calculateRunCounts = (runs:
Array<CalendarTimeRangeResponse>): RunC
if (state in counts) {
counts[state] += count;
}
+ if (run.is_backfill === true) {
+ counts.backfill += count;
+ }
counts.total += count;
});
diff --git a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts
b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts
index 755ca5ead23..8476ac8895f 100644
--- a/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts
+++ b/airflow-core/src/airflow/ui/src/pages/Dag/Calendar/types.ts
@@ -21,6 +21,7 @@ import type { CalendarTimeRangeResponse } from
"openapi/requests/types.gen";
export type DagRunState = "failed" | "planned" | "queued" | "running" |
"success";
export type RunCounts = {
+ backfill: number;
failed: number;
planned: number;
queued: number;
diff --git
a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
index 95d8da70b2e..dccec5832b9 100644
--- a/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
+++ b/airflow-core/tests/unit/api_fastapi/core_api/routes/ui/test_calendar.py
@@ -81,11 +81,31 @@ class TestCalendar:
{
"total_entries": 5,
"dag_runs": [
- {"date": "2025-01-01T00:00:00Z", "state": "failed",
"count": 1},
- {"date": "2025-01-01T00:00:00Z", "state": "success",
"count": 1},
- {"date": "2025-01-02T00:00:00Z", "state": "running",
"count": 1},
- {"date": "2025-01-02T00:00:00Z", "state": "planned",
"count": 1},
- {"date": "2025-01-03T00:00:00Z", "state": "planned",
"count": 2},
+ {"date": "2025-01-01T00:00:00Z", "state": "failed",
"count": 1, "is_backfill": False},
+ {
+ "date": "2025-01-01T00:00:00Z",
+ "state": "success",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {
+ "date": "2025-01-02T00:00:00Z",
+ "state": "running",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {
+ "date": "2025-01-02T00:00:00Z",
+ "state": "planned",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {
+ "date": "2025-01-03T00:00:00Z",
+ "state": "planned",
+ "count": 2,
+ "is_backfill": False,
+ },
],
},
),
@@ -94,8 +114,13 @@ class TestCalendar:
{
"total_entries": 2,
"dag_runs": [
- {"date": "2025-01-01T00:00:00Z", "state": "failed",
"count": 1},
- {"date": "2025-01-01T00:00:00Z", "state": "success",
"count": 1},
+ {"date": "2025-01-01T00:00:00Z", "state": "failed",
"count": 1, "is_backfill": False},
+ {
+ "date": "2025-01-01T00:00:00Z",
+ "state": "success",
+ "count": 1,
+ "is_backfill": False,
+ },
],
},
),
@@ -104,8 +129,18 @@ class TestCalendar:
{
"total_entries": 2,
"dag_runs": [
- {"date": "2025-01-02T00:00:00Z", "state": "running",
"count": 1},
- {"date": "2025-01-02T00:00:00Z", "state": "planned",
"count": 1},
+ {
+ "date": "2025-01-02T00:00:00Z",
+ "state": "running",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {
+ "date": "2025-01-02T00:00:00Z",
+ "state": "planned",
+ "count": 1,
+ "is_backfill": False,
+ },
],
},
),
@@ -128,12 +163,37 @@ class TestCalendar:
{
"total_entries": 6,
"dag_runs": [
- {"date": "2025-01-01T00:00:00Z", "state": "failed",
"count": 1},
- {"date": "2025-01-01T01:00:00Z", "state": "success",
"count": 1},
- {"date": "2025-01-02T00:00:00Z", "state": "running",
"count": 1},
- {"date": "2025-01-02T01:00:00Z", "state": "planned",
"count": 1},
- {"date": "2025-01-03T00:00:00Z", "state": "planned",
"count": 1},
- {"date": "2025-01-03T01:00:00Z", "state": "planned",
"count": 1},
+ {"date": "2025-01-01T00:00:00Z", "state": "failed",
"count": 1, "is_backfill": False},
+ {
+ "date": "2025-01-01T01:00:00Z",
+ "state": "success",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {
+ "date": "2025-01-02T00:00:00Z",
+ "state": "running",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {
+ "date": "2025-01-02T01:00:00Z",
+ "state": "planned",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {
+ "date": "2025-01-03T00:00:00Z",
+ "state": "planned",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {
+ "date": "2025-01-03T01:00:00Z",
+ "state": "planned",
+ "count": 1,
+ "is_backfill": False,
+ },
],
},
),
@@ -146,8 +206,18 @@ class TestCalendar:
{
"total_entries": 2,
"dag_runs": [
- {"date": "2025-01-02T00:00:00Z", "state": "running",
"count": 1},
- {"date": "2025-01-02T01:00:00Z", "state": "planned",
"count": 1},
+ {
+ "date": "2025-01-02T00:00:00Z",
+ "state": "running",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {
+ "date": "2025-01-02T01:00:00Z",
+ "state": "planned",
+ "count": 1,
+ "is_backfill": False,
+ },
],
},
),
@@ -175,8 +245,18 @@ class TestCalendar:
{
"total_entries": 2,
"dag_runs": [
- {"date": "2025-01-02T00:00:00Z", "state": "running",
"count": 1},
- {"date": "2025-01-02T01:00:00Z", "state": "planned",
"count": 1},
+ {
+ "date": "2025-01-02T00:00:00Z",
+ "state": "running",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {
+ "date": "2025-01-02T01:00:00Z",
+ "state": "planned",
+ "count": 1,
+ "is_backfill": False,
+ },
],
},
),
@@ -373,10 +453,25 @@ class TestPartitionedCalendar:
{
"total_entries": 4,
"dag_runs": [
- {"date": "2025-01-01T00:00:00Z", "state": "success",
"count": 1},
- {"date": "2025-01-02T00:00:00Z", "state": "failed",
"count": 1},
- {"date": "2025-01-03T00:00:00Z", "state": "success",
"count": 1},
- {"date": "2025-01-04T00:00:00Z", "state": "running",
"count": 1},
+ {
+ "date": "2025-01-01T00:00:00Z",
+ "state": "success",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {"date": "2025-01-02T00:00:00Z", "state": "failed",
"count": 1, "is_backfill": False},
+ {
+ "date": "2025-01-03T00:00:00Z",
+ "state": "success",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {
+ "date": "2025-01-04T00:00:00Z",
+ "state": "running",
+ "count": 1,
+ "is_backfill": False,
+ },
],
},
),
@@ -385,8 +480,13 @@ class TestPartitionedCalendar:
{
"total_entries": 2,
"dag_runs": [
- {"date": "2025-01-02T00:00:00Z", "state": "failed",
"count": 1},
- {"date": "2025-01-03T00:00:00Z", "state": "success",
"count": 1},
+ {"date": "2025-01-02T00:00:00Z", "state": "failed",
"count": 1, "is_backfill": False},
+ {
+ "date": "2025-01-03T00:00:00Z",
+ "state": "success",
+ "count": 1,
+ "is_backfill": False,
+ },
],
},
),
@@ -395,7 +495,12 @@ class TestPartitionedCalendar:
{
"total_entries": 1,
"dag_runs": [
- {"date": "2025-01-04T00:00:00Z", "state": "running",
"count": 1},
+ {
+ "date": "2025-01-04T00:00:00Z",
+ "state": "running",
+ "count": 1,
+ "is_backfill": False,
+ },
],
},
),
@@ -416,10 +521,25 @@ class TestPartitionedCalendar:
{
"total_entries": 4,
"dag_runs": [
- {"date": "2025-01-01T00:00:00Z", "state": "success",
"count": 1},
- {"date": "2025-01-02T00:00:00Z", "state": "failed",
"count": 1},
- {"date": "2025-01-03T00:00:00Z", "state": "success",
"count": 1},
- {"date": "2025-01-04T00:00:00Z", "state": "running",
"count": 1},
+ {
+ "date": "2025-01-01T00:00:00Z",
+ "state": "success",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {"date": "2025-01-02T00:00:00Z", "state": "failed",
"count": 1, "is_backfill": False},
+ {
+ "date": "2025-01-03T00:00:00Z",
+ "state": "success",
+ "count": 1,
+ "is_backfill": False,
+ },
+ {
+ "date": "2025-01-04T00:00:00Z",
+ "state": "running",
+ "count": 1,
+ "is_backfill": False,
+ },
],
},
),
@@ -432,7 +552,12 @@ class TestPartitionedCalendar:
{
"total_entries": 1,
"dag_runs": [
- {"date": "2025-01-01T00:00:00Z", "state": "success",
"count": 1},
+ {
+ "date": "2025-01-01T00:00:00Z",
+ "state": "success",
+ "count": 1,
+ "is_backfill": False,
+ },
],
},
),
@@ -484,6 +609,58 @@ class TestCalendarPlannedRunsCap:
assert sum(r["count"] for r in planned) ==
CalendarService.MAX_PLANNED_RUNS
+class TestCalendarBackfill:
+ """Historical runs must expose whether they came from a backfill."""
+
+ DAG_NAME = "test_backfill_calendar_dag"
+
+ @pytest.fixture(autouse=True)
+ @provide_session
+ def setup_dag_runs(self, dag_maker, *, session: Session = NEW_SESSION) ->
None:
+ clear_db_runs()
+ clear_db_dags()
+ with dag_maker(
+ self.DAG_NAME,
+ schedule="0 0,1 * * *",
+ start_date=datetime(2025, 1, 1),
+ end_date=datetime(2025, 1, 1, 1),
+ catchup=True,
+ serialized=True,
+ session=session,
+ ):
+ EmptyOperator(task_id="test_task1")
+ dag_maker.create_dagrun(
+ run_id="scheduled_run",
+ state=DagRunState.SUCCESS,
+ run_type=DagRunType.SCHEDULED,
+ logical_date=datetime(2025, 1, 1),
+ )
+ dag_maker.create_dagrun(
+ run_id="backfill_run",
+ state=DagRunState.SUCCESS,
+ run_type=DagRunType.BACKFILL_JOB,
+ logical_date=datetime(2025, 1, 1, 1),
+ )
+ dag_maker.sync_dagbag_to_db()
+ session.commit()
+
+ def teardown_method(self) -> None:
+ clear_db_runs()
+ clear_db_dags()
+
+ def test_backfill_and_scheduled_runs_split_by_is_backfill(self,
test_client):
+ response = test_client.get(f"/calendar/{self.DAG_NAME}")
+ assert response.status_code == 200
+ historical = sorted(
+ (r for r in response.json()["dag_runs"] if r["state"] !=
"planned"),
+ key=lambda r: r["is_backfill"],
+ )
+ assert historical == [
+ {"date": "2025-01-01T00:00:00Z", "state": "success", "count": 1,
"is_backfill": False},
+ {"date": "2025-01-01T00:00:00Z", "state": "success", "count": 1,
"is_backfill": True},
+ ]
+
+
_CALLBACK_PATH =
"tests.unit.api_fastapi.core_api.routes.ui.test_calendar._noop_callback"