chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
import { json } from "@remix-run/server-runtime";
|
||||
import { Ratelimit } from "@upstash/ratelimit";
|
||||
import { tryCatch } from "@trigger.dev/core";
|
||||
import { DevDisconnectRequestBody } from "@trigger.dev/core/v3";
|
||||
import { BulkActionId, RunId } from "@trigger.dev/core/v3/isomorphic";
|
||||
import { BulkActionNotificationType, BulkActionType } from "@trigger.dev/database";
|
||||
import { prisma } from "~/db.server";
|
||||
import { runStore } from "~/v3/runStore.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { RateLimiter } from "~/services/rateLimiter.server";
|
||||
import { createActionApiRoute } from "~/services/routeBuilders/apiBuilder.server";
|
||||
import { CancelTaskRunService } from "~/v3/services/cancelTaskRun.server";
|
||||
import { commonWorker } from "~/v3/commonWorker.server";
|
||||
import pMap from "p-map";
|
||||
|
||||
const CANCEL_REASON = "Dev session ended (CLI exited)";
|
||||
|
||||
// Below this threshold, cancel runs inline with pMap.
|
||||
// Above it, create a bulk action and process asynchronously.
|
||||
const BULK_ACTION_THRESHOLD = 25;
|
||||
|
||||
// Maximum number of runs that can be cancelled in a single disconnect call.
|
||||
const MAX_RUNS = 500;
|
||||
|
||||
// Rate limit: 5 calls per minute per environment
|
||||
const disconnectRateLimiter = new RateLimiter({
|
||||
keyPrefix: "dev-disconnect",
|
||||
limiter: Ratelimit.fixedWindow(5, "1 m"),
|
||||
logFailure: true,
|
||||
});
|
||||
|
||||
const { action } = createActionApiRoute(
|
||||
{
|
||||
body: DevDisconnectRequestBody,
|
||||
maxContentLength: 1024 * 256, // 256KB
|
||||
method: "POST",
|
||||
},
|
||||
async ({ authentication, body }) => {
|
||||
// Only allow dev environments — this endpoint uses finalizeRun which
|
||||
// skips PENDING_CANCEL and immediately finalizes executing runs.
|
||||
if (authentication.environment.type !== "DEVELOPMENT") {
|
||||
return json(
|
||||
{ error: "This endpoint is only available for dev environments" },
|
||||
{ status: 403 }
|
||||
);
|
||||
}
|
||||
|
||||
const environmentId = authentication.environment.id;
|
||||
|
||||
// Rate limit per environment
|
||||
const rateLimitResult = await disconnectRateLimiter.limit(environmentId);
|
||||
if (!rateLimitResult.success) {
|
||||
return json(
|
||||
{
|
||||
error: "Rate limit exceeded",
|
||||
retryAfter: Math.ceil((rateLimitResult.reset - Date.now()) / 1000),
|
||||
},
|
||||
{ status: 429 }
|
||||
);
|
||||
}
|
||||
|
||||
if (body.runFriendlyIds.length > MAX_RUNS) {
|
||||
return json(
|
||||
{ error: `A maximum of ${MAX_RUNS} runs can be cancelled per request` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const { runFriendlyIds } = body;
|
||||
|
||||
if (runFriendlyIds.length === 0) {
|
||||
return json({ cancelled: 0 }, { status: 200 });
|
||||
}
|
||||
|
||||
logger.info("Dev disconnect: cancelling runs", {
|
||||
environmentId,
|
||||
runCount: runFriendlyIds.length,
|
||||
});
|
||||
|
||||
// For small numbers of runs, cancel inline
|
||||
if (runFriendlyIds.length <= BULK_ACTION_THRESHOLD) {
|
||||
const cancelled = await cancelRunsInline(runFriendlyIds, environmentId);
|
||||
return json({ cancelled }, { status: 200 });
|
||||
}
|
||||
|
||||
// For large numbers, create a bulk action to process asynchronously
|
||||
const bulkActionId = await createBulkCancelAction(
|
||||
runFriendlyIds,
|
||||
authentication.environment.project.id,
|
||||
environmentId
|
||||
);
|
||||
|
||||
logger.info("Dev disconnect: created bulk action for large run set", {
|
||||
environmentId,
|
||||
bulkActionId,
|
||||
runCount: runFriendlyIds.length,
|
||||
});
|
||||
|
||||
return json({ cancelled: 0, bulkActionId }, { status: 200 });
|
||||
}
|
||||
);
|
||||
|
||||
async function cancelRunsInline(runFriendlyIds: string[], environmentId: string): Promise<number> {
|
||||
const runIds = runFriendlyIds.map((fid) => RunId.toId(fid));
|
||||
|
||||
const runs = await runStore.findRuns(
|
||||
{
|
||||
where: {
|
||||
id: { in: runIds },
|
||||
runtimeEnvironmentId: environmentId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
engine: true,
|
||||
friendlyId: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
completedAt: true,
|
||||
taskEventStore: true,
|
||||
},
|
||||
},
|
||||
prisma
|
||||
);
|
||||
|
||||
let cancelled = 0;
|
||||
const cancelService = new CancelTaskRunService(prisma);
|
||||
|
||||
await pMap(
|
||||
runs,
|
||||
async (run) => {
|
||||
const [error, result] = await tryCatch(
|
||||
cancelService.call(run, { reason: CANCEL_REASON, finalizeRun: true })
|
||||
);
|
||||
|
||||
if (error) {
|
||||
logger.error("Dev disconnect: failed to cancel run", {
|
||||
runId: run.id,
|
||||
error,
|
||||
});
|
||||
} else if (result && !result.alreadyFinished) {
|
||||
cancelled++;
|
||||
}
|
||||
},
|
||||
{ concurrency: 10 }
|
||||
);
|
||||
|
||||
logger.info("Dev disconnect: completed inline cancellation", {
|
||||
environmentId,
|
||||
cancelled,
|
||||
total: runFriendlyIds.length,
|
||||
});
|
||||
|
||||
return cancelled;
|
||||
}
|
||||
|
||||
async function createBulkCancelAction(
|
||||
runFriendlyIds: string[],
|
||||
projectId: string,
|
||||
environmentId: string
|
||||
): Promise<string> {
|
||||
const { id, friendlyId } = BulkActionId.generate();
|
||||
|
||||
await prisma.bulkActionGroup.create({
|
||||
data: {
|
||||
id,
|
||||
friendlyId,
|
||||
projectId,
|
||||
environmentId,
|
||||
name: "Dev session disconnect",
|
||||
type: BulkActionType.CANCEL,
|
||||
params: { runId: runFriendlyIds, finalizeRun: true },
|
||||
queryName: "bulk_action_v1",
|
||||
totalCount: runFriendlyIds.length,
|
||||
completionNotification: BulkActionNotificationType.NONE,
|
||||
},
|
||||
});
|
||||
|
||||
await commonWorker.enqueue({
|
||||
id: `processBulkAction-${id}`,
|
||||
job: "processBulkAction",
|
||||
payload: { bulkActionId: id },
|
||||
});
|
||||
|
||||
return friendlyId;
|
||||
}
|
||||
|
||||
export { action };
|
||||
Reference in New Issue
Block a user