Skip to content

Commit

Permalink
fix(back): prisma transaction timeout during map submission
Browse files Browse the repository at this point in the history
This was a case of me keeping a Prisma transaction open for the lifetime
of several S3 requests. I've pulled out the S3 logic to run in a
separate task that won't block the DB work.

Haven't written tests for this since I can't think of a reliable way to
E2E test this and we'll test it out on staging tonight.
  • Loading branch information
tsa96 committed Sep 27, 2024
1 parent 198bcd7 commit 4e1edb7
Showing 1 changed file with 94 additions and 84 deletions.
178 changes: 94 additions & 84 deletions apps/backend/src/app/modules/maps/maps.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -546,51 +546,57 @@ export class MapsService {
const bspHash = FileStoreService.getHashForBuffer(bspFile.buffer);

let map: Awaited<ReturnType<typeof this.createMapDbEntry>>;
await this.db.$transaction(async (tx) => {
map = await this.createMapDbEntry(tx, dto, userID, bspHash, hasVmf);

await tx.leaderboard.createMany({
data: LeaderboardHandler.getMaximalLeaderboards(
dto.suggestions.map(({ gamemode, trackType, trackNum }) => ({
gamemode,
trackType,
trackNum
})),
dto.zones
).map((obj) => ({
mapID: map.id,
...obj,
style: 0, // When we add styles support getMaximalLeaderboards should generate all variations of this
type: LeaderboardType.IN_SUBMISSION
}))
});

const version = map.currentVersion;

const tasks: Promise<unknown>[] = [
(async () => {
const zippedVmf = hasVmf
? await this.zipVmfFiles(dto.name, 1, vmfFiles)
: undefined;
const tasks: Promise<any>[] = [
this.db.$transaction(async (tx) => {
map = await this.createMapDbEntry(tx, dto, userID, bspHash, hasVmf);

await tx.leaderboard.createMany({
data: LeaderboardHandler.getMaximalLeaderboards(
dto.suggestions.map(({ gamemode, trackType, trackNum }) => ({
gamemode,
trackType,
trackNum
})),
dto.zones
).map((obj) => ({
mapID: map.id,
...obj,
style: 0, // When we add styles support getMaximalLeaderboards should generate all variations of this
type: LeaderboardType.IN_SUBMISSION
}))
});

return this.uploadMapVersionFiles(version.id, bspFile, zippedVmf);
})(),
// We need the generated map ID from the above query for S3, but do NOT
// want to block our transaction on S3 operations - so push to promise
// array then await everything below.
tasks.push(
(async () => {
const zippedVmf = hasVmf
? await this.zipVmfFiles(dto.name, 1, vmfFiles)
: undefined;

return this.uploadMapVersionFiles(
map.currentVersion.id,
bspFile,
zippedVmf
);
})()
);

this.createMapUploadedActivities(tx, map.id, map.credits)
];
await this.createMapUploadedActivities(tx, map.id, map.credits);

if (dto.wantsPrivateTesting && dto.testInvites?.length > 0) {
tasks.push(
this.mapTestInviteService.createOrUpdatePrivateTestingInvites(
if (dto.wantsPrivateTesting && dto.testInvites?.length > 0) {
await this.mapTestInviteService.createOrUpdatePrivateTestingInvites(
tx,
map.id,
dto.testInvites
)
);
}
);
}
})
];

await Promise.all(tasks);
});
await Promise.all(tasks);

return DtoFactory(MapDto, map);
}
Expand Down Expand Up @@ -659,61 +665,65 @@ export class MapsService {
const oldVersion = map.currentVersion;
const newVersionNum = oldVersion.versionNum + 1;

await this.db.$transaction(async (tx) => {
const newVersion = await tx.mapVersion.create({
data: {
versionNum: newVersionNum,
submitter: { connect: { id: userID } },
hasVmf,
zones: zones as unknown as JsonValue, // TODO: #855
bspHash,
changelog: dto.changelog,
mmap: { connect: { id: mapID } }
}
});
const tasks: Promise<any>[] = [
this.db.$transaction(async (tx) => {
const newVersion = await tx.mapVersion.create({
data: {
versionNum: newVersionNum,
submitter: { connect: { id: userID } },
hasVmf,
zones: zones as unknown as JsonValue, // TODO: #855
bspHash,
changelog: dto.changelog,
mmap: { connect: { id: mapID } }
}
});

await this.generateSubmissionLeaderboards(
tx,
mapID,
map.submission.suggestions as unknown as MapSubmissionSuggestion[], // TODO: #855
zones
);
await this.generateSubmissionLeaderboards(
tx,
mapID,
map.submission.suggestions as unknown as MapSubmissionSuggestion[], // TODO: #855
zones
);

if (dto.resetLeaderboards === true) {
// If it's been approved before, deleting runs is a majorly destructive
// action that we probably don't want to allow the submitter to do.
// If the submitter is fixing the maps in a significant enough way to
// still require a leaderboard reset, they should just get an admin to
// do it.
if (
(map.submission.dates as unknown as MapSubmissionDate[]).some(
(date) => date.status === MapStatus.APPROVED
)
) {
throw new ForbiddenException(
'Cannot reset leaderboards on a previously approved map.' +
' Talk about it with an admin!'
);
}
if (dto.resetLeaderboards === true) {
// If it's been approved before, deleting runs is a majorly destructive
// action that we probably don't want to allow the submitter to do.
// If the submitter is fixing the maps in a significant enough way to
// still require a leaderboard reset, they should just get an admin to
// do it.
if (
(map.submission.dates as unknown as MapSubmissionDate[]).some(
(date) => date.status === MapStatus.APPROVED
)
) {
throw new ForbiddenException(
'Cannot reset leaderboards on a previously approved map.' +
' Talk about it with an admin!'
);
}

await tx.leaderboardRun.deleteMany({ where: { mapID: map.id } });
}
await tx.leaderboardRun.deleteMany({ where: { mapID: map.id } });
}

await parallel(
async () => {
const zippedVmf = hasVmf
? await this.zipVmfFiles(map.name, newVersionNum, vmfFiles)
: undefined;
tasks.push(
(async () => {
const zippedVmf = hasVmf
? await this.zipVmfFiles(map.name, newVersionNum, vmfFiles)
: undefined;

await this.uploadMapVersionFiles(newVersion.id, bspFile, zippedVmf);
},
await this.uploadMapVersionFiles(newVersion.id, bspFile, zippedVmf);
})()
);

tx.mMap.update({
await tx.mMap.update({
where: { id: mapID },
data: { currentVersion: { connect: { id: newVersion.id } } }
})
);
});
});
})
];

await Promise.all(tasks);

if (
map.status === MapStatus.PUBLIC_TESTING ||
Expand Down

0 comments on commit 4e1edb7

Please sign in to comment.