Source code for util.dag_trigger
"""DAG Trigger Scheduler completion callbacks.
Provides on_success_callback / on_failure_callback functions that target DAGs
use to report completion status back to airflow.dag_trigger_run in Redshift.
Usage in a target DAG:
from util.dag_trigger import scheduler_callbacks
@Util.default_dag(
Config, file_name=__file__, email=EMAIL,
on_success_callback=scheduler_callbacks.on_success,
on_failure_callback=scheduler_callbacks.on_failure,
)
def my_dag():
...
Runs triggered manually (without _scheduler_run_id in conf) are ignored.
"""
from config import Config
from operators.redshift.base_redshift import BaseRedshiftOperator
_REDSHIFT_DB = "dataops"
_REGION = "us-east-1"
_SCHEDULER_RUN_ID_KEY = "_scheduler_run_id"
def _mark_completed(airflow_run_id: str, status: str, error_message: str | None = None) -> None:
# Accept pending OR triggered (never overwrite a terminal row). This closes
# the race where a fast-finishing target DAG fires its callback before the
# scheduler's mark_triggered runs (row still 'pending'), which previously
# matched zero rows and silently lost the completion.
error_val = BaseRedshiftOperator.sql_literal(error_message[:65535]) if error_message else "NULL"
# COALESCE triggered_at: a fast target DAG can fire this callback before the
# scheduler's mark_triggered runs (row still 'pending', triggered_at NULL).
# Without this, the run is excluded from find_due_items' last_triggered_at
# watermark (which requires triggered_at IS NOT NULL), causing the next
# incremental run to reprocess a wider window / fall back to full refresh.
# created_at is conservative (<= source-read time), so the cutoff can't skip rows.
sql = (
"UPDATE airflow.dag_trigger_run\n"
f"SET status = {BaseRedshiftOperator.sql_literal(status)},\n"
f" triggered_at = COALESCE(triggered_at, created_at),\n"
f" finished_at = GETDATE(),\n"
f" error_message = {error_val}\n"
f"WHERE airflow_run_id = {BaseRedshiftOperator.sql_literal(airflow_run_id)}\n"
f" AND status IN ('pending', 'triggered');"
)
affected = BaseRedshiftOperator.run_statement(
sql,
workgroup=Config.REDSHIFT_WORKGROUP,
database=_REDSHIFT_DB,
region=_REGION,
)
print(f"[dag_trigger] Marked {affected} row(s) '{status}' for {airflow_run_id}")
def _on_success(context) -> None:
conf = context["dag_run"].conf or {}
run_id = conf.get(_SCHEDULER_RUN_ID_KEY)
if not run_id:
return
try:
_mark_completed(run_id, "success")
except Exception as e:
print(f"[dag_trigger] Failed to mark success for {run_id}: {e}")
def _on_failure(context) -> None:
conf = context["dag_run"].conf or {}
run_id = conf.get(_SCHEDULER_RUN_ID_KEY)
if not run_id:
return
error_msg = str(context.get("exception", ""))[:65535] or None
try:
_mark_completed(run_id, "failed", error_msg)
except Exception as e:
print(f"[dag_trigger] Failed to mark failure for {run_id}: {e}")
[docs]
class scheduler_callbacks:
on_success = staticmethod(_on_success)
on_failure = staticmethod(_on_failure)