Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions airflow-core/docs/core-concepts/dag-run.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,6 +167,8 @@ the errors after going through the logs, you can re-run the tasks by clearing th
scheduled date. Clearing a task instance creates a record of the task instance.
The ``try_number`` of the current task instance is incremented, the ``max_tries`` set to ``0`` and the state set to ``None``, which causes the task to re-run.

An experimental feature in Airflow 3.1.0 allows you to clear the task instances and re-run with the latest bundle version.

Click on the failed task in the Tree or Graph views and then click on **Clear**.
The executor will re-run it.

Expand Down
4 changes: 3 additions & 1 deletion airflow-core/src/airflow/api_fastapi/app.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -84,8 +84,10 @@ def create_app(apps: str = "all") -> FastAPI:
dag_bag = create_dag_bag()
Comment thread
jason810496 marked this conversation as resolved.

if "execution" in apps_list or "all" in apps_list:
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

task_exec_api_app = create_task_execution_api_app()
task_exec_api_app.state.dag_bag = dag_bag
task_exec_api_app.state.dag_bag = SchedulerDagBag()
init_error_handlers(task_exec_api_app)
Comment thread
jason810496 marked this conversation as resolved.
app.mount("/execution", task_exec_api_app)

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,6 +55,10 @@ class DAGRunClearBody(StrictBaseModel):

dry_run: bool = True
only_failed: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after clearing the DAG Run.",
)


class DAGRunResponse(BaseModel):
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,11 @@ class ClearTaskInstancesBody(StrictBaseModel):
include_downstream: bool = False
include_future: bool = False
include_past: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after "
"clearing the task instances.",
)

@model_validator(mode="before")
@classmethod
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8396,6 +8396,12 @@ components:
type: boolean
title: Include Past
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
Comment thread
ephraimbuddy marked this conversation as resolved.
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the task instances.
default: false
additionalProperties: false
type: object
title: ClearTaskInstancesBody
Expand DownExpand Up@@ -9049,6 +9055,12 @@ components:
type: boolean
title: Only Failed
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the DAG Run.
default: false
additionalProperties: false
type: object
title: DAGRunClearBody
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,6 +281,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
dry_run=True,
session=session,
)
Expand All@@ -293,6 +294,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
session=session,
)
dag_run_cleared = session.scalar(select(DagRun).where(DagRun.id == dag_run.id))
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -675,7 +675,11 @@ def post_clear_task_instances(
if dag_run is None:
error_message = f"Dag Run id {dag_run_id} not found in dag {dag_id}"
raise HTTPException(status.HTTP_404_NOT_FOUND, error_message)
# If dag_run_id is provided, we should get the dag from SchedulerDagBag
# to ensure we get the right version.
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag = SchedulerDagBag().get_dag(dag_run=dag_run, session=session)
if past or future:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
Expand DownExpand Up@@ -724,6 +728,7 @@ def post_clear_task_instances(
task_instances,
session,
DagRunState.QUEUED if reset_dag_runs else False,
run_on_latest_version=body.run_on_latest_version,
)

return TaskInstanceCollectionResponse(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,6 @@
from airflow.api_fastapi.execution_api.datamodels.dagrun import DagRunStateResponse, TriggerDAGRunPayload
from airflow.exceptions import DagRunAlreadyExists
from airflow.models.dag import DagModel
from airflow.models.dagbag import DagBag
from airflow.models.dagrun import DagRun
from airflow.utils.types import DagRunTriggeredByType

Expand DownExpand Up@@ -122,9 +121,20 @@ def clear_dag_run(
"message": f"DAG with dag_id: '{dag_id}' has import errors and cannot be triggered",
},
)
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag_run = session.scalar(select(DagRun).where(DagRun.dag_id == dag_id, DagRun.run_id == run_id))
dag_bag = SchedulerDagBag()
Comment thread
jason810496 marked this conversation as resolved.
Comment thread
jedcunningham marked this conversation as resolved.
dag = dag_bag.get_dag(dag_run=dag_run, session=session)
if not dag:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail={
"reason": "Not Found",
"message": f"DAG with dag_id: '{dag_id}' was not found in the DagBag",
},
)

dag_bag = DagBag(dag_folder=dm.fileloc, read_dags_from_db=True)
dag = dag_bag.get_dag(dag_id)
dag.clear(run_id=run_id)


Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,15 +29,15 @@
import attrs
import structlog
from cadwyn import VersionedAPIRouter
from fastapi import Body, HTTPException, Query, status
from fastapi import Body, Depends, HTTPException, Query, status
from pydantic import JsonValue
from sqlalchemy import func, or_, tuple_, update
from sqlalchemy.exc import NoResultFound, SQLAlchemyError
from sqlalchemy.orm import joinedload
from sqlalchemy.sql import select
from structlog.contextvars import bind_contextvars

from airflow.api_fastapi.common.dagbag import DagBagDep
from airflow.api_fastapi.common.dagbag import dag_bag_from_app
from airflow.api_fastapi.common.db.common import SessionDep
from airflow.api_fastapi.common.types import UtcDateTime
from airflow.api_fastapi.execution_api.datamodels.taskinstance import (
Expand DownExpand Up@@ -76,6 +76,9 @@
from airflow.models.expandinput import SchedulerExpandInput
from airflow.sdk.types import Operator

from airflow.jobs.scheduler_job_runner import SchedulerDagBag

SchedulerDagBagDep = Annotated[SchedulerDagBag, Depends(dag_bag_from_app)]
Comment thread
jason810496 marked this conversation as resolved.

router = VersionedAPIRouter()

Expand DownExpand Up@@ -104,7 +107,7 @@ def ti_run(
task_instance_id: UUID,
ti_run_payload: Annotated[TIEnterRunningPayload, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> TIRunContext:
"""
Run a TaskInstance.
Expand DownExpand Up@@ -255,7 +258,7 @@ def ti_run(
or 0
)

if dag := dag_bag.get_dag(ti.dag_id):
if dag := dag_bag.get_dag(dag_run=dr, session=session):
upstream_map_indexes = dict(
_get_upstream_map_indexes(dag.get_task(ti.task_id), ti.map_index, ti.run_id, session)
)
Expand DownExpand Up@@ -330,7 +333,7 @@ def ti_update_state(
task_instance_id: UUID,
ti_patch_payload: Annotated[TIStateUpdate, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
):
"""
Update the state of a TaskInstance.
Expand DownExpand Up@@ -417,8 +420,9 @@ def ti_update_state(
)


def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: DagBagDep) -> None:
ser_dag = dag_bag.get_dag(dag_id)
def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: SchedulerDagBagDep) -> None:
dr = ti.dag_run
ser_dag = dag_bag.get_dag(dag_run=dr, session=session)
if ser_dag and getattr(ser_dag, "fail_fast", False):
task_dict = getattr(ser_dag, "task_dict")
task_teardown_map = {k: v.is_teardown for k, v in task_dict.items()}
Expand All@@ -432,7 +436,7 @@ def _create_ti_state_update_query_and_update_state(
query: Update,
updated_state,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
dag_id: str,
) -> tuple[Update, TaskInstanceState]:
if isinstance(ti_patch_payload, (TITerminalStatePayload, TIRetryStatePayload, TISuccessStatePayload)):
Expand DownExpand Up@@ -893,7 +897,7 @@ def _get_group_tasks(dag_id: str, task_group_id: str, session: SessionDep, logic
def validate_inlets_and_outlets(
task_instance_id: UUID,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> InactiveAssetsResponse:
"""Validate whether there're inactive assets in inlets and outlets of a given task instance."""
ti_id_str = str(task_instance_id)
Expand All@@ -911,7 +915,8 @@ def validate_inlets_and_outlets(
)

if not ti.task:
dag = dag_bag.get_dag(ti.dag_id)
dr = ti.dag_run
dag = dag_bag.get_dag(dag_run=dr, session=session)
if dag:
with contextlib.suppress(TaskNotFound):
ti.task = dag.get_task(ti.task_id)
Expand Down
9 changes: 9 additions & 0 deletions airflow-core/src/airflow/cli/cli_config.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -340,6 +340,14 @@ def string_lower_type(val):
),
choices=("none", "completed", "failed"),
)
ARG_BACKFILL_RUN_ON_LATEST_VERSION = Arg(
("--run-on-latest-version",),
help=(
"(Experimental) If set, the backfill will run tasks using the latest bundle version instead of "
"the version that was active when the original Dag run was created."
),
action="store_true",
)


# misc
Expand DownExpand Up@@ -968,6 +976,7 @@ class GroupCommand(NamedTuple):
ARG_RUN_BACKWARDS,
ARG_MAX_ACTIVE_RUNS,
ARG_BACKFILL_REPROCESS_BEHAVIOR,
ARG_BACKFILL_RUN_ON_LATEST_VERSION,
ARG_BACKFILL_DRY_RUN,
),
),
Expand Down
2 changes: 2 additions & 0 deletions airflow-core/src/airflow/cli/commands/backfill_command.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ def create_backfill(args) -> None:
reverse=args.run_backwards,
dag_run_conf=args.dag_run_conf,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
for k, v in params.items():
console.print(f" - {k} = {v}")
Expand DownExpand Up@@ -88,4 +89,5 @@ def create_backfill(args) -> None:
dag_run_conf=args.dag_run_conf,
triggering_user_name=user,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
15 changes: 8 additions & 7 deletions airflow-core/src/airflow/jobs/scheduler_job_runner.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,15 +131,16 @@ def _get_dag(self, version_id: str, session: Session) -> DAG | None:
return dag

@staticmethod
def _version_from_dag_run(dag_run, session):
if dag_run.bundle_version:
dag_version = dag_run.created_dag_version
else:
def _version_from_dag_run(dag_run, latest, session):
if latest or not dag_run.bundle_version:
dag_version = DagVersion.get_latest_version(dag_id=dag_run.dag_id, session=session)
return dag_version
if dag_version:
return dag_version

return dag_run.created_dag_version

def get_dag(self, dag_run: DagRun, session: Session) -> DAG | None:
version = self._version_from_dag_run(dag_run=dag_run, session=session)
def get_dag(self, dag_run: DagRun, session: Session, latest=False) -> DAG | None:
Comment thread
jedcunningham marked this conversation as resolved.
version = self._version_from_dag_run(dag_run=dag_run, latest=latest, session=session)
if not version:
return None
return self._get_dag(version_id=version.id, session=session)
Expand Down
7 changes: 6 additions & 1 deletion airflow-core/src/airflow/models/backfill.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -291,6 +291,7 @@ def _create_backfill_dag_run(
dag_run_conf,
backfill_sort_ordinal,
triggering_user_name,
run_on_latest_version,
session,
):
from airflow.models.dagrun import DagRun
Expand DownExpand Up@@ -328,6 +329,7 @@ def _create_backfill_dag_run(
info=info,
backfill_id=backfill_id,
sort_ordinal=backfill_sort_ordinal,
run_on_latest=run_on_latest_version,
)
else:
session.add(
Expand DownExpand Up@@ -401,7 +403,7 @@ def _get_info_list(
return dagrun_info_list


def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal, run_on_latest=False):
"""Clear the existing DAG run and update backfill metadata."""
from sqlalchemy.sql import update

Expand All@@ -415,6 +417,7 @@ def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
session=session,
confirm_prompt=False,
dry_run=False,
run_on_latest_version=run_on_latest,
)

# Update backfill_id and run_type in DagRun table
Expand DownExpand Up@@ -447,6 +450,7 @@ def _create_backfill(
dag_run_conf: dict | None,
triggering_user_name: str | None,
reprocess_behavior: ReprocessBehavior | None = None,
run_on_latest_version: bool = False,
) -> Backfill | None:
from airflow.models import DagModel
from airflow.models.serialized_dag import SerializedDagModel
Expand DownExpand Up@@ -510,6 +514,7 @@ def _create_backfill(
reprocess_behavior=br.reprocess_behavior,
backfill_sort_ordinal=backfill_sort_ordinal,
triggering_user_name=br.triggering_user_name,
run_on_latest_version=run_on_latest_version,
session=session,
)
log.info(
Expand Down
7 changes: 7 additions & 0 deletions airflow-core/src/airflow/models/dag.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1282,6 +1282,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1299,6 +1300,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1317,6 +1319,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1335,6 +1338,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1354,6 +1358,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: bool = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1372,6 +1377,7 @@ def clear(
:param dag_run_state: state to set DagRun to. If set to False, dagrun state will not
be changed.
:param dry_run: Find the tasks to clear but don't clear them.
:param run_on_latest_version: whether to run on latest serialized DAG and Bundle version
:param session: The sqlalchemy session to use
:param dag_bag: The DagBag used to find the dags (Optional)
:param exclude_task_ids: A set of ``task_id`` or (``task_id``, ``map_index``)
Expand DownExpand Up@@ -1417,6 +1423,7 @@ def clear(
list(tis),
session,
dag_run_state=dag_run_state,
run_on_latest_version=run_on_latest_version,
)
else:
count = 0
Expand Down
Loading
, '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" + '
Add run_on_latest_version support for backfill and clear operations by ephraimbuddy · Pull Request #52177 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions airflow-core/docs/core-concepts/dag-run.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,6 +167,8 @@ the errors after going through the logs, you can re-run the tasks by clearing th
scheduled date. Clearing a task instance creates a record of the task instance.
The ``try_number`` of the current task instance is incremented, the ``max_tries`` set to ``0`` and the state set to ``None``, which causes the task to re-run.

An experimental feature in Airflow 3.1.0 allows you to clear the task instances and re-run with the latest bundle version.

Click on the failed task in the Tree or Graph views and then click on **Clear**.
The executor will re-run it.

Expand Down
4 changes: 3 additions & 1 deletion airflow-core/src/airflow/api_fastapi/app.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -84,8 +84,10 @@ def create_app(apps: str = "all") -> FastAPI:
dag_bag = create_dag_bag()
Comment thread
jason810496 marked this conversation as resolved.

if "execution" in apps_list or "all" in apps_list:
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

task_exec_api_app = create_task_execution_api_app()
task_exec_api_app.state.dag_bag = dag_bag
task_exec_api_app.state.dag_bag = SchedulerDagBag()
init_error_handlers(task_exec_api_app)
Comment thread
jason810496 marked this conversation as resolved.
app.mount("/execution", task_exec_api_app)

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,6 +55,10 @@ class DAGRunClearBody(StrictBaseModel):

dry_run: bool = True
only_failed: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after clearing the DAG Run.",
)


class DAGRunResponse(BaseModel):
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,11 @@ class ClearTaskInstancesBody(StrictBaseModel):
include_downstream: bool = False
include_future: bool = False
include_past: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after "
"clearing the task instances.",
)

@model_validator(mode="before")
@classmethod
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8396,6 +8396,12 @@ components:
type: boolean
title: Include Past
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
Comment thread
ephraimbuddy marked this conversation as resolved.
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the task instances.
default: false
additionalProperties: false
type: object
title: ClearTaskInstancesBody
Expand DownExpand Up@@ -9049,6 +9055,12 @@ components:
type: boolean
title: Only Failed
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the DAG Run.
default: false
additionalProperties: false
type: object
title: DAGRunClearBody
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,6 +281,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
dry_run=True,
session=session,
)
Expand All@@ -293,6 +294,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
session=session,
)
dag_run_cleared = session.scalar(select(DagRun).where(DagRun.id == dag_run.id))
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -675,7 +675,11 @@ def post_clear_task_instances(
if dag_run is None:
error_message = f"Dag Run id {dag_run_id} not found in dag {dag_id}"
raise HTTPException(status.HTTP_404_NOT_FOUND, error_message)
# If dag_run_id is provided, we should get the dag from SchedulerDagBag
# to ensure we get the right version.
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag = SchedulerDagBag().get_dag(dag_run=dag_run, session=session)
if past or future:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
Expand DownExpand Up@@ -724,6 +728,7 @@ def post_clear_task_instances(
task_instances,
session,
DagRunState.QUEUED if reset_dag_runs else False,
run_on_latest_version=body.run_on_latest_version,
)

return TaskInstanceCollectionResponse(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,6 @@
from airflow.api_fastapi.execution_api.datamodels.dagrun import DagRunStateResponse, TriggerDAGRunPayload
from airflow.exceptions import DagRunAlreadyExists
from airflow.models.dag import DagModel
from airflow.models.dagbag import DagBag
from airflow.models.dagrun import DagRun
from airflow.utils.types import DagRunTriggeredByType

Expand DownExpand Up@@ -122,9 +121,20 @@ def clear_dag_run(
"message": f"DAG with dag_id: '{dag_id}' has import errors and cannot be triggered",
},
)
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag_run = session.scalar(select(DagRun).where(DagRun.dag_id == dag_id, DagRun.run_id == run_id))
dag_bag = SchedulerDagBag()
Comment thread
jason810496 marked this conversation as resolved.
Comment thread
jedcunningham marked this conversation as resolved.
dag = dag_bag.get_dag(dag_run=dag_run, session=session)
if not dag:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail={
"reason": "Not Found",
"message": f"DAG with dag_id: '{dag_id}' was not found in the DagBag",
},
)

dag_bag = DagBag(dag_folder=dm.fileloc, read_dags_from_db=True)
dag = dag_bag.get_dag(dag_id)
dag.clear(run_id=run_id)


Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,15 +29,15 @@
import attrs
import structlog
from cadwyn import VersionedAPIRouter
from fastapi import Body, HTTPException, Query, status
from fastapi import Body, Depends, HTTPException, Query, status
from pydantic import JsonValue
from sqlalchemy import func, or_, tuple_, update
from sqlalchemy.exc import NoResultFound, SQLAlchemyError
from sqlalchemy.orm import joinedload
from sqlalchemy.sql import select
from structlog.contextvars import bind_contextvars

from airflow.api_fastapi.common.dagbag import DagBagDep
from airflow.api_fastapi.common.dagbag import dag_bag_from_app
from airflow.api_fastapi.common.db.common import SessionDep
from airflow.api_fastapi.common.types import UtcDateTime
from airflow.api_fastapi.execution_api.datamodels.taskinstance import (
Expand DownExpand Up@@ -76,6 +76,9 @@
from airflow.models.expandinput import SchedulerExpandInput
from airflow.sdk.types import Operator

from airflow.jobs.scheduler_job_runner import SchedulerDagBag

SchedulerDagBagDep = Annotated[SchedulerDagBag, Depends(dag_bag_from_app)]
Comment thread
jason810496 marked this conversation as resolved.

router = VersionedAPIRouter()

Expand DownExpand Up@@ -104,7 +107,7 @@ def ti_run(
task_instance_id: UUID,
ti_run_payload: Annotated[TIEnterRunningPayload, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> TIRunContext:
"""
Run a TaskInstance.
Expand DownExpand Up@@ -255,7 +258,7 @@ def ti_run(
or 0
)

if dag := dag_bag.get_dag(ti.dag_id):
if dag := dag_bag.get_dag(dag_run=dr, session=session):
upstream_map_indexes = dict(
_get_upstream_map_indexes(dag.get_task(ti.task_id), ti.map_index, ti.run_id, session)
)
Expand DownExpand Up@@ -330,7 +333,7 @@ def ti_update_state(
task_instance_id: UUID,
ti_patch_payload: Annotated[TIStateUpdate, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
):
"""
Update the state of a TaskInstance.
Expand DownExpand Up@@ -417,8 +420,9 @@ def ti_update_state(
)


def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: DagBagDep) -> None:
ser_dag = dag_bag.get_dag(dag_id)
def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: SchedulerDagBagDep) -> None:
dr = ti.dag_run
ser_dag = dag_bag.get_dag(dag_run=dr, session=session)
if ser_dag and getattr(ser_dag, "fail_fast", False):
task_dict = getattr(ser_dag, "task_dict")
task_teardown_map = {k: v.is_teardown for k, v in task_dict.items()}
Expand All@@ -432,7 +436,7 @@ def _create_ti_state_update_query_and_update_state(
query: Update,
updated_state,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
dag_id: str,
) -> tuple[Update, TaskInstanceState]:
if isinstance(ti_patch_payload, (TITerminalStatePayload, TIRetryStatePayload, TISuccessStatePayload)):
Expand DownExpand Up@@ -893,7 +897,7 @@ def _get_group_tasks(dag_id: str, task_group_id: str, session: SessionDep, logic
def validate_inlets_and_outlets(
task_instance_id: UUID,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> InactiveAssetsResponse:
"""Validate whether there're inactive assets in inlets and outlets of a given task instance."""
ti_id_str = str(task_instance_id)
Expand All@@ -911,7 +915,8 @@ def validate_inlets_and_outlets(
)

if not ti.task:
dag = dag_bag.get_dag(ti.dag_id)
dr = ti.dag_run
dag = dag_bag.get_dag(dag_run=dr, session=session)
if dag:
with contextlib.suppress(TaskNotFound):
ti.task = dag.get_task(ti.task_id)
Expand Down
9 changes: 9 additions & 0 deletions airflow-core/src/airflow/cli/cli_config.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -340,6 +340,14 @@ def string_lower_type(val):
),
choices=("none", "completed", "failed"),
)
ARG_BACKFILL_RUN_ON_LATEST_VERSION = Arg(
("--run-on-latest-version",),
help=(
"(Experimental) If set, the backfill will run tasks using the latest bundle version instead of "
"the version that was active when the original Dag run was created."
),
action="store_true",
)


# misc
Expand DownExpand Up@@ -968,6 +976,7 @@ class GroupCommand(NamedTuple):
ARG_RUN_BACKWARDS,
ARG_MAX_ACTIVE_RUNS,
ARG_BACKFILL_REPROCESS_BEHAVIOR,
ARG_BACKFILL_RUN_ON_LATEST_VERSION,
ARG_BACKFILL_DRY_RUN,
),
),
Expand Down
2 changes: 2 additions & 0 deletions airflow-core/src/airflow/cli/commands/backfill_command.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ def create_backfill(args) -> None:
reverse=args.run_backwards,
dag_run_conf=args.dag_run_conf,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
for k, v in params.items():
console.print(f" - {k} = {v}")
Expand DownExpand Up@@ -88,4 +89,5 @@ def create_backfill(args) -> None:
dag_run_conf=args.dag_run_conf,
triggering_user_name=user,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
15 changes: 8 additions & 7 deletions airflow-core/src/airflow/jobs/scheduler_job_runner.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,15 +131,16 @@ def _get_dag(self, version_id: str, session: Session) -> DAG | None:
return dag

@staticmethod
def _version_from_dag_run(dag_run, session):
if dag_run.bundle_version:
dag_version = dag_run.created_dag_version
else:
def _version_from_dag_run(dag_run, latest, session):
if latest or not dag_run.bundle_version:
dag_version = DagVersion.get_latest_version(dag_id=dag_run.dag_id, session=session)
return dag_version
if dag_version:
return dag_version

return dag_run.created_dag_version

def get_dag(self, dag_run: DagRun, session: Session) -> DAG | None:
version = self._version_from_dag_run(dag_run=dag_run, session=session)
def get_dag(self, dag_run: DagRun, session: Session, latest=False) -> DAG | None:
Comment thread
jedcunningham marked this conversation as resolved.
version = self._version_from_dag_run(dag_run=dag_run, latest=latest, session=session)
if not version:
return None
return self._get_dag(version_id=version.id, session=session)
Expand Down
7 changes: 6 additions & 1 deletion airflow-core/src/airflow/models/backfill.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -291,6 +291,7 @@ def _create_backfill_dag_run(
dag_run_conf,
backfill_sort_ordinal,
triggering_user_name,
run_on_latest_version,
session,
):
from airflow.models.dagrun import DagRun
Expand DownExpand Up@@ -328,6 +329,7 @@ def _create_backfill_dag_run(
info=info,
backfill_id=backfill_id,
sort_ordinal=backfill_sort_ordinal,
run_on_latest=run_on_latest_version,
)
else:
session.add(
Expand DownExpand Up@@ -401,7 +403,7 @@ def _get_info_list(
return dagrun_info_list


def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal, run_on_latest=False):
"""Clear the existing DAG run and update backfill metadata."""
from sqlalchemy.sql import update

Expand All@@ -415,6 +417,7 @@ def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
session=session,
confirm_prompt=False,
dry_run=False,
run_on_latest_version=run_on_latest,
)

# Update backfill_id and run_type in DagRun table
Expand DownExpand Up@@ -447,6 +450,7 @@ def _create_backfill(
dag_run_conf: dict | None,
triggering_user_name: str | None,
reprocess_behavior: ReprocessBehavior | None = None,
run_on_latest_version: bool = False,
) -> Backfill | None:
from airflow.models import DagModel
from airflow.models.serialized_dag import SerializedDagModel
Expand DownExpand Up@@ -510,6 +514,7 @@ def _create_backfill(
reprocess_behavior=br.reprocess_behavior,
backfill_sort_ordinal=backfill_sort_ordinal,
triggering_user_name=br.triggering_user_name,
run_on_latest_version=run_on_latest_version,
session=session,
)
log.info(
Expand Down
7 changes: 7 additions & 0 deletions airflow-core/src/airflow/models/dag.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1282,6 +1282,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1299,6 +1300,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1317,6 +1319,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1335,6 +1338,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1354,6 +1358,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: bool = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1372,6 +1377,7 @@ def clear(
:param dag_run_state: state to set DagRun to. If set to False, dagrun state will not
be changed.
:param dry_run: Find the tasks to clear but don't clear them.
:param run_on_latest_version: whether to run on latest serialized DAG and Bundle version
:param session: The sqlalchemy session to use
:param dag_bag: The DagBag used to find the dags (Optional)
:param exclude_task_ids: A set of ``task_id`` or (``task_id``, ``map_index``)
Expand DownExpand Up@@ -1417,6 +1423,7 @@ def clear(
list(tis),
session,
dag_run_state=dag_run_state,
run_on_latest_version=run_on_latest_version,
)
else:
count = 0
Expand Down
Loading
, '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('^' + ".*" + ' Add run_on_latest_version support for backfill and clear operations by ephraimbuddy · Pull Request #52177 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions airflow-core/docs/core-concepts/dag-run.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,6 +167,8 @@ the errors after going through the logs, you can re-run the tasks by clearing th
scheduled date. Clearing a task instance creates a record of the task instance.
The ``try_number`` of the current task instance is incremented, the ``max_tries`` set to ``0`` and the state set to ``None``, which causes the task to re-run.

An experimental feature in Airflow 3.1.0 allows you to clear the task instances and re-run with the latest bundle version.

Click on the failed task in the Tree or Graph views and then click on **Clear**.
The executor will re-run it.

Expand Down
4 changes: 3 additions & 1 deletion airflow-core/src/airflow/api_fastapi/app.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -84,8 +84,10 @@ def create_app(apps: str = "all") -> FastAPI:
dag_bag = create_dag_bag()
Comment thread
jason810496 marked this conversation as resolved.

if "execution" in apps_list or "all" in apps_list:
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

task_exec_api_app = create_task_execution_api_app()
task_exec_api_app.state.dag_bag = dag_bag
task_exec_api_app.state.dag_bag = SchedulerDagBag()
init_error_handlers(task_exec_api_app)
Comment thread
jason810496 marked this conversation as resolved.
app.mount("/execution", task_exec_api_app)

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,6 +55,10 @@ class DAGRunClearBody(StrictBaseModel):

dry_run: bool = True
only_failed: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after clearing the DAG Run.",
)


class DAGRunResponse(BaseModel):
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,11 @@ class ClearTaskInstancesBody(StrictBaseModel):
include_downstream: bool = False
include_future: bool = False
include_past: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after "
"clearing the task instances.",
)

@model_validator(mode="before")
@classmethod
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8396,6 +8396,12 @@ components:
type: boolean
title: Include Past
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
Comment thread
ephraimbuddy marked this conversation as resolved.
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the task instances.
default: false
additionalProperties: false
type: object
title: ClearTaskInstancesBody
Expand DownExpand Up@@ -9049,6 +9055,12 @@ components:
type: boolean
title: Only Failed
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the DAG Run.
default: false
additionalProperties: false
type: object
title: DAGRunClearBody
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,6 +281,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
dry_run=True,
session=session,
)
Expand All@@ -293,6 +294,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
session=session,
)
dag_run_cleared = session.scalar(select(DagRun).where(DagRun.id == dag_run.id))
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -675,7 +675,11 @@ def post_clear_task_instances(
if dag_run is None:
error_message = f"Dag Run id {dag_run_id} not found in dag {dag_id}"
raise HTTPException(status.HTTP_404_NOT_FOUND, error_message)
# If dag_run_id is provided, we should get the dag from SchedulerDagBag
# to ensure we get the right version.
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag = SchedulerDagBag().get_dag(dag_run=dag_run, session=session)
if past or future:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
Expand DownExpand Up@@ -724,6 +728,7 @@ def post_clear_task_instances(
task_instances,
session,
DagRunState.QUEUED if reset_dag_runs else False,
run_on_latest_version=body.run_on_latest_version,
)

return TaskInstanceCollectionResponse(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,6 @@
from airflow.api_fastapi.execution_api.datamodels.dagrun import DagRunStateResponse, TriggerDAGRunPayload
from airflow.exceptions import DagRunAlreadyExists
from airflow.models.dag import DagModel
from airflow.models.dagbag import DagBag
from airflow.models.dagrun import DagRun
from airflow.utils.types import DagRunTriggeredByType

Expand DownExpand Up@@ -122,9 +121,20 @@ def clear_dag_run(
"message": f"DAG with dag_id: '{dag_id}' has import errors and cannot be triggered",
},
)
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag_run = session.scalar(select(DagRun).where(DagRun.dag_id == dag_id, DagRun.run_id == run_id))
dag_bag = SchedulerDagBag()
Comment thread
jason810496 marked this conversation as resolved.
Comment thread
jedcunningham marked this conversation as resolved.
dag = dag_bag.get_dag(dag_run=dag_run, session=session)
if not dag:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail={
"reason": "Not Found",
"message": f"DAG with dag_id: '{dag_id}' was not found in the DagBag",
},
)

dag_bag = DagBag(dag_folder=dm.fileloc, read_dags_from_db=True)
dag = dag_bag.get_dag(dag_id)
dag.clear(run_id=run_id)


Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,15 +29,15 @@
import attrs
import structlog
from cadwyn import VersionedAPIRouter
from fastapi import Body, HTTPException, Query, status
from fastapi import Body, Depends, HTTPException, Query, status
from pydantic import JsonValue
from sqlalchemy import func, or_, tuple_, update
from sqlalchemy.exc import NoResultFound, SQLAlchemyError
from sqlalchemy.orm import joinedload
from sqlalchemy.sql import select
from structlog.contextvars import bind_contextvars

from airflow.api_fastapi.common.dagbag import DagBagDep
from airflow.api_fastapi.common.dagbag import dag_bag_from_app
from airflow.api_fastapi.common.db.common import SessionDep
from airflow.api_fastapi.common.types import UtcDateTime
from airflow.api_fastapi.execution_api.datamodels.taskinstance import (
Expand DownExpand Up@@ -76,6 +76,9 @@
from airflow.models.expandinput import SchedulerExpandInput
from airflow.sdk.types import Operator

from airflow.jobs.scheduler_job_runner import SchedulerDagBag

SchedulerDagBagDep = Annotated[SchedulerDagBag, Depends(dag_bag_from_app)]
Comment thread
jason810496 marked this conversation as resolved.

router = VersionedAPIRouter()

Expand DownExpand Up@@ -104,7 +107,7 @@ def ti_run(
task_instance_id: UUID,
ti_run_payload: Annotated[TIEnterRunningPayload, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> TIRunContext:
"""
Run a TaskInstance.
Expand DownExpand Up@@ -255,7 +258,7 @@ def ti_run(
or 0
)

if dag := dag_bag.get_dag(ti.dag_id):
if dag := dag_bag.get_dag(dag_run=dr, session=session):
upstream_map_indexes = dict(
_get_upstream_map_indexes(dag.get_task(ti.task_id), ti.map_index, ti.run_id, session)
)
Expand DownExpand Up@@ -330,7 +333,7 @@ def ti_update_state(
task_instance_id: UUID,
ti_patch_payload: Annotated[TIStateUpdate, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
):
"""
Update the state of a TaskInstance.
Expand DownExpand Up@@ -417,8 +420,9 @@ def ti_update_state(
)


def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: DagBagDep) -> None:
ser_dag = dag_bag.get_dag(dag_id)
def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: SchedulerDagBagDep) -> None:
dr = ti.dag_run
ser_dag = dag_bag.get_dag(dag_run=dr, session=session)
if ser_dag and getattr(ser_dag, "fail_fast", False):
task_dict = getattr(ser_dag, "task_dict")
task_teardown_map = {k: v.is_teardown for k, v in task_dict.items()}
Expand All@@ -432,7 +436,7 @@ def _create_ti_state_update_query_and_update_state(
query: Update,
updated_state,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
dag_id: str,
) -> tuple[Update, TaskInstanceState]:
if isinstance(ti_patch_payload, (TITerminalStatePayload, TIRetryStatePayload, TISuccessStatePayload)):
Expand DownExpand Up@@ -893,7 +897,7 @@ def _get_group_tasks(dag_id: str, task_group_id: str, session: SessionDep, logic
def validate_inlets_and_outlets(
task_instance_id: UUID,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> InactiveAssetsResponse:
"""Validate whether there're inactive assets in inlets and outlets of a given task instance."""
ti_id_str = str(task_instance_id)
Expand All@@ -911,7 +915,8 @@ def validate_inlets_and_outlets(
)

if not ti.task:
dag = dag_bag.get_dag(ti.dag_id)
dr = ti.dag_run
dag = dag_bag.get_dag(dag_run=dr, session=session)
if dag:
with contextlib.suppress(TaskNotFound):
ti.task = dag.get_task(ti.task_id)
Expand Down
9 changes: 9 additions & 0 deletions airflow-core/src/airflow/cli/cli_config.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -340,6 +340,14 @@ def string_lower_type(val):
),
choices=("none", "completed", "failed"),
)
ARG_BACKFILL_RUN_ON_LATEST_VERSION = Arg(
("--run-on-latest-version",),
help=(
"(Experimental) If set, the backfill will run tasks using the latest bundle version instead of "
"the version that was active when the original Dag run was created."
),
action="store_true",
)


# misc
Expand DownExpand Up@@ -968,6 +976,7 @@ class GroupCommand(NamedTuple):
ARG_RUN_BACKWARDS,
ARG_MAX_ACTIVE_RUNS,
ARG_BACKFILL_REPROCESS_BEHAVIOR,
ARG_BACKFILL_RUN_ON_LATEST_VERSION,
ARG_BACKFILL_DRY_RUN,
),
),
Expand Down
2 changes: 2 additions & 0 deletions airflow-core/src/airflow/cli/commands/backfill_command.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ def create_backfill(args) -> None:
reverse=args.run_backwards,
dag_run_conf=args.dag_run_conf,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
for k, v in params.items():
console.print(f" - {k} = {v}")
Expand DownExpand Up@@ -88,4 +89,5 @@ def create_backfill(args) -> None:
dag_run_conf=args.dag_run_conf,
triggering_user_name=user,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
15 changes: 8 additions & 7 deletions airflow-core/src/airflow/jobs/scheduler_job_runner.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,15 +131,16 @@ def _get_dag(self, version_id: str, session: Session) -> DAG | None:
return dag

@staticmethod
def _version_from_dag_run(dag_run, session):
if dag_run.bundle_version:
dag_version = dag_run.created_dag_version
else:
def _version_from_dag_run(dag_run, latest, session):
if latest or not dag_run.bundle_version:
dag_version = DagVersion.get_latest_version(dag_id=dag_run.dag_id, session=session)
return dag_version
if dag_version:
return dag_version

return dag_run.created_dag_version

def get_dag(self, dag_run: DagRun, session: Session) -> DAG | None:
version = self._version_from_dag_run(dag_run=dag_run, session=session)
def get_dag(self, dag_run: DagRun, session: Session, latest=False) -> DAG | None:
Comment thread
jedcunningham marked this conversation as resolved.
version = self._version_from_dag_run(dag_run=dag_run, latest=latest, session=session)
if not version:
return None
return self._get_dag(version_id=version.id, session=session)
Expand Down
7 changes: 6 additions & 1 deletion airflow-core/src/airflow/models/backfill.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -291,6 +291,7 @@ def _create_backfill_dag_run(
dag_run_conf,
backfill_sort_ordinal,
triggering_user_name,
run_on_latest_version,
session,
):
from airflow.models.dagrun import DagRun
Expand DownExpand Up@@ -328,6 +329,7 @@ def _create_backfill_dag_run(
info=info,
backfill_id=backfill_id,
sort_ordinal=backfill_sort_ordinal,
run_on_latest=run_on_latest_version,
)
else:
session.add(
Expand DownExpand Up@@ -401,7 +403,7 @@ def _get_info_list(
return dagrun_info_list


def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal, run_on_latest=False):
"""Clear the existing DAG run and update backfill metadata."""
from sqlalchemy.sql import update

Expand All@@ -415,6 +417,7 @@ def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
session=session,
confirm_prompt=False,
dry_run=False,
run_on_latest_version=run_on_latest,
)

# Update backfill_id and run_type in DagRun table
Expand DownExpand Up@@ -447,6 +450,7 @@ def _create_backfill(
dag_run_conf: dict | None,
triggering_user_name: str | None,
reprocess_behavior: ReprocessBehavior | None = None,
run_on_latest_version: bool = False,
) -> Backfill | None:
from airflow.models import DagModel
from airflow.models.serialized_dag import SerializedDagModel
Expand DownExpand Up@@ -510,6 +514,7 @@ def _create_backfill(
reprocess_behavior=br.reprocess_behavior,
backfill_sort_ordinal=backfill_sort_ordinal,
triggering_user_name=br.triggering_user_name,
run_on_latest_version=run_on_latest_version,
session=session,
)
log.info(
Expand Down
7 changes: 7 additions & 0 deletions airflow-core/src/airflow/models/dag.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1282,6 +1282,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1299,6 +1300,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1317,6 +1319,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1335,6 +1338,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1354,6 +1358,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: bool = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1372,6 +1377,7 @@ def clear(
:param dag_run_state: state to set DagRun to. If set to False, dagrun state will not
be changed.
:param dry_run: Find the tasks to clear but don't clear them.
:param run_on_latest_version: whether to run on latest serialized DAG and Bundle version
:param session: The sqlalchemy session to use
:param dag_bag: The DagBag used to find the dags (Optional)
:param exclude_task_ids: A set of ``task_id`` or (``task_id``, ``map_index``)
Expand DownExpand Up@@ -1417,6 +1423,7 @@ def clear(
list(tis),
session,
dag_run_state=dag_run_state,
run_on_latest_version=run_on_latest_version,
)
else:
count = 0
Expand Down
Loading
, '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('^' + ".*" + ' Add run_on_latest_version support for backfill and clear operations by ephraimbuddy · Pull Request #52177 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions airflow-core/docs/core-concepts/dag-run.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,6 +167,8 @@ the errors after going through the logs, you can re-run the tasks by clearing th
scheduled date. Clearing a task instance creates a record of the task instance.
The ``try_number`` of the current task instance is incremented, the ``max_tries`` set to ``0`` and the state set to ``None``, which causes the task to re-run.

An experimental feature in Airflow 3.1.0 allows you to clear the task instances and re-run with the latest bundle version.

Click on the failed task in the Tree or Graph views and then click on **Clear**.
The executor will re-run it.

Expand Down
4 changes: 3 additions & 1 deletion airflow-core/src/airflow/api_fastapi/app.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -84,8 +84,10 @@ def create_app(apps: str = "all") -> FastAPI:
dag_bag = create_dag_bag()
Comment thread
jason810496 marked this conversation as resolved.

if "execution" in apps_list or "all" in apps_list:
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

task_exec_api_app = create_task_execution_api_app()
task_exec_api_app.state.dag_bag = dag_bag
task_exec_api_app.state.dag_bag = SchedulerDagBag()
init_error_handlers(task_exec_api_app)
Comment thread
jason810496 marked this conversation as resolved.
app.mount("/execution", task_exec_api_app)

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,6 +55,10 @@ class DAGRunClearBody(StrictBaseModel):

dry_run: bool = True
only_failed: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after clearing the DAG Run.",
)


class DAGRunResponse(BaseModel):
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,11 @@ class ClearTaskInstancesBody(StrictBaseModel):
include_downstream: bool = False
include_future: bool = False
include_past: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after "
"clearing the task instances.",
)

@model_validator(mode="before")
@classmethod
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8396,6 +8396,12 @@ components:
type: boolean
title: Include Past
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
Comment thread
ephraimbuddy marked this conversation as resolved.
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the task instances.
default: false
additionalProperties: false
type: object
title: ClearTaskInstancesBody
Expand DownExpand Up@@ -9049,6 +9055,12 @@ components:
type: boolean
title: Only Failed
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the DAG Run.
default: false
additionalProperties: false
type: object
title: DAGRunClearBody
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,6 +281,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
dry_run=True,
session=session,
)
Expand All@@ -293,6 +294,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
session=session,
)
dag_run_cleared = session.scalar(select(DagRun).where(DagRun.id == dag_run.id))
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -675,7 +675,11 @@ def post_clear_task_instances(
if dag_run is None:
error_message = f"Dag Run id {dag_run_id} not found in dag {dag_id}"
raise HTTPException(status.HTTP_404_NOT_FOUND, error_message)
# If dag_run_id is provided, we should get the dag from SchedulerDagBag
# to ensure we get the right version.
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag = SchedulerDagBag().get_dag(dag_run=dag_run, session=session)
if past or future:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
Expand DownExpand Up@@ -724,6 +728,7 @@ def post_clear_task_instances(
task_instances,
session,
DagRunState.QUEUED if reset_dag_runs else False,
run_on_latest_version=body.run_on_latest_version,
)

return TaskInstanceCollectionResponse(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,6 @@
from airflow.api_fastapi.execution_api.datamodels.dagrun import DagRunStateResponse, TriggerDAGRunPayload
from airflow.exceptions import DagRunAlreadyExists
from airflow.models.dag import DagModel
from airflow.models.dagbag import DagBag
from airflow.models.dagrun import DagRun
from airflow.utils.types import DagRunTriggeredByType

Expand DownExpand Up@@ -122,9 +121,20 @@ def clear_dag_run(
"message": f"DAG with dag_id: '{dag_id}' has import errors and cannot be triggered",
},
)
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag_run = session.scalar(select(DagRun).where(DagRun.dag_id == dag_id, DagRun.run_id == run_id))
dag_bag = SchedulerDagBag()
Comment thread
jason810496 marked this conversation as resolved.
Comment thread
jedcunningham marked this conversation as resolved.
dag = dag_bag.get_dag(dag_run=dag_run, session=session)
if not dag:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail={
"reason": "Not Found",
"message": f"DAG with dag_id: '{dag_id}' was not found in the DagBag",
},
)

dag_bag = DagBag(dag_folder=dm.fileloc, read_dags_from_db=True)
dag = dag_bag.get_dag(dag_id)
dag.clear(run_id=run_id)


Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,15 +29,15 @@
import attrs
import structlog
from cadwyn import VersionedAPIRouter
from fastapi import Body, HTTPException, Query, status
from fastapi import Body, Depends, HTTPException, Query, status
from pydantic import JsonValue
from sqlalchemy import func, or_, tuple_, update
from sqlalchemy.exc import NoResultFound, SQLAlchemyError
from sqlalchemy.orm import joinedload
from sqlalchemy.sql import select
from structlog.contextvars import bind_contextvars

from airflow.api_fastapi.common.dagbag import DagBagDep
from airflow.api_fastapi.common.dagbag import dag_bag_from_app
from airflow.api_fastapi.common.db.common import SessionDep
from airflow.api_fastapi.common.types import UtcDateTime
from airflow.api_fastapi.execution_api.datamodels.taskinstance import (
Expand DownExpand Up@@ -76,6 +76,9 @@
from airflow.models.expandinput import SchedulerExpandInput
from airflow.sdk.types import Operator

from airflow.jobs.scheduler_job_runner import SchedulerDagBag

SchedulerDagBagDep = Annotated[SchedulerDagBag, Depends(dag_bag_from_app)]
Comment thread
jason810496 marked this conversation as resolved.

router = VersionedAPIRouter()

Expand DownExpand Up@@ -104,7 +107,7 @@ def ti_run(
task_instance_id: UUID,
ti_run_payload: Annotated[TIEnterRunningPayload, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> TIRunContext:
"""
Run a TaskInstance.
Expand DownExpand Up@@ -255,7 +258,7 @@ def ti_run(
or 0
)

if dag := dag_bag.get_dag(ti.dag_id):
if dag := dag_bag.get_dag(dag_run=dr, session=session):
upstream_map_indexes = dict(
_get_upstream_map_indexes(dag.get_task(ti.task_id), ti.map_index, ti.run_id, session)
)
Expand DownExpand Up@@ -330,7 +333,7 @@ def ti_update_state(
task_instance_id: UUID,
ti_patch_payload: Annotated[TIStateUpdate, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
):
"""
Update the state of a TaskInstance.
Expand DownExpand Up@@ -417,8 +420,9 @@ def ti_update_state(
)


def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: DagBagDep) -> None:
ser_dag = dag_bag.get_dag(dag_id)
def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: SchedulerDagBagDep) -> None:
dr = ti.dag_run
ser_dag = dag_bag.get_dag(dag_run=dr, session=session)
if ser_dag and getattr(ser_dag, "fail_fast", False):
task_dict = getattr(ser_dag, "task_dict")
task_teardown_map = {k: v.is_teardown for k, v in task_dict.items()}
Expand All@@ -432,7 +436,7 @@ def _create_ti_state_update_query_and_update_state(
query: Update,
updated_state,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
dag_id: str,
) -> tuple[Update, TaskInstanceState]:
if isinstance(ti_patch_payload, (TITerminalStatePayload, TIRetryStatePayload, TISuccessStatePayload)):
Expand DownExpand Up@@ -893,7 +897,7 @@ def _get_group_tasks(dag_id: str, task_group_id: str, session: SessionDep, logic
def validate_inlets_and_outlets(
task_instance_id: UUID,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> InactiveAssetsResponse:
"""Validate whether there're inactive assets in inlets and outlets of a given task instance."""
ti_id_str = str(task_instance_id)
Expand All@@ -911,7 +915,8 @@ def validate_inlets_and_outlets(
)

if not ti.task:
dag = dag_bag.get_dag(ti.dag_id)
dr = ti.dag_run
dag = dag_bag.get_dag(dag_run=dr, session=session)
if dag:
with contextlib.suppress(TaskNotFound):
ti.task = dag.get_task(ti.task_id)
Expand Down
9 changes: 9 additions & 0 deletions airflow-core/src/airflow/cli/cli_config.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -340,6 +340,14 @@ def string_lower_type(val):
),
choices=("none", "completed", "failed"),
)
ARG_BACKFILL_RUN_ON_LATEST_VERSION = Arg(
("--run-on-latest-version",),
help=(
"(Experimental) If set, the backfill will run tasks using the latest bundle version instead of "
"the version that was active when the original Dag run was created."
),
action="store_true",
)


# misc
Expand DownExpand Up@@ -968,6 +976,7 @@ class GroupCommand(NamedTuple):
ARG_RUN_BACKWARDS,
ARG_MAX_ACTIVE_RUNS,
ARG_BACKFILL_REPROCESS_BEHAVIOR,
ARG_BACKFILL_RUN_ON_LATEST_VERSION,
ARG_BACKFILL_DRY_RUN,
),
),
Expand Down
2 changes: 2 additions & 0 deletions airflow-core/src/airflow/cli/commands/backfill_command.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ def create_backfill(args) -> None:
reverse=args.run_backwards,
dag_run_conf=args.dag_run_conf,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
for k, v in params.items():
console.print(f" - {k} = {v}")
Expand DownExpand Up@@ -88,4 +89,5 @@ def create_backfill(args) -> None:
dag_run_conf=args.dag_run_conf,
triggering_user_name=user,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
15 changes: 8 additions & 7 deletions airflow-core/src/airflow/jobs/scheduler_job_runner.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,15 +131,16 @@ def _get_dag(self, version_id: str, session: Session) -> DAG | None:
return dag

@staticmethod
def _version_from_dag_run(dag_run, session):
if dag_run.bundle_version:
dag_version = dag_run.created_dag_version
else:
def _version_from_dag_run(dag_run, latest, session):
if latest or not dag_run.bundle_version:
dag_version = DagVersion.get_latest_version(dag_id=dag_run.dag_id, session=session)
return dag_version
if dag_version:
return dag_version

return dag_run.created_dag_version

def get_dag(self, dag_run: DagRun, session: Session) -> DAG | None:
version = self._version_from_dag_run(dag_run=dag_run, session=session)
def get_dag(self, dag_run: DagRun, session: Session, latest=False) -> DAG | None:
Comment thread
jedcunningham marked this conversation as resolved.
version = self._version_from_dag_run(dag_run=dag_run, latest=latest, session=session)
if not version:
return None
return self._get_dag(version_id=version.id, session=session)
Expand Down
7 changes: 6 additions & 1 deletion airflow-core/src/airflow/models/backfill.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -291,6 +291,7 @@ def _create_backfill_dag_run(
dag_run_conf,
backfill_sort_ordinal,
triggering_user_name,
run_on_latest_version,
session,
):
from airflow.models.dagrun import DagRun
Expand DownExpand Up@@ -328,6 +329,7 @@ def _create_backfill_dag_run(
info=info,
backfill_id=backfill_id,
sort_ordinal=backfill_sort_ordinal,
run_on_latest=run_on_latest_version,
)
else:
session.add(
Expand DownExpand Up@@ -401,7 +403,7 @@ def _get_info_list(
return dagrun_info_list


def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal, run_on_latest=False):
"""Clear the existing DAG run and update backfill metadata."""
from sqlalchemy.sql import update

Expand All@@ -415,6 +417,7 @@ def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
session=session,
confirm_prompt=False,
dry_run=False,
run_on_latest_version=run_on_latest,
)

# Update backfill_id and run_type in DagRun table
Expand DownExpand Up@@ -447,6 +450,7 @@ def _create_backfill(
dag_run_conf: dict | None,
triggering_user_name: str | None,
reprocess_behavior: ReprocessBehavior | None = None,
run_on_latest_version: bool = False,
) -> Backfill | None:
from airflow.models import DagModel
from airflow.models.serialized_dag import SerializedDagModel
Expand DownExpand Up@@ -510,6 +514,7 @@ def _create_backfill(
reprocess_behavior=br.reprocess_behavior,
backfill_sort_ordinal=backfill_sort_ordinal,
triggering_user_name=br.triggering_user_name,
run_on_latest_version=run_on_latest_version,
session=session,
)
log.info(
Expand Down
7 changes: 7 additions & 0 deletions airflow-core/src/airflow/models/dag.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1282,6 +1282,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1299,6 +1300,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1317,6 +1319,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1335,6 +1338,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1354,6 +1358,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: bool = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1372,6 +1377,7 @@ def clear(
:param dag_run_state: state to set DagRun to. If set to False, dagrun state will not
be changed.
:param dry_run: Find the tasks to clear but don't clear them.
:param run_on_latest_version: whether to run on latest serialized DAG and Bundle version
:param session: The sqlalchemy session to use
:param dag_bag: The DagBag used to find the dags (Optional)
:param exclude_task_ids: A set of ``task_id`` or (``task_id``, ``map_index``)
Expand DownExpand Up@@ -1417,6 +1423,7 @@ def clear(
list(tis),
session,
dag_run_state=dag_run_state,
run_on_latest_version=run_on_latest_version,
)
else:
count = 0
Expand Down
Loading
, '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" + ' Add run_on_latest_version support for backfill and clear operations by ephraimbuddy · Pull Request #52177 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions airflow-core/docs/core-concepts/dag-run.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,6 +167,8 @@ the errors after going through the logs, you can re-run the tasks by clearing th
scheduled date. Clearing a task instance creates a record of the task instance.
The ``try_number`` of the current task instance is incremented, the ``max_tries`` set to ``0`` and the state set to ``None``, which causes the task to re-run.

An experimental feature in Airflow 3.1.0 allows you to clear the task instances and re-run with the latest bundle version.

Click on the failed task in the Tree or Graph views and then click on **Clear**.
The executor will re-run it.

Expand Down
4 changes: 3 additions & 1 deletion airflow-core/src/airflow/api_fastapi/app.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -84,8 +84,10 @@ def create_app(apps: str = "all") -> FastAPI:
dag_bag = create_dag_bag()
Comment thread
jason810496 marked this conversation as resolved.

if "execution" in apps_list or "all" in apps_list:
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

task_exec_api_app = create_task_execution_api_app()
task_exec_api_app.state.dag_bag = dag_bag
task_exec_api_app.state.dag_bag = SchedulerDagBag()
init_error_handlers(task_exec_api_app)
Comment thread
jason810496 marked this conversation as resolved.
app.mount("/execution", task_exec_api_app)

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,6 +55,10 @@ class DAGRunClearBody(StrictBaseModel):

dry_run: bool = True
only_failed: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after clearing the DAG Run.",
)


class DAGRunResponse(BaseModel):
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,11 @@ class ClearTaskInstancesBody(StrictBaseModel):
include_downstream: bool = False
include_future: bool = False
include_past: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after "
"clearing the task instances.",
)

@model_validator(mode="before")
@classmethod
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8396,6 +8396,12 @@ components:
type: boolean
title: Include Past
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
Comment thread
ephraimbuddy marked this conversation as resolved.
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the task instances.
default: false
additionalProperties: false
type: object
title: ClearTaskInstancesBody
Expand DownExpand Up@@ -9049,6 +9055,12 @@ components:
type: boolean
title: Only Failed
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the DAG Run.
default: false
additionalProperties: false
type: object
title: DAGRunClearBody
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,6 +281,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
dry_run=True,
session=session,
)
Expand All@@ -293,6 +294,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
session=session,
)
dag_run_cleared = session.scalar(select(DagRun).where(DagRun.id == dag_run.id))
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -675,7 +675,11 @@ def post_clear_task_instances(
if dag_run is None:
error_message = f"Dag Run id {dag_run_id} not found in dag {dag_id}"
raise HTTPException(status.HTTP_404_NOT_FOUND, error_message)
# If dag_run_id is provided, we should get the dag from SchedulerDagBag
# to ensure we get the right version.
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag = SchedulerDagBag().get_dag(dag_run=dag_run, session=session)
if past or future:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
Expand DownExpand Up@@ -724,6 +728,7 @@ def post_clear_task_instances(
task_instances,
session,
DagRunState.QUEUED if reset_dag_runs else False,
run_on_latest_version=body.run_on_latest_version,
)

return TaskInstanceCollectionResponse(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,6 @@
from airflow.api_fastapi.execution_api.datamodels.dagrun import DagRunStateResponse, TriggerDAGRunPayload
from airflow.exceptions import DagRunAlreadyExists
from airflow.models.dag import DagModel
from airflow.models.dagbag import DagBag
from airflow.models.dagrun import DagRun
from airflow.utils.types import DagRunTriggeredByType

Expand DownExpand Up@@ -122,9 +121,20 @@ def clear_dag_run(
"message": f"DAG with dag_id: '{dag_id}' has import errors and cannot be triggered",
},
)
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag_run = session.scalar(select(DagRun).where(DagRun.dag_id == dag_id, DagRun.run_id == run_id))
dag_bag = SchedulerDagBag()
Comment thread
jason810496 marked this conversation as resolved.
Comment thread
jedcunningham marked this conversation as resolved.
dag = dag_bag.get_dag(dag_run=dag_run, session=session)
if not dag:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail={
"reason": "Not Found",
"message": f"DAG with dag_id: '{dag_id}' was not found in the DagBag",
},
)

dag_bag = DagBag(dag_folder=dm.fileloc, read_dags_from_db=True)
dag = dag_bag.get_dag(dag_id)
dag.clear(run_id=run_id)


Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,15 +29,15 @@
import attrs
import structlog
from cadwyn import VersionedAPIRouter
from fastapi import Body, HTTPException, Query, status
from fastapi import Body, Depends, HTTPException, Query, status
from pydantic import JsonValue
from sqlalchemy import func, or_, tuple_, update
from sqlalchemy.exc import NoResultFound, SQLAlchemyError
from sqlalchemy.orm import joinedload
from sqlalchemy.sql import select
from structlog.contextvars import bind_contextvars

from airflow.api_fastapi.common.dagbag import DagBagDep
from airflow.api_fastapi.common.dagbag import dag_bag_from_app
from airflow.api_fastapi.common.db.common import SessionDep
from airflow.api_fastapi.common.types import UtcDateTime
from airflow.api_fastapi.execution_api.datamodels.taskinstance import (
Expand DownExpand Up@@ -76,6 +76,9 @@
from airflow.models.expandinput import SchedulerExpandInput
from airflow.sdk.types import Operator

from airflow.jobs.scheduler_job_runner import SchedulerDagBag

SchedulerDagBagDep = Annotated[SchedulerDagBag, Depends(dag_bag_from_app)]
Comment thread
jason810496 marked this conversation as resolved.

router = VersionedAPIRouter()

Expand DownExpand Up@@ -104,7 +107,7 @@ def ti_run(
task_instance_id: UUID,
ti_run_payload: Annotated[TIEnterRunningPayload, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> TIRunContext:
"""
Run a TaskInstance.
Expand DownExpand Up@@ -255,7 +258,7 @@ def ti_run(
or 0
)

if dag := dag_bag.get_dag(ti.dag_id):
if dag := dag_bag.get_dag(dag_run=dr, session=session):
upstream_map_indexes = dict(
_get_upstream_map_indexes(dag.get_task(ti.task_id), ti.map_index, ti.run_id, session)
)
Expand DownExpand Up@@ -330,7 +333,7 @@ def ti_update_state(
task_instance_id: UUID,
ti_patch_payload: Annotated[TIStateUpdate, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
):
"""
Update the state of a TaskInstance.
Expand DownExpand Up@@ -417,8 +420,9 @@ def ti_update_state(
)


def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: DagBagDep) -> None:
ser_dag = dag_bag.get_dag(dag_id)
def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: SchedulerDagBagDep) -> None:
dr = ti.dag_run
ser_dag = dag_bag.get_dag(dag_run=dr, session=session)
if ser_dag and getattr(ser_dag, "fail_fast", False):
task_dict = getattr(ser_dag, "task_dict")
task_teardown_map = {k: v.is_teardown for k, v in task_dict.items()}
Expand All@@ -432,7 +436,7 @@ def _create_ti_state_update_query_and_update_state(
query: Update,
updated_state,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
dag_id: str,
) -> tuple[Update, TaskInstanceState]:
if isinstance(ti_patch_payload, (TITerminalStatePayload, TIRetryStatePayload, TISuccessStatePayload)):
Expand DownExpand Up@@ -893,7 +897,7 @@ def _get_group_tasks(dag_id: str, task_group_id: str, session: SessionDep, logic
def validate_inlets_and_outlets(
task_instance_id: UUID,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> InactiveAssetsResponse:
"""Validate whether there're inactive assets in inlets and outlets of a given task instance."""
ti_id_str = str(task_instance_id)
Expand All@@ -911,7 +915,8 @@ def validate_inlets_and_outlets(
)

if not ti.task:
dag = dag_bag.get_dag(ti.dag_id)
dr = ti.dag_run
dag = dag_bag.get_dag(dag_run=dr, session=session)
if dag:
with contextlib.suppress(TaskNotFound):
ti.task = dag.get_task(ti.task_id)
Expand Down
9 changes: 9 additions & 0 deletions airflow-core/src/airflow/cli/cli_config.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -340,6 +340,14 @@ def string_lower_type(val):
),
choices=("none", "completed", "failed"),
)
ARG_BACKFILL_RUN_ON_LATEST_VERSION = Arg(
("--run-on-latest-version",),
help=(
"(Experimental) If set, the backfill will run tasks using the latest bundle version instead of "
"the version that was active when the original Dag run was created."
),
action="store_true",
)


# misc
Expand DownExpand Up@@ -968,6 +976,7 @@ class GroupCommand(NamedTuple):
ARG_RUN_BACKWARDS,
ARG_MAX_ACTIVE_RUNS,
ARG_BACKFILL_REPROCESS_BEHAVIOR,
ARG_BACKFILL_RUN_ON_LATEST_VERSION,
ARG_BACKFILL_DRY_RUN,
),
),
Expand Down
2 changes: 2 additions & 0 deletions airflow-core/src/airflow/cli/commands/backfill_command.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ def create_backfill(args) -> None:
reverse=args.run_backwards,
dag_run_conf=args.dag_run_conf,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
for k, v in params.items():
console.print(f" - {k} = {v}")
Expand DownExpand Up@@ -88,4 +89,5 @@ def create_backfill(args) -> None:
dag_run_conf=args.dag_run_conf,
triggering_user_name=user,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
15 changes: 8 additions & 7 deletions airflow-core/src/airflow/jobs/scheduler_job_runner.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,15 +131,16 @@ def _get_dag(self, version_id: str, session: Session) -> DAG | None:
return dag

@staticmethod
def _version_from_dag_run(dag_run, session):
if dag_run.bundle_version:
dag_version = dag_run.created_dag_version
else:
def _version_from_dag_run(dag_run, latest, session):
if latest or not dag_run.bundle_version:
dag_version = DagVersion.get_latest_version(dag_id=dag_run.dag_id, session=session)
return dag_version
if dag_version:
return dag_version

return dag_run.created_dag_version

def get_dag(self, dag_run: DagRun, session: Session) -> DAG | None:
version = self._version_from_dag_run(dag_run=dag_run, session=session)
def get_dag(self, dag_run: DagRun, session: Session, latest=False) -> DAG | None:
Comment thread
jedcunningham marked this conversation as resolved.
version = self._version_from_dag_run(dag_run=dag_run, latest=latest, session=session)
if not version:
return None
return self._get_dag(version_id=version.id, session=session)
Expand Down
7 changes: 6 additions & 1 deletion airflow-core/src/airflow/models/backfill.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -291,6 +291,7 @@ def _create_backfill_dag_run(
dag_run_conf,
backfill_sort_ordinal,
triggering_user_name,
run_on_latest_version,
session,
):
from airflow.models.dagrun import DagRun
Expand DownExpand Up@@ -328,6 +329,7 @@ def _create_backfill_dag_run(
info=info,
backfill_id=backfill_id,
sort_ordinal=backfill_sort_ordinal,
run_on_latest=run_on_latest_version,
)
else:
session.add(
Expand DownExpand Up@@ -401,7 +403,7 @@ def _get_info_list(
return dagrun_info_list


def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal, run_on_latest=False):
"""Clear the existing DAG run and update backfill metadata."""
from sqlalchemy.sql import update

Expand All@@ -415,6 +417,7 @@ def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
session=session,
confirm_prompt=False,
dry_run=False,
run_on_latest_version=run_on_latest,
)

# Update backfill_id and run_type in DagRun table
Expand DownExpand Up@@ -447,6 +450,7 @@ def _create_backfill(
dag_run_conf: dict | None,
triggering_user_name: str | None,
reprocess_behavior: ReprocessBehavior | None = None,
run_on_latest_version: bool = False,
) -> Backfill | None:
from airflow.models import DagModel
from airflow.models.serialized_dag import SerializedDagModel
Expand DownExpand Up@@ -510,6 +514,7 @@ def _create_backfill(
reprocess_behavior=br.reprocess_behavior,
backfill_sort_ordinal=backfill_sort_ordinal,
triggering_user_name=br.triggering_user_name,
run_on_latest_version=run_on_latest_version,
session=session,
)
log.info(
Expand Down
7 changes: 7 additions & 0 deletions airflow-core/src/airflow/models/dag.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1282,6 +1282,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1299,6 +1300,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1317,6 +1319,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1335,6 +1338,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1354,6 +1358,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: bool = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1372,6 +1377,7 @@ def clear(
:param dag_run_state: state to set DagRun to. If set to False, dagrun state will not
be changed.
:param dry_run: Find the tasks to clear but don't clear them.
:param run_on_latest_version: whether to run on latest serialized DAG and Bundle version
:param session: The sqlalchemy session to use
:param dag_bag: The DagBag used to find the dags (Optional)
:param exclude_task_ids: A set of ``task_id`` or (``task_id``, ``map_index``)
Expand DownExpand Up@@ -1417,6 +1423,7 @@ def clear(
list(tis),
session,
dag_run_state=dag_run_state,
run_on_latest_version=run_on_latest_version,
)
else:
count = 0
Expand Down
Loading
, '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('^' + ".*" + ' Add run_on_latest_version support for backfill and clear operations by ephraimbuddy · Pull Request #52177 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions airflow-core/docs/core-concepts/dag-run.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,6 +167,8 @@ the errors after going through the logs, you can re-run the tasks by clearing th
scheduled date. Clearing a task instance creates a record of the task instance.
The ``try_number`` of the current task instance is incremented, the ``max_tries`` set to ``0`` and the state set to ``None``, which causes the task to re-run.

An experimental feature in Airflow 3.1.0 allows you to clear the task instances and re-run with the latest bundle version.

Click on the failed task in the Tree or Graph views and then click on **Clear**.
The executor will re-run it.

Expand Down
4 changes: 3 additions & 1 deletion airflow-core/src/airflow/api_fastapi/app.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -84,8 +84,10 @@ def create_app(apps: str = "all") -> FastAPI:
dag_bag = create_dag_bag()
Comment thread
jason810496 marked this conversation as resolved.

if "execution" in apps_list or "all" in apps_list:
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

task_exec_api_app = create_task_execution_api_app()
task_exec_api_app.state.dag_bag = dag_bag
task_exec_api_app.state.dag_bag = SchedulerDagBag()
init_error_handlers(task_exec_api_app)
Comment thread
jason810496 marked this conversation as resolved.
app.mount("/execution", task_exec_api_app)

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,6 +55,10 @@ class DAGRunClearBody(StrictBaseModel):

dry_run: bool = True
only_failed: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after clearing the DAG Run.",
)


class DAGRunResponse(BaseModel):
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,11 @@ class ClearTaskInstancesBody(StrictBaseModel):
include_downstream: bool = False
include_future: bool = False
include_past: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after "
"clearing the task instances.",
)

@model_validator(mode="before")
@classmethod
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8396,6 +8396,12 @@ components:
type: boolean
title: Include Past
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
Comment thread
ephraimbuddy marked this conversation as resolved.
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the task instances.
default: false
additionalProperties: false
type: object
title: ClearTaskInstancesBody
Expand DownExpand Up@@ -9049,6 +9055,12 @@ components:
type: boolean
title: Only Failed
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the DAG Run.
default: false
additionalProperties: false
type: object
title: DAGRunClearBody
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,6 +281,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
dry_run=True,
session=session,
)
Expand All@@ -293,6 +294,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
session=session,
)
dag_run_cleared = session.scalar(select(DagRun).where(DagRun.id == dag_run.id))
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -675,7 +675,11 @@ def post_clear_task_instances(
if dag_run is None:
error_message = f"Dag Run id {dag_run_id} not found in dag {dag_id}"
raise HTTPException(status.HTTP_404_NOT_FOUND, error_message)
# If dag_run_id is provided, we should get the dag from SchedulerDagBag
# to ensure we get the right version.
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag = SchedulerDagBag().get_dag(dag_run=dag_run, session=session)
if past or future:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
Expand DownExpand Up@@ -724,6 +728,7 @@ def post_clear_task_instances(
task_instances,
session,
DagRunState.QUEUED if reset_dag_runs else False,
run_on_latest_version=body.run_on_latest_version,
)

return TaskInstanceCollectionResponse(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,6 @@
from airflow.api_fastapi.execution_api.datamodels.dagrun import DagRunStateResponse, TriggerDAGRunPayload
from airflow.exceptions import DagRunAlreadyExists
from airflow.models.dag import DagModel
from airflow.models.dagbag import DagBag
from airflow.models.dagrun import DagRun
from airflow.utils.types import DagRunTriggeredByType

Expand DownExpand Up@@ -122,9 +121,20 @@ def clear_dag_run(
"message": f"DAG with dag_id: '{dag_id}' has import errors and cannot be triggered",
},
)
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag_run = session.scalar(select(DagRun).where(DagRun.dag_id == dag_id, DagRun.run_id == run_id))
dag_bag = SchedulerDagBag()
Comment thread
jason810496 marked this conversation as resolved.
Comment thread
jedcunningham marked this conversation as resolved.
dag = dag_bag.get_dag(dag_run=dag_run, session=session)
if not dag:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail={
"reason": "Not Found",
"message": f"DAG with dag_id: '{dag_id}' was not found in the DagBag",
},
)

dag_bag = DagBag(dag_folder=dm.fileloc, read_dags_from_db=True)
dag = dag_bag.get_dag(dag_id)
dag.clear(run_id=run_id)


Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,15 +29,15 @@
import attrs
import structlog
from cadwyn import VersionedAPIRouter
from fastapi import Body, HTTPException, Query, status
from fastapi import Body, Depends, HTTPException, Query, status
from pydantic import JsonValue
from sqlalchemy import func, or_, tuple_, update
from sqlalchemy.exc import NoResultFound, SQLAlchemyError
from sqlalchemy.orm import joinedload
from sqlalchemy.sql import select
from structlog.contextvars import bind_contextvars

from airflow.api_fastapi.common.dagbag import DagBagDep
from airflow.api_fastapi.common.dagbag import dag_bag_from_app
from airflow.api_fastapi.common.db.common import SessionDep
from airflow.api_fastapi.common.types import UtcDateTime
from airflow.api_fastapi.execution_api.datamodels.taskinstance import (
Expand DownExpand Up@@ -76,6 +76,9 @@
from airflow.models.expandinput import SchedulerExpandInput
from airflow.sdk.types import Operator

from airflow.jobs.scheduler_job_runner import SchedulerDagBag

SchedulerDagBagDep = Annotated[SchedulerDagBag, Depends(dag_bag_from_app)]
Comment thread
jason810496 marked this conversation as resolved.

router = VersionedAPIRouter()

Expand DownExpand Up@@ -104,7 +107,7 @@ def ti_run(
task_instance_id: UUID,
ti_run_payload: Annotated[TIEnterRunningPayload, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> TIRunContext:
"""
Run a TaskInstance.
Expand DownExpand Up@@ -255,7 +258,7 @@ def ti_run(
or 0
)

if dag := dag_bag.get_dag(ti.dag_id):
if dag := dag_bag.get_dag(dag_run=dr, session=session):
upstream_map_indexes = dict(
_get_upstream_map_indexes(dag.get_task(ti.task_id), ti.map_index, ti.run_id, session)
)
Expand DownExpand Up@@ -330,7 +333,7 @@ def ti_update_state(
task_instance_id: UUID,
ti_patch_payload: Annotated[TIStateUpdate, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
):
"""
Update the state of a TaskInstance.
Expand DownExpand Up@@ -417,8 +420,9 @@ def ti_update_state(
)


def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: DagBagDep) -> None:
ser_dag = dag_bag.get_dag(dag_id)
def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: SchedulerDagBagDep) -> None:
dr = ti.dag_run
ser_dag = dag_bag.get_dag(dag_run=dr, session=session)
if ser_dag and getattr(ser_dag, "fail_fast", False):
task_dict = getattr(ser_dag, "task_dict")
task_teardown_map = {k: v.is_teardown for k, v in task_dict.items()}
Expand All@@ -432,7 +436,7 @@ def _create_ti_state_update_query_and_update_state(
query: Update,
updated_state,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
dag_id: str,
) -> tuple[Update, TaskInstanceState]:
if isinstance(ti_patch_payload, (TITerminalStatePayload, TIRetryStatePayload, TISuccessStatePayload)):
Expand DownExpand Up@@ -893,7 +897,7 @@ def _get_group_tasks(dag_id: str, task_group_id: str, session: SessionDep, logic
def validate_inlets_and_outlets(
task_instance_id: UUID,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> InactiveAssetsResponse:
"""Validate whether there're inactive assets in inlets and outlets of a given task instance."""
ti_id_str = str(task_instance_id)
Expand All@@ -911,7 +915,8 @@ def validate_inlets_and_outlets(
)

if not ti.task:
dag = dag_bag.get_dag(ti.dag_id)
dr = ti.dag_run
dag = dag_bag.get_dag(dag_run=dr, session=session)
if dag:
with contextlib.suppress(TaskNotFound):
ti.task = dag.get_task(ti.task_id)
Expand Down
9 changes: 9 additions & 0 deletions airflow-core/src/airflow/cli/cli_config.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -340,6 +340,14 @@ def string_lower_type(val):
),
choices=("none", "completed", "failed"),
)
ARG_BACKFILL_RUN_ON_LATEST_VERSION = Arg(
("--run-on-latest-version",),
help=(
"(Experimental) If set, the backfill will run tasks using the latest bundle version instead of "
"the version that was active when the original Dag run was created."
),
action="store_true",
)


# misc
Expand DownExpand Up@@ -968,6 +976,7 @@ class GroupCommand(NamedTuple):
ARG_RUN_BACKWARDS,
ARG_MAX_ACTIVE_RUNS,
ARG_BACKFILL_REPROCESS_BEHAVIOR,
ARG_BACKFILL_RUN_ON_LATEST_VERSION,
ARG_BACKFILL_DRY_RUN,
),
),
Expand Down
2 changes: 2 additions & 0 deletions airflow-core/src/airflow/cli/commands/backfill_command.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ def create_backfill(args) -> None:
reverse=args.run_backwards,
dag_run_conf=args.dag_run_conf,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
for k, v in params.items():
console.print(f" - {k} = {v}")
Expand DownExpand Up@@ -88,4 +89,5 @@ def create_backfill(args) -> None:
dag_run_conf=args.dag_run_conf,
triggering_user_name=user,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
15 changes: 8 additions & 7 deletions airflow-core/src/airflow/jobs/scheduler_job_runner.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,15 +131,16 @@ def _get_dag(self, version_id: str, session: Session) -> DAG | None:
return dag

@staticmethod
def _version_from_dag_run(dag_run, session):
if dag_run.bundle_version:
dag_version = dag_run.created_dag_version
else:
def _version_from_dag_run(dag_run, latest, session):
if latest or not dag_run.bundle_version:
dag_version = DagVersion.get_latest_version(dag_id=dag_run.dag_id, session=session)
return dag_version
if dag_version:
return dag_version

return dag_run.created_dag_version

def get_dag(self, dag_run: DagRun, session: Session) -> DAG | None:
version = self._version_from_dag_run(dag_run=dag_run, session=session)
def get_dag(self, dag_run: DagRun, session: Session, latest=False) -> DAG | None:
Comment thread
jedcunningham marked this conversation as resolved.
version = self._version_from_dag_run(dag_run=dag_run, latest=latest, session=session)
if not version:
return None
return self._get_dag(version_id=version.id, session=session)
Expand Down
7 changes: 6 additions & 1 deletion airflow-core/src/airflow/models/backfill.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -291,6 +291,7 @@ def _create_backfill_dag_run(
dag_run_conf,
backfill_sort_ordinal,
triggering_user_name,
run_on_latest_version,
session,
):
from airflow.models.dagrun import DagRun
Expand DownExpand Up@@ -328,6 +329,7 @@ def _create_backfill_dag_run(
info=info,
backfill_id=backfill_id,
sort_ordinal=backfill_sort_ordinal,
run_on_latest=run_on_latest_version,
)
else:
session.add(
Expand DownExpand Up@@ -401,7 +403,7 @@ def _get_info_list(
return dagrun_info_list


def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal, run_on_latest=False):
"""Clear the existing DAG run and update backfill metadata."""
from sqlalchemy.sql import update

Expand All@@ -415,6 +417,7 @@ def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
session=session,
confirm_prompt=False,
dry_run=False,
run_on_latest_version=run_on_latest,
)

# Update backfill_id and run_type in DagRun table
Expand DownExpand Up@@ -447,6 +450,7 @@ def _create_backfill(
dag_run_conf: dict | None,
triggering_user_name: str | None,
reprocess_behavior: ReprocessBehavior | None = None,
run_on_latest_version: bool = False,
) -> Backfill | None:
from airflow.models import DagModel
from airflow.models.serialized_dag import SerializedDagModel
Expand DownExpand Up@@ -510,6 +514,7 @@ def _create_backfill(
reprocess_behavior=br.reprocess_behavior,
backfill_sort_ordinal=backfill_sort_ordinal,
triggering_user_name=br.triggering_user_name,
run_on_latest_version=run_on_latest_version,
session=session,
)
log.info(
Expand Down
7 changes: 7 additions & 0 deletions airflow-core/src/airflow/models/dag.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1282,6 +1282,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1299,6 +1300,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1317,6 +1319,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1335,6 +1338,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1354,6 +1358,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: bool = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1372,6 +1377,7 @@ def clear(
:param dag_run_state: state to set DagRun to. If set to False, dagrun state will not
be changed.
:param dry_run: Find the tasks to clear but don't clear them.
:param run_on_latest_version: whether to run on latest serialized DAG and Bundle version
:param session: The sqlalchemy session to use
:param dag_bag: The DagBag used to find the dags (Optional)
:param exclude_task_ids: A set of ``task_id`` or (``task_id``, ``map_index``)
Expand DownExpand Up@@ -1417,6 +1423,7 @@ def clear(
list(tis),
session,
dag_run_state=dag_run_state,
run_on_latest_version=run_on_latest_version,
)
else:
count = 0
Expand Down
Loading
, '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('^' + ".*" + ' Add run_on_latest_version support for backfill and clear operations by ephraimbuddy · Pull Request #52177 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions airflow-core/docs/core-concepts/dag-run.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,6 +167,8 @@ the errors after going through the logs, you can re-run the tasks by clearing th
scheduled date. Clearing a task instance creates a record of the task instance.
The ``try_number`` of the current task instance is incremented, the ``max_tries`` set to ``0`` and the state set to ``None``, which causes the task to re-run.

An experimental feature in Airflow 3.1.0 allows you to clear the task instances and re-run with the latest bundle version.

Click on the failed task in the Tree or Graph views and then click on **Clear**.
The executor will re-run it.

Expand Down
4 changes: 3 additions & 1 deletion airflow-core/src/airflow/api_fastapi/app.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -84,8 +84,10 @@ def create_app(apps: str = "all") -> FastAPI:
dag_bag = create_dag_bag()
Comment thread
jason810496 marked this conversation as resolved.

if "execution" in apps_list or "all" in apps_list:
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

task_exec_api_app = create_task_execution_api_app()
task_exec_api_app.state.dag_bag = dag_bag
task_exec_api_app.state.dag_bag = SchedulerDagBag()
init_error_handlers(task_exec_api_app)
Comment thread
jason810496 marked this conversation as resolved.
app.mount("/execution", task_exec_api_app)

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,6 +55,10 @@ class DAGRunClearBody(StrictBaseModel):

dry_run: bool = True
only_failed: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after clearing the DAG Run.",
)


class DAGRunResponse(BaseModel):
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,11 @@ class ClearTaskInstancesBody(StrictBaseModel):
include_downstream: bool = False
include_future: bool = False
include_past: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after "
"clearing the task instances.",
)

@model_validator(mode="before")
@classmethod
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8396,6 +8396,12 @@ components:
type: boolean
title: Include Past
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
Comment thread
ephraimbuddy marked this conversation as resolved.
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the task instances.
default: false
additionalProperties: false
type: object
title: ClearTaskInstancesBody
Expand DownExpand Up@@ -9049,6 +9055,12 @@ components:
type: boolean
title: Only Failed
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the DAG Run.
default: false
additionalProperties: false
type: object
title: DAGRunClearBody
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,6 +281,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
dry_run=True,
session=session,
)
Expand All@@ -293,6 +294,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
session=session,
)
dag_run_cleared = session.scalar(select(DagRun).where(DagRun.id == dag_run.id))
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -675,7 +675,11 @@ def post_clear_task_instances(
if dag_run is None:
error_message = f"Dag Run id {dag_run_id} not found in dag {dag_id}"
raise HTTPException(status.HTTP_404_NOT_FOUND, error_message)
# If dag_run_id is provided, we should get the dag from SchedulerDagBag
# to ensure we get the right version.
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag = SchedulerDagBag().get_dag(dag_run=dag_run, session=session)
if past or future:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
Expand DownExpand Up@@ -724,6 +728,7 @@ def post_clear_task_instances(
task_instances,
session,
DagRunState.QUEUED if reset_dag_runs else False,
run_on_latest_version=body.run_on_latest_version,
)

return TaskInstanceCollectionResponse(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,6 @@
from airflow.api_fastapi.execution_api.datamodels.dagrun import DagRunStateResponse, TriggerDAGRunPayload
from airflow.exceptions import DagRunAlreadyExists
from airflow.models.dag import DagModel
from airflow.models.dagbag import DagBag
from airflow.models.dagrun import DagRun
from airflow.utils.types import DagRunTriggeredByType

Expand DownExpand Up@@ -122,9 +121,20 @@ def clear_dag_run(
"message": f"DAG with dag_id: '{dag_id}' has import errors and cannot be triggered",
},
)
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag_run = session.scalar(select(DagRun).where(DagRun.dag_id == dag_id, DagRun.run_id == run_id))
dag_bag = SchedulerDagBag()
Comment thread
jason810496 marked this conversation as resolved.
Comment thread
jedcunningham marked this conversation as resolved.
dag = dag_bag.get_dag(dag_run=dag_run, session=session)
if not dag:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail={
"reason": "Not Found",
"message": f"DAG with dag_id: '{dag_id}' was not found in the DagBag",
},
)

dag_bag = DagBag(dag_folder=dm.fileloc, read_dags_from_db=True)
dag = dag_bag.get_dag(dag_id)
dag.clear(run_id=run_id)


Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,15 +29,15 @@
import attrs
import structlog
from cadwyn import VersionedAPIRouter
from fastapi import Body, HTTPException, Query, status
from fastapi import Body, Depends, HTTPException, Query, status
from pydantic import JsonValue
from sqlalchemy import func, or_, tuple_, update
from sqlalchemy.exc import NoResultFound, SQLAlchemyError
from sqlalchemy.orm import joinedload
from sqlalchemy.sql import select
from structlog.contextvars import bind_contextvars

from airflow.api_fastapi.common.dagbag import DagBagDep
from airflow.api_fastapi.common.dagbag import dag_bag_from_app
from airflow.api_fastapi.common.db.common import SessionDep
from airflow.api_fastapi.common.types import UtcDateTime
from airflow.api_fastapi.execution_api.datamodels.taskinstance import (
Expand DownExpand Up@@ -76,6 +76,9 @@
from airflow.models.expandinput import SchedulerExpandInput
from airflow.sdk.types import Operator

from airflow.jobs.scheduler_job_runner import SchedulerDagBag

SchedulerDagBagDep = Annotated[SchedulerDagBag, Depends(dag_bag_from_app)]
Comment thread
jason810496 marked this conversation as resolved.

router = VersionedAPIRouter()

Expand DownExpand Up@@ -104,7 +107,7 @@ def ti_run(
task_instance_id: UUID,
ti_run_payload: Annotated[TIEnterRunningPayload, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> TIRunContext:
"""
Run a TaskInstance.
Expand DownExpand Up@@ -255,7 +258,7 @@ def ti_run(
or 0
)

if dag := dag_bag.get_dag(ti.dag_id):
if dag := dag_bag.get_dag(dag_run=dr, session=session):
upstream_map_indexes = dict(
_get_upstream_map_indexes(dag.get_task(ti.task_id), ti.map_index, ti.run_id, session)
)
Expand DownExpand Up@@ -330,7 +333,7 @@ def ti_update_state(
task_instance_id: UUID,
ti_patch_payload: Annotated[TIStateUpdate, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
):
"""
Update the state of a TaskInstance.
Expand DownExpand Up@@ -417,8 +420,9 @@ def ti_update_state(
)


def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: DagBagDep) -> None:
ser_dag = dag_bag.get_dag(dag_id)
def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: SchedulerDagBagDep) -> None:
dr = ti.dag_run
ser_dag = dag_bag.get_dag(dag_run=dr, session=session)
if ser_dag and getattr(ser_dag, "fail_fast", False):
task_dict = getattr(ser_dag, "task_dict")
task_teardown_map = {k: v.is_teardown for k, v in task_dict.items()}
Expand All@@ -432,7 +436,7 @@ def _create_ti_state_update_query_and_update_state(
query: Update,
updated_state,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
dag_id: str,
) -> tuple[Update, TaskInstanceState]:
if isinstance(ti_patch_payload, (TITerminalStatePayload, TIRetryStatePayload, TISuccessStatePayload)):
Expand DownExpand Up@@ -893,7 +897,7 @@ def _get_group_tasks(dag_id: str, task_group_id: str, session: SessionDep, logic
def validate_inlets_and_outlets(
task_instance_id: UUID,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> InactiveAssetsResponse:
"""Validate whether there're inactive assets in inlets and outlets of a given task instance."""
ti_id_str = str(task_instance_id)
Expand All@@ -911,7 +915,8 @@ def validate_inlets_and_outlets(
)

if not ti.task:
dag = dag_bag.get_dag(ti.dag_id)
dr = ti.dag_run
dag = dag_bag.get_dag(dag_run=dr, session=session)
if dag:
with contextlib.suppress(TaskNotFound):
ti.task = dag.get_task(ti.task_id)
Expand Down
9 changes: 9 additions & 0 deletions airflow-core/src/airflow/cli/cli_config.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -340,6 +340,14 @@ def string_lower_type(val):
),
choices=("none", "completed", "failed"),
)
ARG_BACKFILL_RUN_ON_LATEST_VERSION = Arg(
("--run-on-latest-version",),
help=(
"(Experimental) If set, the backfill will run tasks using the latest bundle version instead of "
"the version that was active when the original Dag run was created."
),
action="store_true",
)


# misc
Expand DownExpand Up@@ -968,6 +976,7 @@ class GroupCommand(NamedTuple):
ARG_RUN_BACKWARDS,
ARG_MAX_ACTIVE_RUNS,
ARG_BACKFILL_REPROCESS_BEHAVIOR,
ARG_BACKFILL_RUN_ON_LATEST_VERSION,
ARG_BACKFILL_DRY_RUN,
),
),
Expand Down
2 changes: 2 additions & 0 deletions airflow-core/src/airflow/cli/commands/backfill_command.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ def create_backfill(args) -> None:
reverse=args.run_backwards,
dag_run_conf=args.dag_run_conf,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
for k, v in params.items():
console.print(f" - {k} = {v}")
Expand DownExpand Up@@ -88,4 +89,5 @@ def create_backfill(args) -> None:
dag_run_conf=args.dag_run_conf,
triggering_user_name=user,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
15 changes: 8 additions & 7 deletions airflow-core/src/airflow/jobs/scheduler_job_runner.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,15 +131,16 @@ def _get_dag(self, version_id: str, session: Session) -> DAG | None:
return dag

@staticmethod
def _version_from_dag_run(dag_run, session):
if dag_run.bundle_version:
dag_version = dag_run.created_dag_version
else:
def _version_from_dag_run(dag_run, latest, session):
if latest or not dag_run.bundle_version:
dag_version = DagVersion.get_latest_version(dag_id=dag_run.dag_id, session=session)
return dag_version
if dag_version:
return dag_version

return dag_run.created_dag_version

def get_dag(self, dag_run: DagRun, session: Session) -> DAG | None:
version = self._version_from_dag_run(dag_run=dag_run, session=session)
def get_dag(self, dag_run: DagRun, session: Session, latest=False) -> DAG | None:
Comment thread
jedcunningham marked this conversation as resolved.
version = self._version_from_dag_run(dag_run=dag_run, latest=latest, session=session)
if not version:
return None
return self._get_dag(version_id=version.id, session=session)
Expand Down
7 changes: 6 additions & 1 deletion airflow-core/src/airflow/models/backfill.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -291,6 +291,7 @@ def _create_backfill_dag_run(
dag_run_conf,
backfill_sort_ordinal,
triggering_user_name,
run_on_latest_version,
session,
):
from airflow.models.dagrun import DagRun
Expand DownExpand Up@@ -328,6 +329,7 @@ def _create_backfill_dag_run(
info=info,
backfill_id=backfill_id,
sort_ordinal=backfill_sort_ordinal,
run_on_latest=run_on_latest_version,
)
else:
session.add(
Expand DownExpand Up@@ -401,7 +403,7 @@ def _get_info_list(
return dagrun_info_list


def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal, run_on_latest=False):
"""Clear the existing DAG run and update backfill metadata."""
from sqlalchemy.sql import update

Expand All@@ -415,6 +417,7 @@ def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
session=session,
confirm_prompt=False,
dry_run=False,
run_on_latest_version=run_on_latest,
)

# Update backfill_id and run_type in DagRun table
Expand DownExpand Up@@ -447,6 +450,7 @@ def _create_backfill(
dag_run_conf: dict | None,
triggering_user_name: str | None,
reprocess_behavior: ReprocessBehavior | None = None,
run_on_latest_version: bool = False,
) -> Backfill | None:
from airflow.models import DagModel
from airflow.models.serialized_dag import SerializedDagModel
Expand DownExpand Up@@ -510,6 +514,7 @@ def _create_backfill(
reprocess_behavior=br.reprocess_behavior,
backfill_sort_ordinal=backfill_sort_ordinal,
triggering_user_name=br.triggering_user_name,
run_on_latest_version=run_on_latest_version,
session=session,
)
log.info(
Expand Down
7 changes: 7 additions & 0 deletions airflow-core/src/airflow/models/dag.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1282,6 +1282,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1299,6 +1300,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1317,6 +1319,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1335,6 +1338,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1354,6 +1358,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: bool = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1372,6 +1377,7 @@ def clear(
:param dag_run_state: state to set DagRun to. If set to False, dagrun state will not
be changed.
:param dry_run: Find the tasks to clear but don't clear them.
:param run_on_latest_version: whether to run on latest serialized DAG and Bundle version
:param session: The sqlalchemy session to use
:param dag_bag: The DagBag used to find the dags (Optional)
:param exclude_task_ids: A set of ``task_id`` or (``task_id``, ``map_index``)
Expand DownExpand Up@@ -1417,6 +1423,7 @@ def clear(
list(tis),
session,
dag_run_state=dag_run_state,
run_on_latest_version=run_on_latest_version,
)
else:
count = 0
Expand Down
Loading
, '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); } })(); })(); Add run_on_latest_version support for backfill and clear operations by ephraimbuddy · Pull Request #52177 · apache/airflow · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions airflow-core/docs/core-concepts/dag-run.rst
Original file line numberDiff line numberDiff line change
Expand Up@@ -167,6 +167,8 @@ the errors after going through the logs, you can re-run the tasks by clearing th
scheduled date. Clearing a task instance creates a record of the task instance.
The ``try_number`` of the current task instance is incremented, the ``max_tries`` set to ``0`` and the state set to ``None``, which causes the task to re-run.

An experimental feature in Airflow 3.1.0 allows you to clear the task instances and re-run with the latest bundle version.

Click on the failed task in the Tree or Graph views and then click on **Clear**.
The executor will re-run it.

Expand Down
4 changes: 3 additions & 1 deletion airflow-core/src/airflow/api_fastapi/app.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -84,8 +84,10 @@ def create_app(apps: str = "all") -> FastAPI:
dag_bag = create_dag_bag()
Comment thread
jason810496 marked this conversation as resolved.

if "execution" in apps_list or "all" in apps_list:
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

task_exec_api_app = create_task_execution_api_app()
task_exec_api_app.state.dag_bag = dag_bag
task_exec_api_app.state.dag_bag = SchedulerDagBag()
init_error_handlers(task_exec_api_app)
Comment thread
jason810496 marked this conversation as resolved.
app.mount("/execution", task_exec_api_app)

Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -55,6 +55,10 @@ class DAGRunClearBody(StrictBaseModel):

dry_run: bool = True
only_failed: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after clearing the DAG Run.",
)


class DAGRunResponse(BaseModel):
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -179,6 +179,11 @@ class ClearTaskInstancesBody(StrictBaseModel):
include_downstream: bool = False
include_future: bool = False
include_past: bool = False
run_on_latest_version: bool = Field(
default=False,
description="(Experimental) Run on the latest bundle version of the DAG after "
"clearing the task instances.",
)

@model_validator(mode="before")
@classmethod
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -8396,6 +8396,12 @@ components:
type: boolean
title: Include Past
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
Comment thread
ephraimbuddy marked this conversation as resolved.
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the task instances.
default: false
additionalProperties: false
type: object
title: ClearTaskInstancesBody
Expand DownExpand Up@@ -9049,6 +9055,12 @@ components:
type: boolean
title: Only Failed
default: false
run_on_latest_version:
type: boolean
title: Run On Latest Version
description: (Experimental) Run on the latest bundle version of the DAG
after clearing the DAG Run.
default: false
additionalProperties: false
type: object
title: DAGRunClearBody
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -281,6 +281,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
dry_run=True,
session=session,
)
Expand All@@ -293,6 +294,7 @@ def clear_dag_run(
run_id=dag_run_id,
task_ids=None,
only_failed=body.only_failed,
run_on_latest_version=body.run_on_latest_version,
session=session,
)
dag_run_cleared = session.scalar(select(DagRun).where(DagRun.id == dag_run.id))
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -675,7 +675,11 @@ def post_clear_task_instances(
if dag_run is None:
error_message = f"Dag Run id {dag_run_id} not found in dag {dag_id}"
raise HTTPException(status.HTTP_404_NOT_FOUND, error_message)
# If dag_run_id is provided, we should get the dag from SchedulerDagBag
# to ensure we get the right version.
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag = SchedulerDagBag().get_dag(dag_run=dag_run, session=session)
if past or future:
raise HTTPException(
status.HTTP_400_BAD_REQUEST,
Expand DownExpand Up@@ -724,6 +728,7 @@ def post_clear_task_instances(
task_instances,
session,
DagRunState.QUEUED if reset_dag_runs else False,
run_on_latest_version=body.run_on_latest_version,
)

return TaskInstanceCollectionResponse(
Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,7 +29,6 @@
from airflow.api_fastapi.execution_api.datamodels.dagrun import DagRunStateResponse, TriggerDAGRunPayload
from airflow.exceptions import DagRunAlreadyExists
from airflow.models.dag import DagModel
from airflow.models.dagbag import DagBag
from airflow.models.dagrun import DagRun
from airflow.utils.types import DagRunTriggeredByType

Expand DownExpand Up@@ -122,9 +121,20 @@ def clear_dag_run(
"message": f"DAG with dag_id: '{dag_id}' has import errors and cannot be triggered",
},
)
from airflow.jobs.scheduler_job_runner import SchedulerDagBag

dag_run = session.scalar(select(DagRun).where(DagRun.dag_id == dag_id, DagRun.run_id == run_id))
dag_bag = SchedulerDagBag()
Comment thread
jason810496 marked this conversation as resolved.
Comment thread
jedcunningham marked this conversation as resolved.
dag = dag_bag.get_dag(dag_run=dag_run, session=session)
if not dag:
raise HTTPException(
status.HTTP_404_NOT_FOUND,
detail={
"reason": "Not Found",
"message": f"DAG with dag_id: '{dag_id}' was not found in the DagBag",
},
)

dag_bag = DagBag(dag_folder=dm.fileloc, read_dags_from_db=True)
dag = dag_bag.get_dag(dag_id)
dag.clear(run_id=run_id)


Expand Down
Original file line numberDiff line numberDiff line change
Expand Up@@ -29,15 +29,15 @@
import attrs
import structlog
from cadwyn import VersionedAPIRouter
from fastapi import Body, HTTPException, Query, status
from fastapi import Body, Depends, HTTPException, Query, status
from pydantic import JsonValue
from sqlalchemy import func, or_, tuple_, update
from sqlalchemy.exc import NoResultFound, SQLAlchemyError
from sqlalchemy.orm import joinedload
from sqlalchemy.sql import select
from structlog.contextvars import bind_contextvars

from airflow.api_fastapi.common.dagbag import DagBagDep
from airflow.api_fastapi.common.dagbag import dag_bag_from_app
from airflow.api_fastapi.common.db.common import SessionDep
from airflow.api_fastapi.common.types import UtcDateTime
from airflow.api_fastapi.execution_api.datamodels.taskinstance import (
Expand DownExpand Up@@ -76,6 +76,9 @@
from airflow.models.expandinput import SchedulerExpandInput
from airflow.sdk.types import Operator

from airflow.jobs.scheduler_job_runner import SchedulerDagBag

SchedulerDagBagDep = Annotated[SchedulerDagBag, Depends(dag_bag_from_app)]
Comment thread
jason810496 marked this conversation as resolved.

router = VersionedAPIRouter()

Expand DownExpand Up@@ -104,7 +107,7 @@ def ti_run(
task_instance_id: UUID,
ti_run_payload: Annotated[TIEnterRunningPayload, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> TIRunContext:
"""
Run a TaskInstance.
Expand DownExpand Up@@ -255,7 +258,7 @@ def ti_run(
or 0
)

if dag := dag_bag.get_dag(ti.dag_id):
if dag := dag_bag.get_dag(dag_run=dr, session=session):
upstream_map_indexes = dict(
_get_upstream_map_indexes(dag.get_task(ti.task_id), ti.map_index, ti.run_id, session)
)
Expand DownExpand Up@@ -330,7 +333,7 @@ def ti_update_state(
task_instance_id: UUID,
ti_patch_payload: Annotated[TIStateUpdate, Body()],
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
):
"""
Update the state of a TaskInstance.
Expand DownExpand Up@@ -417,8 +420,9 @@ def ti_update_state(
)


def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: DagBagDep) -> None:
ser_dag = dag_bag.get_dag(dag_id)
def _handle_fail_fast_for_dag(ti: TI, dag_id: str, session: SessionDep, dag_bag: SchedulerDagBagDep) -> None:
dr = ti.dag_run
ser_dag = dag_bag.get_dag(dag_run=dr, session=session)
if ser_dag and getattr(ser_dag, "fail_fast", False):
task_dict = getattr(ser_dag, "task_dict")
task_teardown_map = {k: v.is_teardown for k, v in task_dict.items()}
Expand All@@ -432,7 +436,7 @@ def _create_ti_state_update_query_and_update_state(
query: Update,
updated_state,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
dag_id: str,
) -> tuple[Update, TaskInstanceState]:
if isinstance(ti_patch_payload, (TITerminalStatePayload, TIRetryStatePayload, TISuccessStatePayload)):
Expand DownExpand Up@@ -893,7 +897,7 @@ def _get_group_tasks(dag_id: str, task_group_id: str, session: SessionDep, logic
def validate_inlets_and_outlets(
task_instance_id: UUID,
session: SessionDep,
dag_bag: DagBagDep,
dag_bag: SchedulerDagBagDep,
) -> InactiveAssetsResponse:
"""Validate whether there're inactive assets in inlets and outlets of a given task instance."""
ti_id_str = str(task_instance_id)
Expand All@@ -911,7 +915,8 @@ def validate_inlets_and_outlets(
)

if not ti.task:
dag = dag_bag.get_dag(ti.dag_id)
dr = ti.dag_run
dag = dag_bag.get_dag(dag_run=dr, session=session)
if dag:
with contextlib.suppress(TaskNotFound):
ti.task = dag.get_task(ti.task_id)
Expand Down
9 changes: 9 additions & 0 deletions airflow-core/src/airflow/cli/cli_config.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -340,6 +340,14 @@ def string_lower_type(val):
),
choices=("none", "completed", "failed"),
)
ARG_BACKFILL_RUN_ON_LATEST_VERSION = Arg(
("--run-on-latest-version",),
help=(
"(Experimental) If set, the backfill will run tasks using the latest bundle version instead of "
"the version that was active when the original Dag run was created."
),
action="store_true",
)


# misc
Expand DownExpand Up@@ -968,6 +976,7 @@ class GroupCommand(NamedTuple):
ARG_RUN_BACKWARDS,
ARG_MAX_ACTIVE_RUNS,
ARG_BACKFILL_REPROCESS_BEHAVIOR,
ARG_BACKFILL_RUN_ON_LATEST_VERSION,
ARG_BACKFILL_DRY_RUN,
),
),
Expand Down
2 changes: 2 additions & 0 deletions airflow-core/src/airflow/cli/commands/backfill_command.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -57,6 +57,7 @@ def create_backfill(args) -> None:
reverse=args.run_backwards,
dag_run_conf=args.dag_run_conf,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
for k, v in params.items():
console.print(f" - {k} = {v}")
Expand DownExpand Up@@ -88,4 +89,5 @@ def create_backfill(args) -> None:
dag_run_conf=args.dag_run_conf,
triggering_user_name=user,
reprocess_behavior=reprocess_behavior,
run_on_latest_version=args.run_on_latest_version,
)
15 changes: 8 additions & 7 deletions airflow-core/src/airflow/jobs/scheduler_job_runner.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -131,15 +131,16 @@ def _get_dag(self, version_id: str, session: Session) -> DAG | None:
return dag

@staticmethod
def _version_from_dag_run(dag_run, session):
if dag_run.bundle_version:
dag_version = dag_run.created_dag_version
else:
def _version_from_dag_run(dag_run, latest, session):
if latest or not dag_run.bundle_version:
dag_version = DagVersion.get_latest_version(dag_id=dag_run.dag_id, session=session)
return dag_version
if dag_version:
return dag_version

return dag_run.created_dag_version

def get_dag(self, dag_run: DagRun, session: Session) -> DAG | None:
version = self._version_from_dag_run(dag_run=dag_run, session=session)
def get_dag(self, dag_run: DagRun, session: Session, latest=False) -> DAG | None:
Comment thread
jedcunningham marked this conversation as resolved.
version = self._version_from_dag_run(dag_run=dag_run, latest=latest, session=session)
if not version:
return None
return self._get_dag(version_id=version.id, session=session)
Expand Down
7 changes: 6 additions & 1 deletion airflow-core/src/airflow/models/backfill.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -291,6 +291,7 @@ def _create_backfill_dag_run(
dag_run_conf,
backfill_sort_ordinal,
triggering_user_name,
run_on_latest_version,
session,
):
from airflow.models.dagrun import DagRun
Expand DownExpand Up@@ -328,6 +329,7 @@ def _create_backfill_dag_run(
info=info,
backfill_id=backfill_id,
sort_ordinal=backfill_sort_ordinal,
run_on_latest=run_on_latest_version,
)
else:
session.add(
Expand DownExpand Up@@ -401,7 +403,7 @@ def _get_info_list(
return dagrun_info_list


def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal, run_on_latest=False):
"""Clear the existing DAG run and update backfill metadata."""
from sqlalchemy.sql import update

Expand All@@ -415,6 +417,7 @@ def _handle_clear_run(session, dag, dr, info, backfill_id, sort_ordinal):
session=session,
confirm_prompt=False,
dry_run=False,
run_on_latest_version=run_on_latest,
)

# Update backfill_id and run_type in DagRun table
Expand DownExpand Up@@ -447,6 +450,7 @@ def _create_backfill(
dag_run_conf: dict | None,
triggering_user_name: str | None,
reprocess_behavior: ReprocessBehavior | None = None,
run_on_latest_version: bool = False,
) -> Backfill | None:
from airflow.models import DagModel
from airflow.models.serialized_dag import SerializedDagModel
Expand DownExpand Up@@ -510,6 +514,7 @@ def _create_backfill(
reprocess_behavior=br.reprocess_behavior,
backfill_sort_ordinal=backfill_sort_ordinal,
triggering_user_name=br.triggering_user_name,
run_on_latest_version=run_on_latest_version,
session=session,
)
log.info(
Expand Down
7 changes: 7 additions & 0 deletions airflow-core/src/airflow/models/dag.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1282,6 +1282,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1299,6 +1300,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1317,6 +1319,7 @@ def clear(
only_running: bool = False,
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1335,6 +1338,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: Literal[False] = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1354,6 +1358,7 @@ def clear(
confirm_prompt: bool = False,
dag_run_state: DagRunState = DagRunState.QUEUED,
dry_run: bool = False,
run_on_latest_version: bool = False,
session: Session = NEW_SESSION,
dag_bag: DagBag | None = None,
exclude_task_ids: frozenset[str] | frozenset[tuple[str, int]] | None = frozenset(),
Expand All@@ -1372,6 +1377,7 @@ def clear(
:param dag_run_state: state to set DagRun to. If set to False, dagrun state will not
be changed.
:param dry_run: Find the tasks to clear but don't clear them.
:param run_on_latest_version: whether to run on latest serialized DAG and Bundle version
:param session: The sqlalchemy session to use
:param dag_bag: The DagBag used to find the dags (Optional)
:param exclude_task_ids: A set of ``task_id`` or (``task_id``, ``map_index``)
Expand DownExpand Up@@ -1417,6 +1423,7 @@ def clear(
list(tis),
session,
dag_run_state=dag_run_state,
run_on_latest_version=run_on_latest_version,
)
else:
count = 0
Expand Down
Loading