Fixed sparodically failed run creations

- Use a better way of getting the latest job run number to increment
- Make the CreateRunService transaction more reliable
- Invoke dispatchers in parallel
- No longer swallow prisma errors in $transaction
This commit is contained in:
Eric Allam
2023-09-28 17:08:26 +01:00
parent a42e94c75f
commit a12c7c3b0a
7 changed files with 137 additions and 135 deletions
+7 -5
View File
@@ -31,7 +31,7 @@ export type PrismaTransactionOptions = {
/** Sets the transaction isolation level. By default this is set to the value currently configured in your database. */
isolationLevel?: Prisma.TransactionIsolationLevel;
rethrowPrismaErrors?: boolean;
swallowPrismaErrors?: boolean;
};
export async function $transaction<R>(
@@ -55,11 +55,9 @@ export async function $transaction<R>(
name: error.name,
});
if (options?.rethrowPrismaErrors) {
throw error;
if (options?.swallowPrismaErrors) {
return;
}
return;
}
throw error;
@@ -124,6 +122,10 @@ function getClient() {
emit: "stdout",
level: "warn",
},
// {
// emit: "stdout",
// level: "query",
// },
],
});
@@ -3,7 +3,7 @@ import { json } from "@remix-run/server-runtime";
import type { CompleteTaskBodyOutput, ServerTask } from "@trigger.dev/core";
import { CompleteTaskBodyInputSchema } from "@trigger.dev/core";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { PrismaClient, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
@@ -86,8 +86,8 @@ export class CompleteRunTaskService {
): Promise<ServerTask | undefined> {
// Using a transaction, we'll first check to see if the task already exists and return if if it does
// If it doesn't exist, we'll create it and return it
const task = await this.#prismaClient.$transaction(async (prisma) => {
const existingTask = await prisma.task.findUnique({
const task = await this.#prismaClient.$transaction(async (tx) => {
const existingTask = await tx.task.findUnique({
where: {
id,
},
@@ -129,35 +129,31 @@ export class CompleteRunTaskService {
return existingTask;
}
const task = await $transaction(prisma, async (tx) => {
if (existingTask.attempts.length === 1) {
await tx.taskAttempt.update({
where: {
id: existingTask.attempts[0].id,
},
data: {
status: "COMPLETED",
},
});
}
return await tx.task.update({
if (existingTask.attempts.length === 1) {
await tx.taskAttempt.update({
where: {
id,
id: existingTask.attempts[0].id,
},
data: {
status: "COMPLETED",
output: taskBody.output ?? undefined,
completedAt: new Date(),
outputProperties: taskBody.properties,
},
include: {
attempts: true,
},
});
});
}
return task;
return await tx.task.update({
where: {
id,
},
data: {
status: "COMPLETED",
output: taskBody.output ?? undefined,
completedAt: new Date(),
outputProperties: taskBody.properties,
},
include: {
attempts: true,
},
});
});
return task ? taskWithAttemptsToServerTask(task) : undefined;
@@ -2,7 +2,7 @@ import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { FailTaskBodyInput, FailTaskBodyInputSchema, ServerTask } from "@trigger.dev/core";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { PrismaClient, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
@@ -86,8 +86,8 @@ export class FailRunTaskService {
): Promise<ServerTask | undefined> {
// Using a transaction, we'll first check to see if the task already exists and return if if it does
// If it doesn't exist, we'll create it and return it
const task = await this.#prismaClient.$transaction(async (prisma) => {
const existingTask = await prisma.task.findUnique({
const task = await this.#prismaClient.$transaction(async (tx) => {
const existingTask = await tx.task.findUnique({
where: {
id,
},
@@ -129,35 +129,31 @@ export class FailRunTaskService {
return existingTask;
}
const task = await $transaction(prisma, async (tx) => {
if (existingTask.attempts.length === 1) {
await tx.taskAttempt.update({
where: {
id: existingTask.attempts[0].id,
},
data: {
status: "ERRORED",
error: formatError(taskBody.error),
},
});
}
return await prisma.task.update({
if (existingTask.attempts.length === 1) {
await tx.taskAttempt.update({
where: {
id,
id: existingTask.attempts[0].id,
},
data: {
status: "ERRORED",
output: taskBody.error ?? undefined,
completedAt: new Date(),
},
include: {
attempts: true,
error: formatError(taskBody.error),
},
});
});
}
return task;
return await tx.task.update({
where: {
id,
},
data: {
status: "ERRORED",
output: taskBody.error ?? undefined,
completedAt: new Date(),
},
include: {
attempts: true,
},
});
});
return task ? taskWithAttemptsToServerTask(task) : undefined;
@@ -34,77 +34,55 @@ export class IngestSendEvent {
try {
const deliverAt = this.#calculateDeliverAt(options);
return await $transaction(
this.#prismaClient,
async (tx) => {
const externalAccount = options?.accountId
? await tx.externalAccount.upsert({
where: {
environmentId_identifier: {
environmentId: environment.id,
identifier: options.accountId,
},
},
create: {
return await $transaction(this.#prismaClient, async (tx) => {
const externalAccount = options?.accountId
? await tx.externalAccount.upsert({
where: {
environmentId_identifier: {
environmentId: environment.id,
organizationId: environment.organizationId,
identifier: options.accountId,
},
update: {},
})
: undefined;
},
create: {
environmentId: environment.id,
organizationId: environment.organizationId,
identifier: options.accountId,
},
update: {},
})
: undefined;
// Create a new event in the database
const eventLog = await tx.eventRecord.create({
data: {
organization: {
connect: {
id: environment.organizationId,
},
},
project: {
connect: {
id: environment.projectId,
},
},
environment: {
connect: {
id: environment.id,
},
},
eventId: event.id,
name: event.name,
timestamp: event.timestamp ?? new Date(),
payload: event.payload ?? {},
context: event.context ?? {},
source: event.source ?? "trigger.dev",
sourceContext,
deliverAt: deliverAt,
externalAccount: externalAccount
? {
connect: {
id: externalAccount.id,
},
}
: {},
// Create a new event in the database
const eventLog = await tx.eventRecord.create({
data: {
organizationId: environment.organizationId,
projectId: environment.projectId,
environmentId: environment.id,
eventId: event.id,
name: event.name,
timestamp: event.timestamp ?? new Date(),
payload: event.payload ?? {},
context: event.context ?? {},
source: event.source ?? "trigger.dev",
sourceContext,
deliverAt: deliverAt,
externalAccountId: externalAccount ? externalAccount.id : undefined,
},
});
if (this.deliverEvents) {
// Produce a message to the event bus
await workerQueue.enqueue(
"deliverEvent",
{
id: eventLog.id,
},
});
{ runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` }
);
}
if (this.deliverEvents) {
// Produce a message to the event bus
await workerQueue.enqueue(
"deliverEvent",
{
id: eventLog.id,
},
{ runAt: eventLog.deliverAt, tx, jobKey: `event:${eventLog.id}` }
);
}
return eventLog;
},
{ rethrowPrismaErrors: true }
);
return eventLog;
});
} catch (error) {
const prismaError = PrismaErrorSchema.safeParse(error);
@@ -42,29 +42,32 @@ export class CreateRunService {
return await $transaction(this.#prismaClient, async (tx) => {
// Get the current max number for the given jobId
const currentMaxNumber = await tx.jobRun.aggregate({
const latestJob = await tx.jobRun.findFirst({
where: { jobId: job.id },
_max: { number: true },
orderBy: { id: "desc" },
select: {
number: true,
},
});
// Increment the number for the new execution
const newNumber = (currentMaxNumber._max.number ?? 0) + 1;
const newNumber = (latestJob?.number ?? 0) + 1;
// Create the new execution with the incremented number
const run = await tx.jobRun.create({
data: {
number: newNumber,
preprocess: version.preprocessRuns,
job: { connect: { id: job.id } },
version: { connect: { id: version.id } },
event: { connect: { id: eventId } },
environment: { connect: { id: environment.id } },
organization: { connect: { id: environment.organizationId } },
project: { connect: { id: environment.projectId } },
endpoint: { connect: { id: endpoint.id } },
queue: { connect: { id: jobQueue.id } },
externalAccount: eventRecord.externalAccountId
? { connect: { id: eventRecord.externalAccountId } }
jobId: job.id,
versionId: version.id,
eventId: eventId,
environmentId: environment.id,
organizationId: environment.organizationId,
projectId: environment.projectId,
endpointId: endpoint.id,
queueId: jobQueue.id,
externalAccountId: eventRecord.externalAccountId
? eventRecord.externalAccountId
: undefined,
isTest: eventRecord.isTest,
},
+2 -1
View File
@@ -161,7 +161,8 @@ function getWorkerQueue() {
tasks: {
"events.invokeDispatcher": {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
maxAttempts: 6,
queueName: (payload) => `dispatcher:${payload.id}`, // use a queue for a dispatcher so runs are created sequentially
handler: async (payload, job) => {
const service = new InvokeDispatcherService();
+27 -1
View File
@@ -56,6 +56,32 @@ async function main() {
// }
}
async function mainParallel() {
console.log("Preparing perf tests...");
// wait for 10 seconds
await new Promise((resolve) => setTimeout(resolve, 10000));
console.log("Starting perf tests in 1 second...");
// wait for 1 seconds
await new Promise((resolve) => setTimeout(resolve, 1000));
// Send 5 events per second for 30 seconds (1 event == 10 runs)
for (let i = 0; i < 30; i++) {
console.log("Sending 5 event...");
await Promise.all([sendEvent(), sendEvent(), sendEvent(), sendEvent(), sendEvent()]);
await new Promise((resolve) => setTimeout(resolve, 250));
}
// console.log("Sending 30 events...");
// for (let i = 0; i < 30; i++) {
// await sendEvent();
// }
}
async function mainLong() {
console.log("Preparing long perf tests...");
@@ -95,7 +121,7 @@ async function mainSerial() {
}
}
mainSerial().catch((err) => {
mainParallel().catch((err) => {
console.error(err);
process.exit(1);
});