amoghrajesh opened a new pull request, #75721:
URL: https://github.com/apache/airflow/pull/75721

    <!-- SPDX-License-Identifier: Apache-2.0
         https://www.apache.org/licenses/LICENSE-2.0 -->
   
   <!--
   Thank you for contributing!
   
   Please provide above a brief description of the changes made in this pull 
request.
   Write a good git commit message following this guide: 
https://chris.beams.io/posts/git-commit/
   
   Please make sure that your code changes are covered with tests.
   And in case of new features or big changes remember to adjust the 
documentation.
   
   For user-facing UI changes, please attach before/after screenshots (or a 
short
   screen recording) so reviewers can assess the visual impact.
   
   Feel free to ping (in general) for the review if you do not see reaction for 
a few days
   (72 Hours is the minimum reaction time you can expect from volunteers) - we 
sometimes miss notifications.
   
   In case of an existing issue, reference it using one of the following:
   
   * closes: #ISSUE
   * related: #ISSUE
   -->
   
   ---
   
   ##### Was generative AI tooling used to co-author this PR?
   
   <!--
   If generative AI tooling has been used in the process of authoring this PR, 
please
   change below checkbox to `[X]` followed by the name of the tool, uncomment 
the "Generated-by".
   -->
   
   - [x] Yes - claude sonnet
   
   <!--
   Generated-by: [Tool Name] following [the 
guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions)
   -->
   
   ### What
   
   The five `resumable_job.*` counters ship with a single `operator` tag. That 
answers "how often did durable execution avoid redoing work, for which 
operator", but not "for which Dag". Per-Dag is the question people ask when 
they want to know whether the feature is earning its keep on a specific 
pipeline, and today it cannot be answered from these metrics at all.
   
   ### Current behaviour
   
   Every `resumable_job.*` counter carries `operator`, plus `team_name` on 
multi-team deployments. Nothing identifies the Dag or task that produced the 
count, so all executions of every Dag using a given operator collapse into one 
series.
   
   ### Proposed change
   
   Take the whole of `RuntimeTaskInstance.stats_tags` instead of pulling one 
key out of it. That property already assembles the label set every other task 
metric uses, so the counters now carry `dag_id`, `task_id` and `run_type` 
alongside `operator`, with `team_name` continuing to appear where it applies.
   
   The metric descriptions in `metrics_template.yaml` are updated to match.
   
   ### Changes of Note
   
   `stats_tags` also includes author-chosen Dag tags when `[metrics] 
dag_tags_in_metrics` is enabled. These counters now inherit that, same as 
`ti.start` and `ti.finish`.
   
   The alternative was filtering to a fixed key list (`dag_id`, `task_id`, 
`run_type`, `team_name`), which bounds cardinality by construction. I kept the 
consistent behaviour instead, because the config is off by default and a 
deployment that turns it on has opted into that blast radius everywhere else 
already. Happy to switch if reviewers would rather these five counters be 
special-cased.
   
   ### User implications / backcompat
   
   No breaking change. The labels only materialise on backends that carry tags: 
OTel and dogstatsd natively, StatsD only with `[metrics] 
statsd_influxdb_enabled`. That is the same constraint the existing `operator` 
tag already has, not a new one, so plain StatsD deployments see no difference.
   
   Series cardinality on these five counters increases for anyone already 
receiving the tags, bounded by `dag_id` x `task_id` x `operator`.
   
   ### Testing
   
   Using this dag:
   
   ```python
   from __future__ import annotations
   
   import random
   import string
   import time
   from datetime import datetime, timedelta
   from typing import TYPE_CHECKING, Any
   
   from airflow.sdk import DAG, BaseOperator
   from airflow.sdk.bases.resumablejobmixin import ResumableJobMixin
   
   if TYPE_CHECKING:
       from pydantic import JsonValue
   
       from airflow.sdk.definitions.context import Context
   
   _JOBS: dict[str, str] = {}
   
   
   def _generate_job_id() -> str:
       return "batch-" + "".join(random.choices(string.ascii_lowercase + 
string.digits, k=8))
   
   
   def _submit(job_id: str) -> None:
       _JOBS[job_id] = "RUNNING"
   
   
   def _get_status(job_id: str) -> str:
       return _JOBS.get(job_id, "UNKNOWN")
   
   
   def _poll(job_id: str) -> None:
       time.sleep(1)
       _JOBS[job_id] = "SUCCEEDED"
   
   
   # ---------------------------------------------------------------------------
   # Operator
   # ---------------------------------------------------------------------------
   
   
   class MockBatchOperator(ResumableJobMixin, BaseOperator):
       """
       Submits a mock batch job and polls until completion.
   
       Demonstrates how ``ResumableJobMixin`` handles a worker crash between
       submission and the start of polling: on retry the job ID is read from
       ``task_store`` and the operator reconnects rather than resubmitting.
       """
   
       external_id_key = "job_id"
   
       def execute(self, context: Context) -> Any:
           return self.execute_resumable(context)
   
       def submit_job(self, context: Context) -> JsonValue:
           job_id = _generate_job_id()
           _submit(job_id)
           self.log.info("Submitted batch job", job_id=job_id)
           return job_id
   
       def get_job_status(self, external_id: JsonValue, context: Context) -> 
str:
           simulate = context["params"].get("simulate_final_status", "")
           if simulate:
               return simulate
           # Default to RUNNING if the ID is not in the local cache — this 
simulates
           # a real external system that keeps running after a worker crash.
           return _JOBS.get(str(external_id), "RUNNING")
   
       def is_job_active(self, status: str) -> bool:
           return status == "RUNNING"
   
       def is_job_succeeded(self, status: str) -> bool:
           return status == "SUCCEEDED"
   
       def poll_until_complete(self, external_id: JsonValue, context: Context) 
-> None:
           self.log.info("Polling job — kill the worker now to trigger a 
retry", job_id=external_id)
           time.sleep(10)
           self.log.info("Polling job until complete", job_id=external_id)
           _poll(str(external_id))
   
       def get_job_result(self, external_id: JsonValue, context: Context) -> 
str:
           return f"Job {external_id} finished with status 
{_get_status(str(external_id))}"
   
   
   with DAG(
       dag_id="example_resumable_job",
       schedule=None,
       start_date=datetime(2026, 1, 1),
       catchup=False,
       tags=["example", "resumable", "task-store"],
       doc_md=__doc__,
       params={"simulate_final_status": ""},
   ):
       MockBatchOperator(
           task_id="run_batch_job",
           retries=2,
           retry_delay=timedelta(seconds=5),
       )
   
   ```
   
   #### Case 1: Running it normally without interruption
   
   <img width="1650" height="809" alt="image" 
src="https://github.com/user-attachments/assets/b3a01f81-a909-4e27-9680-28141f15f810";
 />
   
   
   Metrics have the added tags now:
   ```shell
   amoghrajesh.desai  …/airflow   
add-more-ti-stats-to-resumablemixin-counters $!?    orbstack  ♥ 12:21  
curl -s localhost:28889/metrics | grep '^airflow_resumable_job'
   
   
airflow_resumable_job_fresh_submit_total{dag_id="example_resumable_job",instance="36b43c68-344c-4222-ad4a-3c93128716d2",job="test",operator="MockBatchOperator",otel_scope_name="airflow.sdk._shared.observability.metrics.otel_logger",otel_scope_schema_url="",otel_scope_version="",run_type="manual",task_id="run_batch_job"}
 1
   ```
   
   Before this change the same line read `{operator="MockBatchOperator"}`.
   
   
   #### Case 2: Running with `simulate_final_status` as `SUCCEEDED`
   
   Crashed the worker when the task waits for it
   
   <img width="1650" height="809" alt="image" 
src="https://github.com/user-attachments/assets/15f5a1a9-e1f5-4ca6-9f34-c7f21b2b469e";
 />
   
   
   <img width="1650" height="809" alt="image" 
src="https://github.com/user-attachments/assets/273070ad-484b-41ab-a859-2151fa3c217d";
 />
   
   
   Now the metrics:
   
   ```shell
   
airflow_resumable_job_already_succeeded_total{dag_id="example_resumable_job",instance="36ec9a2f-9faf-475e-93b8-6590f4fc4a73",job="test",operator="MockBatchOperator",otel_scope_name="airflow.sdk._shared.observability.metrics.otel_logger",otel_scope_schema_url="",otel_scope_version="",run_type="manual",task_id="run_batch_job"}
 1
   
airflow_resumable_job_fresh_submit_total{dag_id="example_resumable_job",instance="36b43c68-344c-4222-ad4a-3c93128716d2",job="test",operator="MockBatchOperator",otel_scope_name="airflow.sdk._shared.observability.metrics.otel_logger",otel_scope_schema_url="",otel_scope_version="",run_type="manual",task_id="run_batch_job"}
 1
   
airflow_resumable_job_reconnect_attempt_total{dag_id="example_resumable_job",instance="36ec9a2f-9faf-475e-93b8-6590f4fc4a73",job="test",operator="MockBatchOperator",otel_scope_name="airflow.sdk._shared.observability.metrics.otel_logger",otel_scope_schema_url="",otel_scope_version="",run_type="manual",task_id="run_batch_job"}
 1
   ```
   
   
   
   ---
   
   * Read the **[Pull Request 
Guidelines](https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#pull-request-guidelines)**
 for more information. Note: commit author/co-author name and email in commits 
become permanently public when merged.
   * For fundamental code changes, an Airflow Improvement Proposal 
([AIP](https://cwiki.apache.org/confluence/display/AIRFLOW/Airflow+Improvement+Proposals))
 is needed.
   * When adding dependency, check compliance with the [ASF 3rd Party License 
Policy](https://www.apache.org/legal/resolved.html#category-x).
   * For significant user-facing changes create newsfragment: 
`{pr_number}.significant.rst`, in 
[airflow-core/newsfragments](https://github.com/apache/airflow/tree/main/airflow-core/newsfragments).
 You can add this file in a follow-up commit after the PR is created so you 
know the PR number.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to