feat: 增加发布单版本冲突保护
- deploy-run: 将 version 纳入发布单摘要并在 run/step 更新时递增 - repository: 使用 expectedVersion 做乐观锁更新,冲突返回 CONFLICT - execution: Jenkins 同步、通知、取消路径按最新版本串行推进
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
Prisma,
|
||||
RunStepStatus as PrismaRunStepStatus,
|
||||
} from '@prisma/client';
|
||||
import { AppError } from '../common/errors/app-error';
|
||||
import { EnvConfig } from '../config/env.schema';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { seedDeployRuns } from './deploy-run.seed';
|
||||
@@ -217,9 +218,10 @@ export class DeployRunRepository {
|
||||
async updateRun(
|
||||
id: string,
|
||||
patch: DeployRunMemoryPatch,
|
||||
expectedVersion?: number,
|
||||
): Promise<DeployRunSummary | null> {
|
||||
if (!this.useDatabase()) {
|
||||
return this.updateMemoryRun(id, patch);
|
||||
return this.updateMemoryRun(id, patch, expectedVersion);
|
||||
}
|
||||
|
||||
const current = await this.prisma.deployRun.findUnique({
|
||||
@@ -279,11 +281,16 @@ export class DeployRunRepository {
|
||||
data.finishedAt = this.toDate(patch.finishedAt);
|
||||
}
|
||||
|
||||
await this.prisma.deployRun.update({
|
||||
where: { id },
|
||||
const version = expectedVersion ?? current.version;
|
||||
const updated = await this.prisma.deployRun.updateMany({
|
||||
where: { id, version },
|
||||
data,
|
||||
});
|
||||
|
||||
if (updated.count === 0) {
|
||||
await this.raiseRunVersionConflict(id, version);
|
||||
}
|
||||
|
||||
return this.findById(id);
|
||||
}
|
||||
|
||||
@@ -291,9 +298,15 @@ export class DeployRunRepository {
|
||||
runId: string,
|
||||
bpmnNodeId: string,
|
||||
patch: DeployRunStepMemoryPatch,
|
||||
expectedRunVersion?: number,
|
||||
): Promise<DeployRunSummary | null> {
|
||||
if (!this.useDatabase()) {
|
||||
return this.updateMemoryStep(runId, bpmnNodeId, patch);
|
||||
return this.updateMemoryStep(
|
||||
runId,
|
||||
bpmnNodeId,
|
||||
patch,
|
||||
expectedRunVersion,
|
||||
);
|
||||
}
|
||||
|
||||
const current = await this.prisma.runStep.findUnique({
|
||||
@@ -303,6 +316,9 @@ export class DeployRunRepository {
|
||||
bpmnNodeId,
|
||||
},
|
||||
},
|
||||
include: {
|
||||
deployRun: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!current) {
|
||||
@@ -355,19 +371,89 @@ export class DeployRunRepository {
|
||||
data.metadata = metadata;
|
||||
}
|
||||
|
||||
await this.prisma.runStep.update({
|
||||
where: {
|
||||
deployRunId_bpmnNodeId: {
|
||||
deployRunId: runId,
|
||||
bpmnNodeId,
|
||||
const runVersion = expectedRunVersion ?? current.deployRun.version;
|
||||
|
||||
await this.prisma.$transaction(async (tx) => {
|
||||
const updatedRun = await tx.deployRun.updateMany({
|
||||
where: {
|
||||
id: runId,
|
||||
version: runVersion,
|
||||
},
|
||||
},
|
||||
data,
|
||||
data: {
|
||||
version: {
|
||||
increment: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (updatedRun.count === 0) {
|
||||
throw this.createRunVersionConflict(runId, runVersion, {
|
||||
version: current.deployRun.version,
|
||||
status: current.deployRun.status,
|
||||
updatedAt: current.deployRun.updatedAt,
|
||||
});
|
||||
}
|
||||
|
||||
await tx.runStep.update({
|
||||
where: {
|
||||
deployRunId_bpmnNodeId: {
|
||||
deployRunId: runId,
|
||||
bpmnNodeId,
|
||||
},
|
||||
},
|
||||
data,
|
||||
});
|
||||
});
|
||||
|
||||
return this.findById(runId);
|
||||
}
|
||||
|
||||
private async raiseRunVersionConflict(
|
||||
id: string,
|
||||
expectedVersion: number,
|
||||
): Promise<never> {
|
||||
throw await this.buildRunVersionConflict(id, expectedVersion);
|
||||
}
|
||||
|
||||
private async buildRunVersionConflict(
|
||||
id: string,
|
||||
expectedVersion: number,
|
||||
): Promise<AppError> {
|
||||
const latest = await this.prisma.deployRun.findUnique({
|
||||
where: { id },
|
||||
select: {
|
||||
status: true,
|
||||
version: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return this.createRunVersionConflict(id, expectedVersion, latest);
|
||||
}
|
||||
|
||||
private createRunVersionConflict(
|
||||
id: string,
|
||||
expectedVersion: number,
|
||||
latest?: {
|
||||
status?: PrismaDeployRunStatus;
|
||||
version?: number;
|
||||
updatedAt?: Date;
|
||||
} | null,
|
||||
): AppError {
|
||||
return new AppError(
|
||||
'CONFLICT',
|
||||
`发布记录 ${id} 已被其他流程更新,请刷新后重试`,
|
||||
409,
|
||||
{
|
||||
id,
|
||||
expectedVersion,
|
||||
latestVersion: latest?.version,
|
||||
latestStatus: latest?.status?.toLowerCase(),
|
||||
latestUpdatedAt: latest?.updatedAt?.toISOString(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private async findRecordById(id: string): Promise<DeployRunRecord | null> {
|
||||
return this.prisma.deployRun.findUnique({
|
||||
where: { id },
|
||||
@@ -440,6 +526,7 @@ export class DeployRunRepository {
|
||||
|
||||
return {
|
||||
id: idempotencyKey,
|
||||
version: 1,
|
||||
projectKey: input.projectKey,
|
||||
environment: input.environment,
|
||||
ref: input.ref,
|
||||
@@ -497,6 +584,7 @@ export class DeployRunRepository {
|
||||
private updateMemoryRun(
|
||||
id: string,
|
||||
patch: DeployRunMemoryPatch,
|
||||
expectedVersion?: number,
|
||||
): DeployRunSummary | null {
|
||||
const run = this.memoryRuns.find((item) => item.id === id);
|
||||
|
||||
@@ -504,6 +592,7 @@ export class DeployRunRepository {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.assertMemoryVersion(run, expectedVersion);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
if (patch.status) {
|
||||
@@ -542,6 +631,7 @@ export class DeployRunRepository {
|
||||
run.finishedAt = patch.finishedAt;
|
||||
}
|
||||
|
||||
run.version += 1;
|
||||
return run;
|
||||
}
|
||||
|
||||
@@ -549,6 +639,7 @@ export class DeployRunRepository {
|
||||
runId: string,
|
||||
bpmnNodeId: string,
|
||||
patch: DeployRunStepMemoryPatch,
|
||||
expectedRunVersion?: number,
|
||||
): DeployRunSummary | null {
|
||||
const run = this.memoryRuns.find((item) => item.id === runId);
|
||||
const step = run?.steps.find((item) => item.bpmnNodeId === bpmnNodeId);
|
||||
@@ -557,6 +648,7 @@ export class DeployRunRepository {
|
||||
return null;
|
||||
}
|
||||
|
||||
this.assertMemoryVersion(run, expectedRunVersion);
|
||||
const now = new Date().toISOString();
|
||||
|
||||
if (patch.status) {
|
||||
@@ -591,15 +683,38 @@ export class DeployRunRepository {
|
||||
step.logExcerpt = patch.logExcerpt;
|
||||
}
|
||||
|
||||
run.version += 1;
|
||||
return run;
|
||||
}
|
||||
|
||||
private assertMemoryVersion(
|
||||
run: DeployRunSummary,
|
||||
expectedVersion?: number,
|
||||
): void {
|
||||
if (expectedVersion === undefined || run.version === expectedVersion) {
|
||||
return;
|
||||
}
|
||||
|
||||
throw new AppError(
|
||||
'CONFLICT',
|
||||
`发布记录 ${run.id} 已被其他流程更新,请刷新后重试`,
|
||||
409,
|
||||
{
|
||||
id: run.id,
|
||||
expectedVersion,
|
||||
latestVersion: run.version,
|
||||
latestStatus: run.status,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
private toSummary(run: DeployRunRecord): DeployRunSummary {
|
||||
const metadata = this.objectMetadata<DeployRunMetadata>(run.metadata);
|
||||
const sortedSteps = [...run.steps].sort((left, right) => left.order - right.order);
|
||||
|
||||
return {
|
||||
id: run.id,
|
||||
version: run.version,
|
||||
projectKey: run.project.key,
|
||||
environment: run.environment.name === 'TEST' ? 'test' : 'production',
|
||||
ref: run.ref,
|
||||
|
||||
Reference in New Issue
Block a user