Files
Eric Allam 8fc8f57b39 v2: MarQS powered job executions (#1149)
* WIP

* Allow marqsv2 and v2 graphile to run in parallel

* Fix missing GraphileLogger import

* Fixed heartbeat after rebase

* Replace postgres based run counters with redis ones with a backfill

* Add back in the graphile logger

* Remove duplicate visibility timeout calls

* Clamp simple weighted strategy to max of 5
2024-06-06 13:56:27 +01:00

246 lines
7.4 KiB
TypeScript

import {
type ConnectionType,
type Integration,
type IntegrationConnection,
} from "@trigger.dev/database";
import type { PrismaClient, PrismaClientOrTransaction } from "~/db.server";
import { prisma } from "~/db.server";
import { autoIncrementCounter } from "../autoIncrementCounter.server";
import { logger } from "../logger.server";
import { workerQueue } from "../worker.server";
import { ResumeRunService } from "./resumeRun.server";
type FoundRun = NonNullable<Awaited<ReturnType<typeof findRun>>>;
type RunConnectionsByKey = Awaited<ReturnType<typeof createRunConnections>>;
export class StartRunService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(id: string) {
const run = await findRun(this.#prismaClient, id);
if (!run || !this.#runIsStartable(run)) {
return;
}
const runConnectionsByKey = await createRunConnections(this.#prismaClient, run);
if (hasMissingConnections(runConnectionsByKey)) {
await this.#handleMissingConnections(id, runConnectionsByKey);
} else {
await this.#startRun(id, run, runConnectionsByKey);
}
}
#runIsStartable(run: FoundRun) {
if (!run.organization.runsEnabled) {
logger.debug("StartRunService: Runs are disabled for this organization", {
organizationId: run.organization.id,
});
return false;
}
const startableStatuses = ["PENDING", "WAITING_ON_CONNECTIONS"] as const;
return startableStatuses.includes(run.status);
}
async #startRun(id: string, run: FoundRun, runConnectionsByKey: RunConnectionsByKey) {
const createRunConnections = Object.entries(runConnectionsByKey)
.map(([key, runConnection]) =>
runConnection.result === "resolvedHosted"
? ({
key,
connectionId: runConnection.connection.id,
integrationId: runConnection.integration.id,
authSource: "HOSTED",
} as const)
: runConnection.result === "resolvedLocal" || runConnection.result === "resolvedResolver"
? ({
key,
integrationId: runConnection.integration.id,
authSource: runConnection.result === "resolvedLocal" ? "LOCAL" : "RESOLVER",
} as const)
: undefined
)
.filter(Boolean);
await autoIncrementCounter.incrementInTransaction(
`v2-run:${run.jobId}`,
async (num, tx) => {
const updatedRun = await tx.jobRun.update({
where: { id },
data: {
number: num,
status: "QUEUED",
queuedAt: new Date(),
runConnections: {
create: createRunConnections,
},
},
});
await ResumeRunService.enqueue(updatedRun, tx);
},
async (_, tx) => {
const counter = await tx.jobCounter.findUnique({
where: { jobId: run.jobId },
select: { lastNumber: true },
});
return counter?.lastNumber;
},
this.#prismaClient,
{ timeout: 10_000 }
);
}
async #handleMissingConnections(id: string, runConnectionsByKey: RunConnectionsByKey) {
const missingConnections = Object.values(runConnectionsByKey)
.map((runConnection) => (runConnection.result === "missing" ? runConnection : undefined))
.filter(Boolean);
const updatedRun = await this.#prismaClient.jobRun.update({
where: { id },
data: {
status: "WAITING_ON_CONNECTIONS",
missingConnections: {
connectOrCreate: missingConnections.map((connection) => ({
where: {
integrationId_connectionType_accountIdentifier: {
integrationId: connection.integration.id,
connectionType: connection.connectionType,
accountIdentifier: connection.externalAccountId ?? "DEVELOPER",
},
},
create: {
integrationId: connection.integration.id,
connectionType: connection.connectionType,
accountIdentifier: connection.externalAccountId ?? "DEVELOPER",
externalAccountId: connection.externalAccountId,
resolved: false,
},
})),
},
},
include: {
missingConnections: {
include: {
_count: {
select: { runs: true },
},
},
},
},
});
for (const missingConnection of updatedRun.missingConnections) {
if (missingConnection._count.runs === 1) {
workerQueue.enqueue("missingConnectionCreated", {
id: missingConnection.id,
});
}
}
}
}
async function findRun(tx: PrismaClientOrTransaction, id: string) {
return await tx.jobRun.findUnique({
where: { id },
include: {
queue: true,
environment: true,
organization: true,
version: {
include: {
integrations: {
include: {
integration: true,
},
},
},
},
},
});
}
async function createRunConnections(tx: PrismaClientOrTransaction, run: FoundRun) {
return await run.version.integrations.reduce(
async (
accP: Promise<
Record<
string,
| {
result: "resolvedHosted";
connection: IntegrationConnection;
integration: Integration;
}
| { result: "resolvedLocal"; integration: Integration }
| { result: "resolvedResolver"; integration: Integration }
| {
result: "missing";
connectionType: ConnectionType;
integration: Integration;
externalAccountId?: string;
}
>
>,
jobIntegration
) => {
const acc = await accP;
if (jobIntegration.integration.authSource === "LOCAL") {
acc[jobIntegration.key] = {
result: "resolvedLocal",
integration: jobIntegration.integration,
};
} else if (jobIntegration.integration.authSource === "RESOLVER") {
acc[jobIntegration.key] = {
result: "resolvedResolver",
integration: jobIntegration.integration,
};
} else {
const connection = run.externalAccountId
? await tx.integrationConnection.findFirst({
where: {
integrationId: jobIntegration.integration.id,
connectionType: "EXTERNAL",
externalAccountId: run.externalAccountId,
},
})
: await tx.integrationConnection.findFirst({
where: {
integrationId: jobIntegration.integration.id,
connectionType: "DEVELOPER",
},
});
if (connection) {
acc[jobIntegration.key] = {
result: "resolvedHosted",
connection,
integration: jobIntegration.integration,
};
} else {
acc[jobIntegration.key] = {
result: "missing",
connectionType: run.externalAccountId ? "EXTERNAL" : "DEVELOPER",
externalAccountId: run.externalAccountId ?? undefined,
integration: jobIntegration.integration,
};
}
}
return acc;
},
Promise.resolve({})
);
}
function hasMissingConnections(runConnectionsByKey: RunConnectionsByKey) {
return Object.values(runConnectionsByKey).some((connection) => connection.result === "missing");
}