Skip to content

Commit d68ba3d

Browse files
committed
fix(webapp): convert epoch ms to BigInt before nanosecond multiplication
Multiplying epoch milliseconds by 1e6 as a Number exceeds Number.MAX_SAFE_INTEGER, so the product is rounded to a multiple of 256 before it is converted to BigInt. Span start times and durations could therefore be off by up to a few hundred nanoseconds. Fixes #3292
1 parent ed97cee commit d68ba3d

4 files changed

Lines changed: 45 additions & 4 deletions

File tree

‎apps/webapp/app/v3/eventRepository/common.server.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export function extractContextFromCarrier(carrier: Record<string, unknown>) {
2626
}
2727

2828
export function getNowInNanoseconds(): bigint {
29-
return BigInt(new Date().getTime() * 1_000_000);
29+
return convertDateToNanoseconds(new Date());
3030
}
3131

3232
export function getDateFromNanoseconds(nanoseconds: bigint): Date {
@@ -40,7 +40,7 @@ export function calculateDurationFromStart(
4040
) {
4141
const $endtime = typeof endTime === "string" ? new Date(endTime) : endTime;
4242

43-
const duration = Number(BigInt($endtime.getTime() * 1_000_000) - startTime);
43+
const duration = Number(convertDateToNanoseconds($endtime) - startTime);
4444

4545
if (minimumDuration && duration < minimumDuration) {
4646
return minimumDuration;

‎apps/webapp/app/v3/eventRepository/index.server.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,7 +208,7 @@ async function recordRunEvent(
208208
runId: foundRun.friendlyId,
209209
...attributes,
210210
},
211-
startTime: BigInt((startTime?.getTime() ?? Date.now()) * 1_000_000),
211+
startTime: BigInt(startTime?.getTime() ?? Date.now()) * BigInt(1_000_000),
212212
...optionsRest,
213213
});
214214

‎apps/webapp/app/v3/runEngineHandlers.server.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ export function registerRunEngineEventBusHandlers() {
557557
);
558558

559559
await eventRepository.recordEvent(retryMessage, {
560-
startTime: BigInt(time.getTime() * 1000000),
560+
startTime: BigInt(time.getTime()) * BigInt(1_000_000),
561561
taskSlug: run.taskIdentifier,
562562
environment,
563563
attributes: {
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import { afterEach, describe, expect, it, vi } from "vitest";
2+
import {
3+
calculateDurationFromStart,
4+
convertDateToNanoseconds,
5+
getNowInNanoseconds,
6+
} from "~/v3/eventRepository/common.server";
7+
8+
// 1700000000001 * 1e6 is not representable as a double (spacing is 256 at ~1.7e18),
9+
// so Number multiplication before BigInt conversion rounds it to ...000999936.
10+
const unsafeMs = 1_700_000_000_001;
11+
const unsafeNs = 1_700_000_000_001_000_000n;
12+
13+
describe("eventRepository nanosecond conversions", () => {
14+
afterEach(() => {
15+
vi.useRealTimers();
16+
});
17+
18+
it("convertDateToNanoseconds keeps exact precision beyond MAX_SAFE_INTEGER", () => {
19+
expect(convertDateToNanoseconds(new Date(unsafeMs))).toBe(unsafeNs);
20+
});
21+
22+
it("getNowInNanoseconds keeps exact precision beyond MAX_SAFE_INTEGER", () => {
23+
vi.useFakeTimers();
24+
vi.setSystemTime(unsafeMs);
25+
26+
expect(getNowInNanoseconds()).toBe(unsafeNs);
27+
expect(getNowInNanoseconds() % 1_000_000n).toBe(0n);
28+
});
29+
30+
it("calculateDurationFromStart is exact for sub-millisecond-sensitive end times", () => {
31+
const startTime = convertDateToNanoseconds(new Date(unsafeMs - 1));
32+
33+
expect(calculateDurationFromStart(startTime, new Date(unsafeMs))).toBe(1_000_000);
34+
});
35+
36+
it("calculateDurationFromStart applies the minimum duration", () => {
37+
const startTime = convertDateToNanoseconds(new Date(unsafeMs - 1));
38+
39+
expect(calculateDurationFromStart(startTime, new Date(unsafeMs), 5_000_000)).toBe(5_000_000);
40+
});
41+
});

0 commit comments

Comments
 (0)