211 lines
5.8 KiB
TypeScript
211 lines
5.8 KiB
TypeScript
import type { TaskQueue, User } from "@trigger.dev/database";
|
|
import { errAsync, fromPromise, okAsync } from "neverthrow";
|
|
import type { PrismaClientOrTransaction } from "~/db.server";
|
|
import type { AuthenticatedEnvironment } from "~/services/apiAuth.server";
|
|
import { removeQueueConcurrencyLimits, updateQueueConcurrencyLimits } from "../runQueue.server";
|
|
import { engine } from "../runEngine.server";
|
|
|
|
export type ConcurrencySystemOptions = {
|
|
db: PrismaClientOrTransaction;
|
|
reader: PrismaClientOrTransaction;
|
|
};
|
|
|
|
export type QueueInput = string | { type: "task" | "custom"; name: string };
|
|
|
|
export class ConcurrencySystem {
|
|
constructor(private readonly options: ConcurrencySystemOptions) {}
|
|
|
|
private get db() {
|
|
return this.options.db;
|
|
}
|
|
|
|
get queues() {
|
|
return {
|
|
overrideQueueConcurrencyLimit: (
|
|
environment: AuthenticatedEnvironment,
|
|
queue: QueueInput,
|
|
concurrencyLimit: number,
|
|
overriddenBy?: User
|
|
) => {
|
|
return findQueueFromInput(this.db, environment, queue)
|
|
.andThen((queue) =>
|
|
overrideQueueConcurrencyLimit(
|
|
this.db,
|
|
environment,
|
|
queue,
|
|
concurrencyLimit,
|
|
overriddenBy
|
|
)
|
|
)
|
|
.andThen((queue) => syncQueueConcurrencyToEngine(environment, queue))
|
|
.andThen((queue) => getQueueStats(environment, queue));
|
|
},
|
|
resetConcurrencyLimit: (environment: AuthenticatedEnvironment, queue: QueueInput) => {
|
|
return findQueueFromInput(this.db, environment, queue)
|
|
.andThen((queue) => resetQueueConcurrencyLimit(this.db, queue))
|
|
.andThen((queue) => syncQueueConcurrencyToEngine(environment, queue))
|
|
.andThen((queue) => getQueueStats(environment, queue));
|
|
},
|
|
};
|
|
}
|
|
}
|
|
|
|
function findQueueFromInput(
|
|
db: PrismaClientOrTransaction,
|
|
environment: AuthenticatedEnvironment,
|
|
queue: QueueInput
|
|
) {
|
|
if (typeof queue === "string") {
|
|
return findQueueByFriendlyId(db, environment, queue);
|
|
}
|
|
|
|
const queueName =
|
|
queue.type === "task" ? `task/${queue.name.replace(/^task\//, "")}` : queue.name;
|
|
|
|
return findQueueByName(db, environment, queueName);
|
|
}
|
|
|
|
function findQueueByFriendlyId(
|
|
db: PrismaClientOrTransaction,
|
|
environment: AuthenticatedEnvironment,
|
|
friendlyId: string
|
|
) {
|
|
return fromPromise(
|
|
db.taskQueue.findFirst({
|
|
where: {
|
|
runtimeEnvironmentId: environment.id,
|
|
friendlyId,
|
|
},
|
|
}),
|
|
(error) => ({
|
|
type: "other" as const,
|
|
cause: error,
|
|
})
|
|
).andThen((queue) => {
|
|
if (!queue) {
|
|
return errAsync({ type: "queue_not_found" as const });
|
|
}
|
|
return okAsync(queue);
|
|
});
|
|
}
|
|
|
|
function findQueueByName(
|
|
db: PrismaClientOrTransaction,
|
|
environment: AuthenticatedEnvironment,
|
|
queue: string
|
|
) {
|
|
return fromPromise(
|
|
db.taskQueue.findFirst({
|
|
where: {
|
|
runtimeEnvironmentId: environment.id,
|
|
name: queue,
|
|
},
|
|
}),
|
|
(error) => ({
|
|
type: "other" as const,
|
|
cause: error,
|
|
})
|
|
).andThen((queue) => {
|
|
if (!queue) {
|
|
return errAsync({ type: "queue_not_found" as const });
|
|
}
|
|
return okAsync(queue);
|
|
});
|
|
}
|
|
|
|
function overrideQueueConcurrencyLimit(
|
|
db: PrismaClientOrTransaction,
|
|
environment: AuthenticatedEnvironment,
|
|
queue: TaskQueue,
|
|
concurrencyLimit: number,
|
|
overriddenBy?: User
|
|
) {
|
|
const newConcurrencyLimit = Math.max(
|
|
Math.min(concurrencyLimit, environment.maximumConcurrencyLimit),
|
|
0
|
|
);
|
|
|
|
const concurrencyLimitBase = queue.concurrencyLimitOverriddenAt
|
|
? queue.concurrencyLimitBase
|
|
: queue.concurrencyLimit;
|
|
|
|
return fromPromise(
|
|
db.taskQueue.update({
|
|
where: {
|
|
id: queue.id,
|
|
},
|
|
data: {
|
|
concurrencyLimit: newConcurrencyLimit,
|
|
concurrencyLimitBase: concurrencyLimitBase ?? null,
|
|
concurrencyLimitOverriddenAt: new Date(),
|
|
concurrencyLimitOverriddenBy: overriddenBy?.id ?? null,
|
|
},
|
|
}),
|
|
(error) => ({
|
|
type: "queue_update_failed" as const,
|
|
cause: error,
|
|
})
|
|
);
|
|
}
|
|
|
|
function resetQueueConcurrencyLimit(db: PrismaClientOrTransaction, queue: TaskQueue) {
|
|
if (queue.concurrencyLimitOverriddenAt === null) {
|
|
return errAsync({ type: "queue_not_overridden" as const });
|
|
}
|
|
|
|
const newConcurrencyLimit = queue.concurrencyLimitBase;
|
|
|
|
return fromPromise(
|
|
db.taskQueue.update({
|
|
where: { id: queue.id },
|
|
data: {
|
|
concurrencyLimitOverriddenAt: null,
|
|
concurrencyLimit: newConcurrencyLimit,
|
|
concurrencyLimitBase: null,
|
|
concurrencyLimitOverriddenBy: null,
|
|
},
|
|
}),
|
|
(error) => ({
|
|
type: "queue_update_failed" as const,
|
|
cause: error,
|
|
})
|
|
);
|
|
}
|
|
|
|
function syncQueueConcurrencyToEngine(environment: AuthenticatedEnvironment, queue: TaskQueue) {
|
|
if (queue.paused) {
|
|
// Queue is paused, don't update Redis limits - keep at 0
|
|
return okAsync(queue);
|
|
}
|
|
|
|
if (typeof queue.concurrencyLimit === "number") {
|
|
return fromPromise(
|
|
updateQueueConcurrencyLimits(environment, queue.name, queue.concurrencyLimit),
|
|
(error) => ({
|
|
type: "sync_queue_concurrency_to_engine_failed" as const,
|
|
cause: error,
|
|
})
|
|
).andThen(() => okAsync(queue));
|
|
} else {
|
|
return fromPromise(removeQueueConcurrencyLimits(environment, queue.name), (error) => ({
|
|
type: "sync_queue_concurrency_to_engine_failed" as const,
|
|
cause: error,
|
|
})).andThen(() => okAsync(queue));
|
|
}
|
|
}
|
|
|
|
function getQueueStats(environment: AuthenticatedEnvironment, queue: TaskQueue) {
|
|
return fromPromise(
|
|
Promise.all([
|
|
engine.lengthOfQueues(environment, [queue.name]),
|
|
engine.currentConcurrencyOfQueues(environment, [queue.name]),
|
|
]),
|
|
(error) => ({
|
|
type: "get_queue_stats_failed" as const,
|
|
cause: error,
|
|
})
|
|
).andThen(([queued, running]) =>
|
|
okAsync({ queued: queued[queue.name] ?? 0, running: running[queue.name] ?? 0, ...queue })
|
|
);
|
|
}
|