fix: 避免幂等发布重复执行
- src/deploy-runs/deploy-run.repository.ts: 返回发布单是否新建,唯一键竞争时回读已有记录 - src/deploy-runs/deploy-runs.service.ts: 重复 idempotencyKey 只返回已有发布单并记录复用审计,不再重复触发 Jenkins
This commit is contained in:
@@ -49,6 +49,14 @@ type DeployRunRecord = Prisma.DeployRunGetPayload<{
|
||||
};
|
||||
}>;
|
||||
type DeployRunDatabaseClient = PrismaService | Prisma.TransactionClient;
|
||||
type CreateDeployRunResult = {
|
||||
run: DeployRunSummary;
|
||||
created: boolean;
|
||||
};
|
||||
type CreateDatabaseDeployRunResult = {
|
||||
record: DeployRunRecord;
|
||||
created: boolean;
|
||||
};
|
||||
|
||||
type DeployRunMetadata = Prisma.InputJsonObject & {
|
||||
jenkinsBuildUrl?: string;
|
||||
@@ -143,16 +151,19 @@ export class DeployRunRepository {
|
||||
return run ? this.toSummary(run) : null;
|
||||
}
|
||||
|
||||
async create(input: CreateDeployRunInput): Promise<DeployRunSummary> {
|
||||
async create(input: CreateDeployRunInput): Promise<CreateDeployRunResult> {
|
||||
const idempotencyKey = this.resolveIdempotencyKey(input);
|
||||
|
||||
if (!this.useDatabase()) {
|
||||
return this.createMemoryRun(input, idempotencyKey);
|
||||
}
|
||||
|
||||
const created = await this.createDatabaseRun(input, idempotencyKey);
|
||||
const result = await this.createDatabaseRun(input, idempotencyKey);
|
||||
|
||||
return this.toSummary(created);
|
||||
return {
|
||||
run: this.toSummary(result.record),
|
||||
created: result.created,
|
||||
};
|
||||
}
|
||||
|
||||
async updateStatus(
|
||||
@@ -437,7 +448,7 @@ export class DeployRunRepository {
|
||||
private async createDatabaseRun(
|
||||
input: CreateDeployRunInput,
|
||||
idempotencyKey: string,
|
||||
): Promise<DeployRunRecord> {
|
||||
): Promise<CreateDatabaseDeployRunResult> {
|
||||
try {
|
||||
return await this.prisma.$transaction(async (tx) => {
|
||||
const existing = await this.findRecordByIdempotencyKey(
|
||||
@@ -446,14 +457,17 @@ export class DeployRunRepository {
|
||||
);
|
||||
|
||||
if (existing) {
|
||||
return existing;
|
||||
return {
|
||||
record: existing,
|
||||
created: false,
|
||||
};
|
||||
}
|
||||
|
||||
const target = await this.findTargetProjectEnvironment(input, tx);
|
||||
const initialRun = this.buildInitialRun(input, idempotencyKey);
|
||||
const metadata = this.inputMetadata(input);
|
||||
|
||||
return tx.deployRun.create({
|
||||
const record = await tx.deployRun.create({
|
||||
data: {
|
||||
projectId: target.project.id,
|
||||
environmentId: target.environment.id,
|
||||
@@ -483,13 +497,21 @@ export class DeployRunRepository {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
record,
|
||||
created: true,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.isIdempotencyUniqueConflict(error)) {
|
||||
const existing = await this.findRecordByIdempotencyKey(idempotencyKey);
|
||||
|
||||
if (existing) {
|
||||
return existing;
|
||||
return {
|
||||
record: existing,
|
||||
created: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,16 +587,22 @@ export class DeployRunRepository {
|
||||
private createMemoryRun(
|
||||
input: CreateDeployRunInput,
|
||||
idempotencyKey: string,
|
||||
): DeployRunSummary {
|
||||
): CreateDeployRunResult {
|
||||
const existing = this.memoryRuns.find((run) => run.id === idempotencyKey);
|
||||
|
||||
if (existing) {
|
||||
return existing;
|
||||
return {
|
||||
run: existing,
|
||||
created: false,
|
||||
};
|
||||
}
|
||||
|
||||
const run = this.buildInitialRun(input, idempotencyKey);
|
||||
this.memoryRuns.unshift(run);
|
||||
return run;
|
||||
return {
|
||||
run,
|
||||
created: true,
|
||||
};
|
||||
}
|
||||
|
||||
private buildInitialRun(
|
||||
|
||||
@@ -114,11 +114,12 @@ export class DeployRunsService {
|
||||
|
||||
const project = await this.projectsService.getProject(input.projectKey);
|
||||
await this.validateReleaseRequest(input, project);
|
||||
const run = await this.deployRunRepository.create(input);
|
||||
const creation = await this.deployRunRepository.create(input);
|
||||
const run = creation.run;
|
||||
const confirmation = this.productionConfirmationDigest(input);
|
||||
|
||||
await this.auditService.record({
|
||||
action: 'DEPLOY_REQUESTED',
|
||||
action: creation.created ? 'DEPLOY_REQUESTED' : 'DEPLOY_REQUEST_REUSED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
actorName: input.operator,
|
||||
@@ -132,6 +133,10 @@ export class DeployRunsService {
|
||||
},
|
||||
});
|
||||
|
||||
if (!creation.created) {
|
||||
return run;
|
||||
}
|
||||
|
||||
return this.deployExecutionService.execute(run, project);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user