Skip to content

Commit 80a90fc

Browse files
author
Ravi Patel
committed
fix(webapp): prevent IEEE 754 precision loss on OTLP nanosecond conversions
Fixes #3292 Root Cause: Converting epoch milliseconds to nanoseconds via `BigInt(ms * 1_000_000)` performs multiplication in IEEE 754 float representation before casting to BigInt. Because epoch ms * 1e6 is ~1.7e18 and exceeds Number.MAX_SAFE_INTEGER (~9e15), the lowest bits lose precision, creating +-256ns non-deterministic drift in span ordering and duration metrics. Resolution: Cast epoch ms to BigInt before multiplying by BigInt(1_000_000) across all webapp event repository and run engine call sites.
1 parent 9f8cc32 commit 80a90fc

4 files changed

Lines changed: 29 additions & 4 deletions

File tree

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { describe, it, expect } from "vitest";
2+
import { getNowInNanoseconds, calculateDurationFromStart } from "./common.server";
3+
4+
describe("OTLP nanosecond precision", () => {
5+
it("guarantees exact integer arithmetic without float truncation", () => {
6+
const sampleMs = 1729200000123;
7+
const sampleDate = new Date(sampleMs);
8+
const expectedNano = BigInt(sampleMs) * 1_000_000n;
9+
10+
// Buggy float math produces 1729200000123000064n (+64ns error)
11+
const buggyNano = BigInt(sampleMs * 1_000_000);
12+
expect(buggyNano).not.toBe(expectedNano);
13+
14+
// Fixed calculation produces exact 1729200000123000000n
15+
const fixedNano = BigInt(sampleDate.getTime()) * BigInt(1_000_000);
16+
expect(fixedNano).toBe(expectedNano);
17+
});
18+
19+
it("preserves exact duration calculation", () => {
20+
const start = 1729200000000n * 1_000_000n;
21+
const end = new Date(1729200000500); // 500ms later
22+
const duration = calculateDurationFromStart(end, start);
23+
expect(duration).toBe(500_000_000);
24+
});
25+
});

‎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 BigInt(new Date().getTime()) * BigInt(1_000_000);
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(BigInt($endtime.getTime()) * BigInt(1_000_000) - 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: {

0 commit comments

Comments
 (0)