98 lines
3.8 KiB
TypeScript
98 lines
3.8 KiB
TypeScript
import { RunId } from "@trigger.dev/core/v3/isomorphic";
|
|
import type { MollifierBuffer } from "@trigger.dev/redis-worker";
|
|
import { serialiseMollifierSnapshot, type MollifierSnapshot } from "./mollifierSnapshot.server";
|
|
import type { TripDecision } from "./mollifierGate.server";
|
|
|
|
export type MollifyNotice = {
|
|
code: "mollifier.queued";
|
|
message: string;
|
|
docs: string;
|
|
};
|
|
|
|
export type MollifySyntheticResult = {
|
|
// `id` is the canonical TaskRun primary key derived from `friendlyId`
|
|
// via `RunId.fromFriendlyId`. Downstream consumers in the trigger
|
|
// route — notably `saveRequestIdempotency` — index the request-
|
|
// idempotency cache by this id; without it the cache stores
|
|
// `undefined` and Prisma's `findFirst({ where: { id: undefined } })`
|
|
// on retry strips the predicate and returns an arbitrary TaskRun
|
|
// (potential cross-tenant leak). Always populated.
|
|
//
|
|
// `spanId` is the root-span id allocated at gate-accept time and
|
|
// stored in the snapshot. Callers like the dashboard's Test action
|
|
// use it to build a `v3RunSpanPath` URL that auto-opens the right
|
|
// details panel — without it, the buffered run lands on the
|
|
// run-detail page with no span selected (parity gap with PG runs).
|
|
run: { id: string; friendlyId: string; spanId: string };
|
|
error: undefined;
|
|
// The race-loser path: if accept's SETNX hit an existing
|
|
// buffered run with the same (env, task, idempotencyKey), the
|
|
// response echoes the winner's runId with isCached=true. The
|
|
// mollifier-queued notice is only attached for the happy accept.
|
|
isCached: boolean;
|
|
notice?: MollifyNotice;
|
|
};
|
|
|
|
const NOTICE: MollifyNotice = {
|
|
code: "mollifier.queued",
|
|
message: "Trigger accepted into burst buffer. Consider batchTrigger for fan-outs of 100+.",
|
|
docs: "https://trigger.dev/docs/management/tasks/batch-trigger",
|
|
};
|
|
|
|
export async function mollifyTrigger(args: {
|
|
runFriendlyId: string;
|
|
environmentId: string;
|
|
organizationId: string;
|
|
engineTriggerInput: MollifierSnapshot;
|
|
decision: Extract<TripDecision, { divert: true }>;
|
|
buffer: MollifierBuffer;
|
|
// Optional idempotency context. When both are passed, accept SETNXes
|
|
// the lookup so the buffered window participates in trigger-time
|
|
// dedup symmetrically with PG.
|
|
idempotencyKey?: string;
|
|
taskIdentifier?: string;
|
|
}): Promise<MollifySyntheticResult> {
|
|
const result = await args.buffer.accept({
|
|
runId: args.runFriendlyId,
|
|
envId: args.environmentId,
|
|
orgId: args.organizationId,
|
|
payload: serialiseMollifierSnapshot(args.engineTriggerInput),
|
|
idempotencyKey: args.idempotencyKey,
|
|
taskIdentifier: args.taskIdentifier,
|
|
});
|
|
|
|
if (result.kind === "duplicate_idempotency") {
|
|
// Race loser. Echo the winner's runId so the SDK's response shape
|
|
// matches PG-side idempotency cache hits. The winner's spanId isn't
|
|
// readily available without a second buffer fetch; an empty string
|
|
// causes `v3RunSpanPath` to omit the `?span=` param, which matches
|
|
// current behaviour for cached PG responses.
|
|
return {
|
|
run: {
|
|
id: RunId.fromFriendlyId(result.existingRunId),
|
|
friendlyId: result.existingRunId,
|
|
spanId: "",
|
|
},
|
|
error: undefined,
|
|
isCached: true,
|
|
};
|
|
}
|
|
|
|
// Both "accepted" and "duplicate_run_id" produce the same customer-
|
|
// visible response: a buffered-trigger acknowledgement. The duplicate
|
|
// runId case is unreachable in practice (runIds are server-generated
|
|
// and unique) but is silently idempotent at the buffer layer either way.
|
|
const rawSpanId = args.engineTriggerInput.spanId;
|
|
const spanId = typeof rawSpanId === "string" ? rawSpanId : "";
|
|
return {
|
|
run: {
|
|
id: RunId.fromFriendlyId(args.runFriendlyId),
|
|
friendlyId: args.runFriendlyId,
|
|
spanId,
|
|
},
|
|
error: undefined,
|
|
isCached: false,
|
|
notice: NOTICE,
|
|
};
|
|
}
|