You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@airflow.apache.org by ra...@apache.org on 2026/08/03 13:11:42 UTC

(airflow) branch v3-3-test updated: [v3-3-test] Fix Dag run duration stats crash on PostgreSQL 14+ (#70833) (#70964)

This is an automated email from the ASF dual-hosted git repository.

vatsrahul1001 pushed a commit to branch v3-3-test
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/v3-3-test by this push:
     new d172ab22a61 [v3-3-test] Fix Dag run duration stats crash on PostgreSQL 14+ (#70833) (#70964)
d172ab22a61 is described below

commit d172ab22a6163f558d606418471768b3e43a92d9
Author: github-actions[bot] <41...@users.noreply.github.com>
AuthorDate: Mon Aug 3 18:41:32 2026 +0530

    [v3-3-test] Fix Dag run duration stats crash on PostgreSQL 14+ (#70833) (#70964)
    
    PostgreSQL 14 changed EXTRACT(epoch FROM ...) to return numeric rather than double precision, so DagRun.duration is hydrated as Decimal on Postgres 14+. The duration-stats percentile interpolation then multiplies a Decimal by a float, which Python disallows, so the Dag run stats endpoint raises a TypeError for any Dag that has completed runs.
    (cherry picked from commit 30006eb9f8c76b42beb349b5c1d859678a641253)
    
    Co-authored-by: Pierre Jeambrun <pi...@gmail.com>
---
 .../api_fastapi/core_api/services/ui/dag_run.py    |  4 ++-
 .../core_api/services/ui/test_dag_run.py           | 41 ++++++++++++++++++++++
 2 files changed, 44 insertions(+), 1 deletion(-)

diff --git a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/dag_run.py b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/dag_run.py
index a9ba75417c8..2a3ab183252 100644
--- a/airflow-core/src/airflow/api_fastapi/core_api/services/ui/dag_run.py
+++ b/airflow-core/src/airflow/api_fastapi/core_api/services/ui/dag_run.py
@@ -35,7 +35,9 @@ def compute_duration_stats(durations: list[float]) -> DurationStats | None:
     if not durations:
         return None
 
-    sorted_d = sorted(durations)
+    # On Postgres 14+, DagRun.duration comes back as Decimal (EXTRACT(epoch ...) returns
+    # numeric); coerce to float so the percentile interpolation below (Decimal * float) works.
+    sorted_d = sorted(float(d) for d in durations)
 
     counts = Counter(round(d) for d in sorted_d)
     max_count = max(counts.values())
diff --git a/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_dag_run.py b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_dag_run.py
new file mode 100644
index 00000000000..afa9c5277e2
--- /dev/null
+++ b/airflow-core/tests/unit/api_fastapi/core_api/services/ui/test_dag_run.py
@@ -0,0 +1,41 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+from __future__ import annotations
+
+from decimal import Decimal
+
+import pytest
+
+from airflow.api_fastapi.core_api.services.ui.dag_run import compute_duration_stats
+
+
+@pytest.mark.parametrize("cast", [float, Decimal], ids=["float", "decimal"])
+def test_compute_duration_stats_handles_float_and_decimal(cast):
+    """Durations arrive as Decimal on Postgres 14+ (EXTRACT(epoch ...) returns numeric) and as
+    float/int on other backends; the stats must compute for either without raising TypeError."""
+    durations = [cast(v) for v in (10, 20, 20, 30, 40)]
+
+    stats = compute_duration_stats(durations)
+
+    assert stats is not None
+    assert stats.mean == 24.0
+    assert stats.mode == 20.0
+    assert stats.p50 == 20.0
+    assert stats.p90 == 36.0
+    assert stats.p95 == 38.0
+    assert stats.p99 == 39.6