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 DeployRunDatabaseClient = PrismaService | Prisma.TransactionClient;
|
||||||
|
type CreateDeployRunResult = {
|
||||||
|
run: DeployRunSummary;
|
||||||
|
created: boolean;
|
||||||
|
};
|
||||||
|
type CreateDatabaseDeployRunResult = {
|
||||||
|
record: DeployRunRecord;
|
||||||
|
created: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
type DeployRunMetadata = Prisma.InputJsonObject & {
|
type DeployRunMetadata = Prisma.InputJsonObject & {
|
||||||
jenkinsBuildUrl?: string;
|
jenkinsBuildUrl?: string;
|
||||||
@@ -143,16 +151,19 @@ export class DeployRunRepository {
|
|||||||
return run ? this.toSummary(run) : null;
|
return run ? this.toSummary(run) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(input: CreateDeployRunInput): Promise<DeployRunSummary> {
|
async create(input: CreateDeployRunInput): Promise<CreateDeployRunResult> {
|
||||||
const idempotencyKey = this.resolveIdempotencyKey(input);
|
const idempotencyKey = this.resolveIdempotencyKey(input);
|
||||||
|
|
||||||
if (!this.useDatabase()) {
|
if (!this.useDatabase()) {
|
||||||
return this.createMemoryRun(input, idempotencyKey);
|
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(
|
async updateStatus(
|
||||||
@@ -437,7 +448,7 @@ export class DeployRunRepository {
|
|||||||
private async createDatabaseRun(
|
private async createDatabaseRun(
|
||||||
input: CreateDeployRunInput,
|
input: CreateDeployRunInput,
|
||||||
idempotencyKey: string,
|
idempotencyKey: string,
|
||||||
): Promise<DeployRunRecord> {
|
): Promise<CreateDatabaseDeployRunResult> {
|
||||||
try {
|
try {
|
||||||
return await this.prisma.$transaction(async (tx) => {
|
return await this.prisma.$transaction(async (tx) => {
|
||||||
const existing = await this.findRecordByIdempotencyKey(
|
const existing = await this.findRecordByIdempotencyKey(
|
||||||
@@ -446,14 +457,17 @@ export class DeployRunRepository {
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return existing;
|
return {
|
||||||
|
record: existing,
|
||||||
|
created: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const target = await this.findTargetProjectEnvironment(input, tx);
|
const target = await this.findTargetProjectEnvironment(input, tx);
|
||||||
const initialRun = this.buildInitialRun(input, idempotencyKey);
|
const initialRun = this.buildInitialRun(input, idempotencyKey);
|
||||||
const metadata = this.inputMetadata(input);
|
const metadata = this.inputMetadata(input);
|
||||||
|
|
||||||
return tx.deployRun.create({
|
const record = await tx.deployRun.create({
|
||||||
data: {
|
data: {
|
||||||
projectId: target.project.id,
|
projectId: target.project.id,
|
||||||
environmentId: target.environment.id,
|
environmentId: target.environment.id,
|
||||||
@@ -483,13 +497,21 @@ export class DeployRunRepository {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
record,
|
||||||
|
created: true,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (this.isIdempotencyUniqueConflict(error)) {
|
if (this.isIdempotencyUniqueConflict(error)) {
|
||||||
const existing = await this.findRecordByIdempotencyKey(idempotencyKey);
|
const existing = await this.findRecordByIdempotencyKey(idempotencyKey);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return existing;
|
return {
|
||||||
|
record: existing,
|
||||||
|
created: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -565,16 +587,22 @@ export class DeployRunRepository {
|
|||||||
private createMemoryRun(
|
private createMemoryRun(
|
||||||
input: CreateDeployRunInput,
|
input: CreateDeployRunInput,
|
||||||
idempotencyKey: string,
|
idempotencyKey: string,
|
||||||
): DeployRunSummary {
|
): CreateDeployRunResult {
|
||||||
const existing = this.memoryRuns.find((run) => run.id === idempotencyKey);
|
const existing = this.memoryRuns.find((run) => run.id === idempotencyKey);
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
return existing;
|
return {
|
||||||
|
run: existing,
|
||||||
|
created: false,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const run = this.buildInitialRun(input, idempotencyKey);
|
const run = this.buildInitialRun(input, idempotencyKey);
|
||||||
this.memoryRuns.unshift(run);
|
this.memoryRuns.unshift(run);
|
||||||
return run;
|
return {
|
||||||
|
run,
|
||||||
|
created: true,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildInitialRun(
|
private buildInitialRun(
|
||||||
|
|||||||
@@ -114,11 +114,12 @@ export class DeployRunsService {
|
|||||||
|
|
||||||
const project = await this.projectsService.getProject(input.projectKey);
|
const project = await this.projectsService.getProject(input.projectKey);
|
||||||
await this.validateReleaseRequest(input, project);
|
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);
|
const confirmation = this.productionConfirmationDigest(input);
|
||||||
|
|
||||||
await this.auditService.record({
|
await this.auditService.record({
|
||||||
action: 'DEPLOY_REQUESTED',
|
action: creation.created ? 'DEPLOY_REQUESTED' : 'DEPLOY_REQUEST_REUSED',
|
||||||
resourceType: 'deploy_run',
|
resourceType: 'deploy_run',
|
||||||
resourceId: run.id,
|
resourceId: run.id,
|
||||||
actorName: input.operator,
|
actorName: input.operator,
|
||||||
@@ -132,6 +133,10 @@ export class DeployRunsService {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (!creation.created) {
|
||||||
|
return run;
|
||||||
|
}
|
||||||
|
|
||||||
return this.deployExecutionService.execute(run, project);
|
return this.deployExecutionService.execute(run, project);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user