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

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


The following commit(s) were added to refs/heads/v3-2-test by this push:
     new 727f12c4d0c Fix millisecond floating point duration bug (#66560) 
(#66915)
727f12c4d0c is described below

commit 727f12c4d0ce2e5564ef741bf5a4a5edfa9f82e9
Author: Rahul Vats <[email protected]>
AuthorDate: Thu May 14 19:53:04 2026 +0530

    Fix millisecond floating point duration bug (#66560) (#66915)
    
    * Fix millisecond floating point duration bug
    
    
    (cherry picked from commit 52098c2b89f18ff77049aa4f1dcab4b922579d42)
    
    Co-authored-by: Brent Bovenzi <[email protected]>
    Co-authored-by: hojeong park <[email protected]>
---
 .../src/airflow/ui/src/utils/datetimeUtils.test.ts | 24 ++++++++++++++++++---
 .../src/airflow/ui/src/utils/datetimeUtils.ts      | 25 ++++++++++++++--------
 2 files changed, 37 insertions(+), 12 deletions(-)

diff --git a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts 
b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts
index a0a39211257..dac233789d5 100644
--- a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts
+++ b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.test.ts
@@ -16,11 +16,15 @@
  * specific language governing permissions and limitations
  * under the License.
  */
+import dayjs from "dayjs";
+import dayjsDuration from "dayjs/plugin/duration";
 import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
 
 import { getDuration, renderDuration, getRelativeTime } from "./datetimeUtils";
 
-describe("getDuration", () => {
+dayjs.extend(dayjsDuration);
+
+describe("getDuration & formatDuration", () => {
   it("handles durations less than 60 seconds", () => {
     const start = "2024-03-14T10:00:00.000Z";
     const end = "2024-03-14T10:00:05.5111111Z";
@@ -72,11 +76,25 @@ describe("getDuration", () => {
     const start = "2024-03-14T10:00:00.000Z";
 
     // eslint-disable-next-line unicorn/no-null
-    expect(getDuration(start, null)).toBe("00:00:10.000");
-    expect(getDuration(start, undefined)).toBe("00:00:10.000");
+    expect(getDuration(start, null)).toBe("00:00:10");
+    expect(getDuration(start, undefined)).toBe("00:00:10");
 
     vi.useRealTimers();
   });
+
+  it("handles both numbers and duration objects", () => {
+    expect(renderDuration(dayjs.duration(10, "seconds"))).toBe("00:00:10");
+    expect(renderDuration(10)).toBe("00:00:10");
+  });
+
+  it("handles floating point milliseconds", () => {
+    expect(renderDuration(dayjs.duration(10.000_499_738, 
"seconds"))).toBe("00:00:10");
+    expect(renderDuration(10.000_499_738)).toBe("00:00:10");
+    expect(renderDuration(dayjs.duration(10.000_500, 
"seconds"))).toBe("00:00:10.001");
+    expect(renderDuration(10.000_500)).toBe("00:00:10.001");
+    expect(renderDuration(dayjs.duration(10.838_999_738, 
"seconds"))).toBe("00:00:10.839");
+    expect(renderDuration(10.838_999_738)).toBe("00:00:10.839");
+  });
 });
 
 describe("getRelativeTime", () => {
diff --git a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts 
b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
index 773e6d2b072..77e08fa0a88 100644
--- a/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
+++ b/airflow-core/src/airflow/ui/src/utils/datetimeUtils.ts
@@ -29,22 +29,29 @@ export const DEFAULT_DATETIME_FORMAT = "YYYY-MM-DD 
HH:mm:ss";
 export const DEFAULT_DATETIME_FORMAT_WITH_TZ = `${DEFAULT_DATETIME_FORMAT} z`;
 
 export const renderDuration = (
-  durationSeconds: number | null | undefined,
+  durationSeconds: dayjsDuration.Duration | number | null | undefined,
   withMilliseconds: boolean = true,
 ): string | undefined => {
-  if (durationSeconds === null || durationSeconds === undefined || 
durationSeconds <= 0.01) {
+  if (durationSeconds === null || durationSeconds === undefined) {
+    return undefined;
+  }
+
+  // Handle floating point milliseconds
+  const duration = dayjs.isDuration(durationSeconds)
+    ? dayjs.duration(Math.round(durationSeconds.asMilliseconds()))
+    : dayjs.duration(Number(durationSeconds.toFixed(3)), "seconds");
+
+  if (duration.asMilliseconds() < 1) {
     return undefined;
   }
 
   // If under 60 seconds, render milliseconds
-  if (durationSeconds < 60 && withMilliseconds) {
-    return dayjs.duration(Number(durationSeconds.toFixed(3)), 
"seconds").format("HH:mm:ss.SSS");
+  if (duration.asSeconds() < 60 && duration.milliseconds() > 0 && 
withMilliseconds) {
+    return duration.format("HH:mm:ss.SSS");
   }
 
   // If under 1 day, render as HH:mm:ss otherwise include the number of days
-  return durationSeconds < 86_400
-    ? dayjs.duration(durationSeconds, "seconds").format("HH:mm:ss")
-    : dayjs.duration(durationSeconds, "seconds").format("D[d]HH:mm:ss");
+  return duration.asSeconds() < 86_400 ? duration.format("HH:mm:ss") : 
duration.format("D[d]HH:mm:ss");
 };
 
 export const getDuration = (
@@ -57,9 +64,9 @@ export const getDuration = (
   }
 
   const end = endDate ?? dayjs().toISOString();
-  const seconds = dayjs.duration(dayjs(end).diff(startDate)).asSeconds();
+  const milliseconds = dayjs.duration(dayjs(end).diff(startDate));
 
-  return renderDuration(seconds, withMilliseconds);
+  return renderDuration(milliseconds, withMilliseconds);
 };
 
 export const formatDate = (

Reply via email to