chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,181 @@
|
||||
import { parseWithZod } from "@conform-to/zod";
|
||||
import { json } from "@remix-run/node";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "~/db.server";
|
||||
import { redirectWithErrorMessage, redirectWithSuccessMessage } from "~/models/message.server";
|
||||
import { logger } from "~/services/logger.server";
|
||||
import { dashboardAction } from "~/services/routeBuilders/dashboardBuilder";
|
||||
import { CancelTaskRunService } from "~/v3/services/cancelTaskRun.server";
|
||||
import { getMollifierBuffer } from "~/v3/mollifier/mollifierBuffer.server";
|
||||
import { runStore } from "~/v3/runStore.server";
|
||||
import { controlPlaneResolver } from "~/v3/runOpsMigration/controlPlaneResolver.server";
|
||||
|
||||
export const cancelSchema = z.object({
|
||||
redirectUrl: z.string(),
|
||||
});
|
||||
|
||||
const ParamSchema = z.object({
|
||||
runParam: z.string(),
|
||||
});
|
||||
|
||||
// Resolve the run's organization so the RBAC auth scope can resolve the
|
||||
// user's role in it. The run may not be in Postgres yet (buffered during a
|
||||
// burst), so fall back to the buffer entry's org.
|
||||
async function resolveRunOrganizationId(runParam: string): Promise<string | null> {
|
||||
// Keyed by friendlyId only so the store routes to the owning run-ops DB.
|
||||
const run = await runStore.findRun(
|
||||
{ friendlyId: runParam },
|
||||
{ select: { runtimeEnvironmentId: true } }
|
||||
);
|
||||
if (run) {
|
||||
const env = await controlPlaneResolver.resolveEnv(run.runtimeEnvironmentId);
|
||||
if (env?.organizationId) {
|
||||
return env.organizationId;
|
||||
}
|
||||
}
|
||||
|
||||
const buffer = getMollifierBuffer();
|
||||
const entry = buffer ? await buffer.getEntry(runParam) : null;
|
||||
if (entry?.orgId) {
|
||||
return entry.orgId;
|
||||
}
|
||||
|
||||
// Replica lag with the buffer entry already drained: the run can exist in the
|
||||
// primary while both lookups above miss. Fall back to the primary so the RBAC
|
||||
// scope is never resolved without an org (which would let the role check run
|
||||
// unscoped under the RBAC plugin). Keyed by friendlyId so routing still applies.
|
||||
const primaryRun = await runStore.findRun(
|
||||
{ friendlyId: runParam },
|
||||
{ select: { runtimeEnvironmentId: true } },
|
||||
prisma
|
||||
);
|
||||
if (!primaryRun) {
|
||||
return null;
|
||||
}
|
||||
const primaryEnv = await controlPlaneResolver.resolveEnv(primaryRun.runtimeEnvironmentId);
|
||||
return primaryEnv?.organizationId ?? null;
|
||||
}
|
||||
|
||||
export const action = dashboardAction(
|
||||
{
|
||||
params: ParamSchema,
|
||||
context: async (params) => {
|
||||
const organizationId = await resolveRunOrganizationId(params.runParam);
|
||||
return organizationId ? { organizationId } : {};
|
||||
},
|
||||
authorization: { action: "write", resource: { type: "runs" } },
|
||||
},
|
||||
async ({ request, params, user }) => {
|
||||
const { runParam } = params;
|
||||
|
||||
const formData = await request.formData();
|
||||
const submission = parseWithZod(formData, { schema: cancelSchema });
|
||||
|
||||
if (submission.status !== "success") {
|
||||
return json(submission.reply());
|
||||
}
|
||||
|
||||
try {
|
||||
// Keyed by friendlyId only so the store routes to the owning run-ops DB.
|
||||
// The project-scope + membership auth is a control-plane concern resolved
|
||||
// separately below; joining project/organization here is a cross-DB join
|
||||
// that returns nothing once the run lives in the run-ops DB.
|
||||
const taskRun = await runStore.findRun(
|
||||
{ friendlyId: runParam },
|
||||
{
|
||||
select: {
|
||||
id: true,
|
||||
engine: true,
|
||||
status: true,
|
||||
friendlyId: true,
|
||||
taskEventStore: true,
|
||||
createdAt: true,
|
||||
completedAt: true,
|
||||
projectId: true,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Project-scope + membership auth is control-plane only — keyed by the
|
||||
// run's projectId. A miss is treated as not-found (mirrors the old where).
|
||||
const authorized = taskRun
|
||||
? await prisma.project.findFirst({
|
||||
where: {
|
||||
id: taskRun.projectId,
|
||||
organization: { members: { some: { userId: user.id } } },
|
||||
},
|
||||
select: { id: true },
|
||||
})
|
||||
: null;
|
||||
|
||||
if (taskRun && authorized) {
|
||||
const cancelRunService = new CancelTaskRunService();
|
||||
await cancelRunService.call(taskRun);
|
||||
return redirectWithSuccessMessage(submission.value.redirectUrl, request, `Canceled run`);
|
||||
}
|
||||
|
||||
// PG miss — try the mollifier buffer (customer cancelled a buffered run
|
||||
// during the burst window). Snapshot a `mark_cancelled` patch; the drainer
|
||||
// routes the run to `engine.createCancelledRun` on next pop.
|
||||
const buffer = getMollifierBuffer();
|
||||
const entry = buffer ? await buffer.getEntry(runParam) : null;
|
||||
if (!entry) {
|
||||
return json(submission.reply({ fieldErrors: { runParam: ["Run not found"] } }));
|
||||
}
|
||||
|
||||
// Tenancy: verify the requesting user is a member of the buffered
|
||||
// run's org. The API path scopes by env id from the authenticated
|
||||
// request; the dashboard route uses org-membership because the URL
|
||||
// doesn't carry an envId.
|
||||
const member = await prisma.orgMember.findFirst({
|
||||
where: { userId: user.id, organizationId: entry.orgId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!member) {
|
||||
return json(submission.reply({ fieldErrors: { runParam: ["Run not found"] } }));
|
||||
}
|
||||
|
||||
const result = await buffer!.mutateSnapshot(runParam, {
|
||||
type: "mark_cancelled",
|
||||
cancelledAt: new Date().toISOString(),
|
||||
cancelReason: "Canceled by user",
|
||||
});
|
||||
if (result === "applied_to_snapshot") {
|
||||
return redirectWithSuccessMessage(submission.value.redirectUrl, request, `Canceled run`);
|
||||
}
|
||||
// "not_found" or "busy" — both indicate the drainer raced us between
|
||||
// the getEntry check above and mutateSnapshot. On "not_found" the
|
||||
// entry was just popped and the PG row is in flight; on "busy" the
|
||||
// drainer is mid-materialisation. Either way the customer should
|
||||
// retry — by then the PG row exists and the regular cancel path at
|
||||
// the top of this action takes over.
|
||||
return redirectWithErrorMessage(
|
||||
submission.value.redirectUrl,
|
||||
request,
|
||||
"Run is materialising — retry in a moment"
|
||||
);
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
logger.error("Failed to cancel run", {
|
||||
error: {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack,
|
||||
},
|
||||
});
|
||||
return redirectWithErrorMessage(
|
||||
submission.value.redirectUrl,
|
||||
request,
|
||||
`Failed to cancel run, ${error.message}`
|
||||
);
|
||||
} else {
|
||||
logger.error("Failed to cancel run", { error });
|
||||
return redirectWithErrorMessage(
|
||||
submission.value.redirectUrl,
|
||||
request,
|
||||
`Failed to cancel run, ${JSON.stringify(error)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
Reference in New Issue
Block a user