156 lines
5.6 KiB
TypeScript
156 lines
5.6 KiB
TypeScript
import { type BatchTaskRunStatus, type Prisma } from "@trigger.dev/database";
|
|
import { type PrismaClientOrTransaction, type PrismaReplicaClient } from "~/db.server";
|
|
import { findDisplayableEnvironment } from "~/models/runtimeEnvironment.server";
|
|
import { engine } from "~/v3/runEngine.server";
|
|
import { readThroughRun } from "~/v3/runOpsMigration/readThrough.server";
|
|
import { BasePresenter } from "./basePresenter.server";
|
|
|
|
type BatchPresenterOptions = {
|
|
environmentId: string;
|
|
batchId: string;
|
|
userId?: string;
|
|
};
|
|
|
|
// Shared by the read-through closures and the passthrough so every store path returns
|
|
// a byte-identical row shape.
|
|
const BATCH_SELECT = {
|
|
id: true,
|
|
friendlyId: true,
|
|
status: true,
|
|
runCount: true,
|
|
batchVersion: true,
|
|
createdAt: true,
|
|
updatedAt: true,
|
|
completedAt: true,
|
|
processingStartedAt: true,
|
|
processingCompletedAt: true,
|
|
successfulRunCount: true,
|
|
failedRunCount: true,
|
|
idempotencyKey: true,
|
|
errors: {
|
|
select: {
|
|
id: true,
|
|
index: true,
|
|
taskIdentifier: true,
|
|
error: true,
|
|
errorCode: true,
|
|
createdAt: true,
|
|
},
|
|
orderBy: {
|
|
index: "asc",
|
|
},
|
|
},
|
|
} satisfies Prisma.BatchTaskRunSelect;
|
|
|
|
type BatchRow = Prisma.BatchTaskRunGetPayload<{ select: typeof BATCH_SELECT }>;
|
|
|
|
type BatchPresenterDeps = {
|
|
/** Resolved boot constant; never awaited per-request when supplied. */
|
|
splitEnabled?: boolean;
|
|
newClient?: PrismaReplicaClient;
|
|
legacyReplica?: PrismaReplicaClient;
|
|
readThrough?: typeof readThroughRun;
|
|
resolveDisplayableEnvironment?: typeof findDisplayableEnvironment;
|
|
};
|
|
|
|
export type BatchPresenterData = Awaited<ReturnType<BatchPresenter["call"]>>;
|
|
|
|
export class BatchPresenter extends BasePresenter {
|
|
constructor(
|
|
_prisma?: PrismaClientOrTransaction,
|
|
_replica?: PrismaClientOrTransaction,
|
|
private readonly deps: BatchPresenterDeps = {}
|
|
) {
|
|
super(_prisma, _replica);
|
|
}
|
|
|
|
public async call({ environmentId, batchId, userId }: BatchPresenterOptions) {
|
|
// Reads the BatchTaskRun (run-ops) via the read-through layer: split on -> new run-ops
|
|
// first, then the LEGACY RUN-OPS READ REPLICA only for not-yet-migrated batches (never the
|
|
// legacy primary); split off (single-DB / self-host) -> one plain batchTaskRun.findFirst
|
|
// (passthrough). The runtimeEnvironment (control-plane) is resolved separately because its
|
|
// FK is physically dropped on cloud, so a batch row on the new run-ops DB cannot single-SQL
|
|
// join to control-plane RuntimeEnvironment.
|
|
const where = { runtimeEnvironmentId: environmentId, friendlyId: batchId } as const;
|
|
const readBatch = (client: PrismaReplicaClient): Promise<BatchRow | null> =>
|
|
client.batchTaskRun.findFirst({ select: BATCH_SELECT, where });
|
|
|
|
const readThrough = this.deps.readThrough ?? readThroughRun;
|
|
const batchResult = await readThrough<BatchRow>({
|
|
// The read-through key; here it is the batch friendlyId. A cuid-shaped batch friendlyId
|
|
// classifies as LEGACY and the read-through probes both stores (new first, then legacy
|
|
// replica); a run-ops-shaped one (cut-over orgs) classifies as NEW and reads only the new
|
|
// store — either way the row is found on the DB that owns it.
|
|
runId: batchId,
|
|
environmentId,
|
|
readNew: readBatch,
|
|
readLegacy: readBatch,
|
|
deps: {
|
|
splitEnabled: this.deps.splitEnabled,
|
|
newClient: this.deps.newClient,
|
|
legacyReplica: this.deps.legacyReplica,
|
|
},
|
|
});
|
|
|
|
const batch =
|
|
batchResult.source === "new" || batchResult.source === "legacy-replica"
|
|
? batchResult.value
|
|
: null; // not-found / past-retention => normal not-found surface
|
|
|
|
if (!batch) {
|
|
throw new Error("Batch not found");
|
|
}
|
|
|
|
const hasFinished = batch.status !== "PENDING" && batch.status !== "PROCESSING";
|
|
const isV2 = batch.batchVersion === "runengine:v2";
|
|
|
|
// For v2 batches in PROCESSING state, get live progress from Redis
|
|
// This provides real-time updates without waiting for the batch to complete
|
|
let liveSuccessCount = batch.successfulRunCount ?? 0;
|
|
let liveFailureCount = batch.failedRunCount ?? 0;
|
|
|
|
if (isV2 && batch.status === "PROCESSING") {
|
|
const liveProgress = await engine.getBatchQueueProgress(batch.id);
|
|
if (liveProgress) {
|
|
liveSuccessCount = liveProgress.successCount;
|
|
liveFailureCount = liveProgress.failureCount;
|
|
}
|
|
}
|
|
|
|
// Control-plane env resolved separately from the run-ops batch row (cross-seam FK dropped).
|
|
const resolveEnv = this.deps.resolveDisplayableEnvironment ?? findDisplayableEnvironment;
|
|
|
|
return {
|
|
id: batch.id,
|
|
friendlyId: batch.friendlyId,
|
|
status: batch.status as BatchTaskRunStatus,
|
|
runCount: batch.runCount,
|
|
batchVersion: batch.batchVersion,
|
|
isV2,
|
|
createdAt: batch.createdAt.toISOString(),
|
|
updatedAt: batch.updatedAt.toISOString(),
|
|
completedAt: batch.completedAt?.toISOString(),
|
|
processingStartedAt: batch.processingStartedAt?.toISOString(),
|
|
processingCompletedAt: batch.processingCompletedAt?.toISOString(),
|
|
finishedAt: batch.completedAt
|
|
? batch.completedAt.toISOString()
|
|
: hasFinished
|
|
? batch.updatedAt.toISOString()
|
|
: undefined,
|
|
hasFinished,
|
|
successfulRunCount: liveSuccessCount,
|
|
failedRunCount: liveFailureCount,
|
|
idempotencyKey: batch.idempotencyKey,
|
|
environment: await resolveEnv(environmentId, userId),
|
|
errors: batch.errors.map((error) => ({
|
|
id: error.id,
|
|
index: error.index,
|
|
taskIdentifier: error.taskIdentifier,
|
|
error: error.error,
|
|
errorCode: error.errorCode,
|
|
createdAt: error.createdAt.toISOString(),
|
|
})),
|
|
};
|
|
}
|
|
}
|