Skip to content

Commit 648409d

Browse files
committed
perf(webapp,core): time-encode webhook delivery ids to prune the detail lookup
The delivery detail point lookup queried WebhookDelivery by id and environment only. The table is RANGE-partitioned on createdAt, so with no createdAt predicate Postgres cannot prune and probes every daily partition. The delivery id now embeds its mint timestamp (a 6-byte big-endian unix ms prefix plus random bytes, base32hex encoded), and the engine stores that same timestamp as the row's createdAt, so the id's timestamp is the partition key. getDelivery recovers it from the friendlyId and adds it as an exact predicate, pruning to the row's partition for every caller. A legacy id that does not decode falls back to the unpruned lookup.
1 parent 8c42b04 commit 648409d

4 files changed

Lines changed: 136 additions & 9 deletions

File tree

apps/webapp/app/services/webhookDeliveriesRepository/clickhouseWebhookDeliveriesRepository.server.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { type ClickhouseQueryBuilder } from "@internal/clickhouse";
2+
import { WebhookDeliveryId } from "@trigger.dev/core/v3/isomorphic";
23
import { boundedIn } from "@trigger.dev/database";
34
import { decodeRunsCursor, encodeRunsCursor } from "../runsRepository/runsCursor.server";
45
import {
@@ -281,14 +282,23 @@ export class ClickHouseWebhookDeliveriesRepository implements IWebhookDeliveries
281282
* A point lookup: pure Postgres, never ClickHouse. `friendlyId` is `whd_` + the row id, so we
282283
* strip the prefix and hit the composite-PK index (scoped by environment). ClickHouse is for
283284
* aggregations and for filtering/ordering a list into ids, never for selecting a row's columns.
285+
*
286+
* `WebhookDelivery` is RANGE-partitioned on `createdAt`, so a bare `id` predicate can't prune and
287+
* probes every partition. The delivery id is time-encoded (see `WebhookDeliveryId`) with the same
288+
* timestamp the engine stores as `createdAt`, so we recover it from the id and add it as an exact
289+
* predicate to prune to the row's partition. A legacy id that doesn't decode falls back to the
290+
* unpruned lookup.
284291
*/
285292
async getDelivery(options: GetWebhookDeliveryOptions): Promise<DetailedWebhookDelivery | null> {
286-
const id = options.friendlyId.startsWith(WEBHOOK_DELIVERY_ID_PREFIX)
287-
? options.friendlyId.slice(WEBHOOK_DELIVERY_ID_PREFIX.length)
288-
: options.friendlyId;
293+
const id = WebhookDeliveryId.toId(options.friendlyId);
294+
const createdAt = WebhookDeliveryId.parseTimestamp(options.friendlyId);
289295

290296
return this.options.prisma.webhookDelivery.findFirst({
291-
where: { id, runtimeEnvironmentId: options.environmentId },
297+
where: {
298+
id,
299+
runtimeEnvironmentId: options.environmentId,
300+
...(createdAt ? { createdAt } : {}),
301+
},
292302
select: DELIVERY_DETAIL_SELECT,
293303
});
294304
}

internal-packages/webhook-engine/src/engine/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,7 @@ export class WebhookEngine {
229229
: undefined;
230230

231231
const gateKey = `webhookdedupe:${endpoint.id}:${idempotencyKey}`;
232-
const { id, friendlyId } = WebhookDeliveryId.generate();
233-
const createdAt = new Date();
232+
const { id, friendlyId, timestamp: createdAt } = WebhookDeliveryId.generate();
234233

235234
const claimed = await this.frontGate.set(
236235
gateKey,
@@ -377,8 +376,7 @@ export class WebhookEngine {
377376
return { outcome: "unsupported_target" };
378377
}
379378

380-
const { id, friendlyId } = WebhookDeliveryId.generate();
381-
const createdAt = new Date();
379+
const { id, friendlyId, timestamp: createdAt } = WebhookDeliveryId.generate();
382380

383381
await this.prisma.webhookDelivery.create({
384382
data: {

packages/core/src/v3/isomorphic/friendlyId.test.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
WaitpointId,
55
SnapshotId,
66
QueueId,
7+
WebhookDeliveryId,
78
RUN_OPS_ID_LENGTH,
89
RUN_OPS_ID_REGION_INDEX,
910
RUN_OPS_ID_VERSION,
@@ -201,3 +202,51 @@ describe("firekeeper pod-name round-trip (runner-<id>[-attempt-N] → run_<id>)"
201202
expect(podName.length).toBeLessThanOrEqual(63);
202203
});
203204
});
205+
206+
describe("WebhookDeliveryId (time-encoded)", () => {
207+
it("generate() round-trips and parseTimestamp recovers the exact mint timestamp", () => {
208+
const { id, friendlyId, timestamp } = WebhookDeliveryId.generate();
209+
210+
expect(friendlyId).toBe(`whd_${id}`);
211+
expect(id.length).toBe(25);
212+
expect(WebhookDeliveryId.toId(friendlyId)).toBe(id);
213+
expect(WebhookDeliveryId.toId(id)).toBe(id);
214+
expect(WebhookDeliveryId.toFriendlyId(id)).toBe(friendlyId);
215+
expect(WebhookDeliveryId.parseTimestamp(friendlyId)?.getTime()).toBe(timestamp.getTime());
216+
expect(WebhookDeliveryId.parseTimestamp(id)?.getTime()).toBe(timestamp.getTime());
217+
});
218+
219+
it("encodes the wall-clock mint time so the partition key is recoverable", () => {
220+
vi.useFakeTimers();
221+
try {
222+
const minted = new Date("2026-08-09T12:34:56.789Z");
223+
vi.setSystemTime(minted);
224+
const { friendlyId, timestamp } = WebhookDeliveryId.generate();
225+
expect(timestamp.getTime()).toBe(minted.getTime());
226+
expect(WebhookDeliveryId.parseTimestamp(friendlyId)?.toISOString()).toBe(
227+
"2026-08-09T12:34:56.789Z"
228+
);
229+
} finally {
230+
vi.useRealTimers();
231+
}
232+
});
233+
234+
it("sorts lexicographically in mint order at millisecond resolution", () => {
235+
vi.useFakeTimers();
236+
try {
237+
vi.setSystemTime(new Date("2026-08-09T00:00:00.000Z"));
238+
const first = WebhookDeliveryId.generate().id;
239+
vi.setSystemTime(new Date("2026-08-09T00:00:00.001Z"));
240+
const second = WebhookDeliveryId.generate().id;
241+
expect(first < second).toBe(true);
242+
} finally {
243+
vi.useRealTimers();
244+
}
245+
});
246+
247+
it("returns undefined for legacy or malformed ids so callers skip pruning", () => {
248+
expect(WebhookDeliveryId.parseTimestamp("whd_tooShort")).toBeUndefined();
249+
expect(WebhookDeliveryId.parseTimestamp(`whd_${"z".repeat(24)}1`)).toBeUndefined();
250+
expect(WebhookDeliveryId.parseTimestamp(`whd_${"0".repeat(24)}9`)).toBeUndefined();
251+
});
252+
});

packages/core/src/v3/isomorphic/friendlyId.ts

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,77 @@ export const AttemptId = new IdUtil("attempt");
268268
export const ErrorId = new IdUtil("error");
269269
export const SessionId = new IdUtil("session");
270270
export const WebhookEndpointId = new IdUtil("wh"); // wh_...
271-
export const WebhookDeliveryId = new IdUtil("whd"); // whd_...
271+
272+
/**
273+
* Webhook delivery id: time-encoded so the partition key (`createdAt`) is recoverable from the id.
274+
* The body is `base32hex(6-byte big-endian unix ms timestamp + 9 CSPRNG bytes)` (24 chars) plus a
275+
* version char "1", prefixed `whd_`. `WebhookDelivery` is RANGE-partitioned on `createdAt`, and the
276+
* engine sets the row's `createdAt` to the mint timestamp, so a point lookup by id can recover the
277+
* partition from the id alone instead of threading `createdAt` through every caller. Legacy (cuid)
278+
* ids decode to `undefined` so callers fall back to an unpruned lookup.
279+
*/
280+
const WEBHOOK_DELIVERY_ID_PREFIX = "whd";
281+
const WEBHOOK_DELIVERY_ID_TIMESTAMP_BYTES = 6;
282+
const WEBHOOK_DELIVERY_ID_CORE_BYTES = 15;
283+
const WEBHOOK_DELIVERY_ID_CORE_LENGTH = 24;
284+
const WEBHOOK_DELIVERY_ID_VERSION = "1";
285+
const WEBHOOK_DELIVERY_ID_BODY_LENGTH = 25;
286+
287+
function webhookDeliveryIdBody(idOrFriendlyId: string): string {
288+
return idOrFriendlyId.startsWith(`${WEBHOOK_DELIVERY_ID_PREFIX}_`)
289+
? idOrFriendlyId.slice(WEBHOOK_DELIVERY_ID_PREFIX.length + 1)
290+
: idOrFriendlyId;
291+
}
292+
293+
export const WebhookDeliveryId = {
294+
/**
295+
* Mint a delivery id. The row's `createdAt` MUST be set to the returned `timestamp` so the id's
296+
* embedded timestamp equals the partition key that {@link WebhookDeliveryId.parseTimestamp} recovers.
297+
*/
298+
generate(): { id: string; friendlyId: string; timestamp: Date } {
299+
const timestamp = new Date();
300+
const core = new Uint8Array(WEBHOOK_DELIVERY_ID_CORE_BYTES);
301+
let ms = timestamp.getTime();
302+
for (let i = WEBHOOK_DELIVERY_ID_TIMESTAMP_BYTES - 1; i >= 0; i--) {
303+
core[i] = ms % 256;
304+
ms = Math.floor(ms / 256);
305+
}
306+
getRandomValues(core.subarray(WEBHOOK_DELIVERY_ID_TIMESTAMP_BYTES));
307+
const id = `${base32hexEncode(core)}${WEBHOOK_DELIVERY_ID_VERSION}`;
308+
return { id, friendlyId: `${WEBHOOK_DELIVERY_ID_PREFIX}_${id}`, timestamp };
309+
},
310+
311+
toFriendlyId(id: string): string {
312+
return id.startsWith(`${WEBHOOK_DELIVERY_ID_PREFIX}_`)
313+
? id
314+
: `${WEBHOOK_DELIVERY_ID_PREFIX}_${id}`;
315+
},
316+
317+
toId(idOrFriendlyId: string): string {
318+
return webhookDeliveryIdBody(idOrFriendlyId);
319+
},
320+
321+
/**
322+
* Decode the mint timestamp (== the row's `createdAt` partition key) from a v1 id or friendlyId.
323+
* Returns `undefined` for a legacy (cuid) id, which signals the caller to skip partition pruning.
324+
*/
325+
parseTimestamp(idOrFriendlyId: string): Date | undefined {
326+
const body = webhookDeliveryIdBody(idOrFriendlyId);
327+
if (body.length !== WEBHOOK_DELIVERY_ID_BODY_LENGTH) return undefined;
328+
if (body[WEBHOOK_DELIVERY_ID_CORE_LENGTH] !== WEBHOOK_DELIVERY_ID_VERSION) return undefined;
329+
let core: Uint8Array;
330+
try {
331+
core = base32hexDecode(body.slice(0, WEBHOOK_DELIVERY_ID_CORE_LENGTH));
332+
} catch {
333+
return undefined;
334+
}
335+
let ms = 0;
336+
for (let i = 0; i < WEBHOOK_DELIVERY_ID_TIMESTAMP_BYTES; i++) {
337+
ms = ms * 256 + (core[i] ?? 0);
338+
}
339+
return new Date(ms);
340+
},
341+
};
272342

273343
export class IdGenerator {
274344
private alphabet: string;

0 commit comments

Comments
 (0)