Skip to content

Commit 0e96415

Browse files
authored
fix(426): Concurrency issue with multiple requests to create build (#251)
* fix(426): Concurrency issue with multiple requests to create build closes Visual-Regression-Tracker/Visual-Regression-Tracker#426
1 parent 531a2c1 commit 0e96415

File tree

3 files changed

+150
-14
lines changed

3 files changed

+150
-14
lines changed

src/builds/builds.service.spec.ts

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { mocked, MockedObject } from 'jest-mock';
88
import { BuildDto } from './dto/build.dto';
99
import { ProjectsService } from '../projects/projects.service';
1010
import { generateTestRun } from '../_data_';
11+
import { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';
1112

1213
jest.mock('./dto/build.dto');
1314

@@ -221,4 +222,106 @@ describe('BuildsService', () => {
221222
});
222223
expect(testRunApproveMock).toHaveBeenCalledWith(build.testRuns[0].id, true);
223224
});
225+
226+
describe('findOsCreate', () => {
227+
it('create without ciBuildId', async () => {
228+
const buildUpsertMock = jest.fn().mockResolvedValueOnce(build);
229+
230+
service = await initService({ buildUpsertMock });
231+
service.incrementBuildNumber = jest.fn().mockResolvedValueOnce(build);
232+
233+
await service.findOrCreate({
234+
projectId: '111',
235+
branchName: 'develop',
236+
});
237+
238+
expect(buildUpsertMock).toHaveBeenCalledWith({
239+
where: {
240+
id: '111',
241+
},
242+
create: {
243+
branchName: 'develop',
244+
isRunning: true,
245+
project: {
246+
connect: {
247+
id: '111',
248+
},
249+
},
250+
},
251+
update: {
252+
isRunning: true,
253+
},
254+
});
255+
});
256+
257+
it('create with ciBuildId', async () => {
258+
const buildUpsertMock = jest.fn().mockResolvedValueOnce(build);
259+
service = await initService({ buildUpsertMock });
260+
service.incrementBuildNumber = jest.fn().mockResolvedValueOnce(build);
261+
262+
await service.findOrCreate({
263+
projectId: '111',
264+
branchName: 'develop',
265+
ciBuildId: '222',
266+
});
267+
268+
expect(buildUpsertMock).toHaveBeenCalledWith({
269+
where: {
270+
projectId_ciBuildId: {
271+
projectId: '111',
272+
ciBuildId: '222',
273+
},
274+
},
275+
create: {
276+
branchName: 'develop',
277+
ciBuildId: '222',
278+
isRunning: true,
279+
project: {
280+
connect: {
281+
id: '111',
282+
},
283+
},
284+
},
285+
update: {
286+
isRunning: true,
287+
},
288+
});
289+
});
290+
291+
it('create with retry', async () => {
292+
const buildUpsertMock = jest
293+
.fn()
294+
.mockRejectedValueOnce(new PrismaClientKnownRequestError('mock error', { code: 'P2002', clientVersion: '5' }));
295+
const buildUpdateMock = jest.fn().mockResolvedValueOnce(build);
296+
service = await initService({ buildUpsertMock, buildUpdateMock });
297+
service.incrementBuildNumber = jest.fn().mockResolvedValueOnce(build);
298+
299+
const result = await service.findOrCreate({
300+
projectId: '111',
301+
branchName: 'develop',
302+
});
303+
304+
expect(result).toEqual(build);
305+
});
306+
307+
it('update already created', async () => {
308+
const buildUpsertMock = jest.fn().mockResolvedValueOnce({
309+
...build,
310+
number: 100,
311+
});
312+
service = await initService({ buildUpsertMock });
313+
service.incrementBuildNumber = jest.fn();
314+
315+
const result = await service.findOrCreate({
316+
projectId: '111',
317+
branchName: 'develop',
318+
});
319+
320+
expect(service.incrementBuildNumber).toHaveBeenCalledTimes(0);
321+
expect(result).toEqual({
322+
...build,
323+
number: 100,
324+
});
325+
});
326+
});
224327
});

src/builds/builds.service.ts

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -133,22 +133,37 @@ export class BuildsService {
133133
}
134134
: { id: projectId };
135135

136-
let build = await this.prismaService.build.upsert({
137-
where,
138-
create: {
139-
ciBuildId,
140-
branchName,
141-
isRunning: true,
142-
project: {
143-
connect: {
144-
id: projectId,
136+
let build: Build;
137+
try {
138+
build = await this.prismaService.build.upsert({
139+
where,
140+
create: {
141+
ciBuildId,
142+
branchName,
143+
isRunning: true,
144+
project: {
145+
connect: {
146+
id: projectId,
147+
},
145148
},
146149
},
147-
},
148-
update: {
149-
isRunning: true,
150-
},
151-
});
150+
update: {
151+
isRunning: true,
152+
},
153+
});
154+
} catch (e) {
155+
if (e instanceof Prisma.PrismaClientKnownRequestError) {
156+
if (e.code === 'P2002') {
157+
// cuncurent upsert workaround https://www.prisma.io/docs/reference/api-reference/prisma-client-reference#unique-key-constraint-errors-on-upserts
158+
build = await this.prismaService.build.update({
159+
where,
160+
data: {
161+
isRunning: true,
162+
},
163+
});
164+
}
165+
}
166+
}
152167

153168
// assigne build number
154169
if (!build.number) {

test/builds.e2e-spec.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,24 @@ describe('Builds (e2e)', () => {
151151
});
152152
});
153153

154+
it('201 cuncurrent', async () => {
155+
const createBuildDto: CreateBuildDto = {
156+
ciBuildId: 'ciBuildId',
157+
branchName: 'branchName',
158+
project: project.name,
159+
};
160+
161+
const builds = await Promise.all([
162+
buildsController.create(createBuildDto),
163+
buildsController.create(createBuildDto),
164+
buildsController.create(createBuildDto),
165+
buildsController.create(createBuildDto),
166+
buildsController.create(createBuildDto),
167+
]);
168+
169+
expect(new Set(builds.map((build) => build.id)).size).toBe(1);
170+
});
171+
154172
it('404', () => {
155173
const createBuildDto: CreateBuildDto = {
156174
branchName: 'branchName',

0 commit comments

Comments
 (0)