Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 21 additions & 3 deletions src/release-sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,18 @@ async function listSyncedVersions(prisma: PrismaClient, type: ReleaseType): Prom
return new Set(releases.map(release => release.version));
}

async function releaseExists(
prisma: PrismaClient,
type: ReleaseType,
version: string,
): Promise<boolean> {
const release = await prisma.release.findUnique({
where: { version_type: { version, type } },
select: { id: true },
});
return release !== null;
}

function isUniqueViolation(error: unknown): boolean {
return (
error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2002"
Expand Down Expand Up @@ -279,8 +291,10 @@ async function createRelease(
});
} catch (error) {
// Another API instance can win the race between the version listing and
// this insert. The row it wrote is the one we wanted, so treat it as synced.
if (isUniqueViolation(error)) {
// this insert, in which case the row it wrote is the one we wanted. Any
// other unique violation (a stale id sequence after a data import, say)
// leaves no row and is a real failure.
if (isUniqueViolation(error) && (await releaseExists(clients.prisma, type, version))) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2f4a51d. The contextual wrap moved up to the per-version loop in syncReleases, so any failure for a version carries its type and version: the insert, the row lookup after a unique violation, and the S3 artifact scan before either. createRelease now rethrows the original error unchanged.

console.log(`[sync-releases] ${type} ${version}: created concurrently elsewhere, skipping`);
return "already-synced";
}
Expand Down Expand Up @@ -326,9 +340,13 @@ export async function syncReleases(
]);

for (const version of versions) {
// Name the release in any failure, whichever step raised it, so the
// scheduled run log does not need to be traced back to a version.
const outcome = synced.has(version)
? "already-synced"
: await createRelease(clients, config, decide, type, version);
: await createRelease(clients, config, decide, type, version).catch((error: unknown) => {
throw new Error(`[sync-releases] ${type} ${version}: sync failed`, { cause: error });
});
stats[outcome]++;

if (outcome === "aborted") {
Expand Down
37 changes: 36 additions & 1 deletion test/sync-releases.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {
ListObjectsV2Command,
S3Client,
} from "@aws-sdk/client-s3";
import { PrismaClient } from "@prisma/client";
import { Prisma, PrismaClient } from "@prisma/client";
import { afterEach, describe, expect, beforeEach, it, vi } from "vitest";

import {
Expand Down Expand Up @@ -376,6 +376,7 @@ describe("syncReleases", () => {
const racingPrisma = {
release: {
findMany: async () => [],
findUnique: (args: unknown) => testPrisma.release.findUnique(args as any),
create: (args: unknown) => testPrisma.release.create(args as any),
},
} as unknown as PrismaClient;
Expand All @@ -392,6 +393,40 @@ describe("syncReleases", () => {
});
expect(release.url).toBe("https://cdn.test.com/other-instance");
});

it("fails on a unique violation that left no release row behind", async () => {
const version = "9.9.9";
mockS3ListVersions("app", [version]);
mockS3HashFile("app", version, "app-hash");

// A stale id sequence (a database restored from a dump) makes the insert
// fail on the primary key with the same error code as the race above,
// but no row for this version exists afterwards.
const stalePrisma = {
release: {
findMany: async () => [],
findUnique: async () => null,
create: async () => {
throw new Prisma.PrismaClientKnownRequestError("Unique constraint failed", {
code: "P2002",
clientVersion: "test",
meta: { target: ["id"] },
});
},
},
} as unknown as PrismaClient;

await expect(
syncReleases(
{ prisma: stalePrisma, s3Client: syncS3Client },
{ bucketName: SYNC_BUCKET, baseUrl: SYNC_BASE_URL },
createAtDefaultRollout,
),
).rejects.toMatchObject({
message: "[sync-releases] app 9.9.9: sync failed",
cause: expect.objectContaining({ code: "P2002" }),
});
});
});

describe("scheduleReleaseSync", () => {
Expand Down
Loading