fix(webapp): prevent duplicate envs from provision race (#4261)
Fixes TRI-12078 ## Summary Prevents concurrent environment setup requests from creating duplicate Staging and Preview environments. ## Fix Adds database-enforced uniqueness for root Staging and Preview environments. If two requests race, the losing request loads the environment created by the winner and continues successfully instead of creating a duplicate or returning an error.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
area: webapp
|
||||
type: fix
|
||||
---
|
||||
|
||||
Prevent duplicate Staging and Preview environments when account setup requests overlap
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type ActionFunctionArgs, json } from "@remix-run/server-runtime";
|
||||
import {
|
||||
Prisma,
|
||||
type RuntimeEnvironment,
|
||||
type Organization,
|
||||
type Project,
|
||||
@@ -61,9 +62,16 @@ async function upsertEnvironment(
|
||||
type: RuntimeEnvironmentType,
|
||||
isBranchableEnvironment: boolean
|
||||
) {
|
||||
const existingEnvironment = project.environments.find((env) => env.type === type);
|
||||
const existingEnvironment = project.environments.find(
|
||||
(env) => env.type === type && env.parentEnvironmentId === null
|
||||
);
|
||||
|
||||
if (!existingEnvironment) {
|
||||
if (existingEnvironment) {
|
||||
await updateEnvConcurrencyLimits({ ...existingEnvironment, organization, project });
|
||||
return { status: "updated", environment: existingEnvironment };
|
||||
}
|
||||
|
||||
try {
|
||||
const newEnvironment = await createEnvironment({
|
||||
organization,
|
||||
project,
|
||||
@@ -72,8 +80,23 @@ async function upsertEnvironment(
|
||||
});
|
||||
await updateEnvConcurrencyLimits({ ...newEnvironment, organization, project });
|
||||
return { status: "created", environment: newEnvironment };
|
||||
} else {
|
||||
await updateEnvConcurrencyLimits({ ...existingEnvironment, organization, project });
|
||||
return { status: "updated", environment: existingEnvironment };
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002") {
|
||||
const existingAfterConflict = await prisma.runtimeEnvironment.findFirst({
|
||||
where: {
|
||||
organizationId: organization.id,
|
||||
projectId: project.id,
|
||||
type,
|
||||
parentEnvironmentId: null,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingAfterConflict) {
|
||||
await updateEnvConcurrencyLimits({ ...existingAfterConflict, organization, project });
|
||||
return { status: "updated", environment: existingAfterConflict };
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
+4
@@ -0,0 +1,4 @@
|
||||
CREATE UNIQUE INDEX CONCURRENTLY IF NOT EXISTS "RuntimeEnvironment_projectId_type_staging_preview_root_key"
|
||||
ON "RuntimeEnvironment" ("projectId", "type")
|
||||
WHERE "parentEnvironmentId" IS NULL
|
||||
AND "type" IN ('STAGING', 'PREVIEW');
|
||||
@@ -394,6 +394,8 @@ model RuntimeEnvironment {
|
||||
taskIdentifiers TaskIdentifier[]
|
||||
revokedApiKeys RevokedApiKey[]
|
||||
|
||||
// A partial unique index also enforces one STAGING/PREVIEW root per project and type.
|
||||
// It is defined in SQL because Prisma does not support partial indexes in the schema.
|
||||
@@unique([projectId, slug, orgMemberId])
|
||||
@@unique([projectId, shortcode])
|
||||
@@index([parentEnvironmentId])
|
||||
|
||||
Reference in New Issue
Block a user