fix: Admin Retry 幂等键从 timestamp 改为 operationId
All checks were successful
Deploy API Server / build-and-unit (push) Successful in 34s
Deploy API Server / current-integration (push) Successful in 30s
Deploy API Server / backward-compat (push) Successful in 0s
Deploy API Server / m-ai-03-synthetic-e2e (push) Successful in 9s
Deploy API Server / deploy (push) Successful in 1m1s
All checks were successful
Deploy API Server / build-and-unit (push) Successful in 34s
Deploy API Server / current-integration (push) Successful in 30s
Deploy API Server / backward-compat (push) Successful in 0s
Deploy API Server / m-ai-03-synthetic-e2e (push) Successful in 9s
Deploy API Server / deploy (push) Successful in 1m1s
- retryJob 新增 operationId 参数(必填,调用方提供稳定操作标识) - 幂等键: admin-retry:<jobId>:<operationId>(同 operationId → 同 Job) - Admin Controller: operationId 校验(1-128 字符,alphanumeric + hyphen/underscore) - 不同 operationId 允许管理员多次独立重试同一失败 Job Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
d0f75531ab
commit
6962db3f1a
@ -1,4 +1,4 @@
|
|||||||
import { Controller, Get, Post, Param, Query, Body, UseGuards } from '@nestjs/common';
|
import { Controller, Get, Post, Param, Query, Body, UseGuards, BadRequestException } from '@nestjs/common';
|
||||||
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
import { ApiTags, ApiBearerAuth, ApiOperation } from '@nestjs/swagger';
|
||||||
import { InjectQueue } from '@nestjs/bullmq';
|
import { InjectQueue } from '@nestjs/bullmq';
|
||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
@ -47,8 +47,14 @@ export class AiJobAdminController {
|
|||||||
@Post(':jobId/retry')
|
@Post(':jobId/retry')
|
||||||
@AdminRoles('SUPER_ADMIN' as AdminRole)
|
@AdminRoles('SUPER_ADMIN' as AdminRole)
|
||||||
@ApiOperation({ summary: 'Admin 重试(创建新 Job,parentJobId 关联原 Job)' })
|
@ApiOperation({ summary: 'Admin 重试(创建新 Job,parentJobId 关联原 Job)' })
|
||||||
async retry(@Param('jobId') jobId: string) {
|
async retry(@Param('jobId') jobId: string, @Query('operationId') operationId: string) {
|
||||||
return this.service.retryJob('admin', jobId);
|
if (!operationId || operationId.trim().length === 0) {
|
||||||
|
throw new BadRequestException('operationId is required for Admin retry');
|
||||||
|
}
|
||||||
|
if (operationId.length > 128 || !operationId.match(/^[a-zA-Z0-9_-]+$/)) {
|
||||||
|
throw new BadRequestException('operationId must be 1-128 alphanumeric chars, hyphens, or underscores');
|
||||||
|
}
|
||||||
|
return this.service.retryJob('admin', jobId, operationId.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Get('queues/status')
|
@Get('queues/status')
|
||||||
|
|||||||
@ -139,7 +139,7 @@ export class AiJobService {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
async retryJob(adminUserId: string, jobId: string) {
|
async retryJob(adminUserId: string, jobId: string, operationId: string) {
|
||||||
// 1. Load original Job
|
// 1. Load original Job
|
||||||
const original = await this.prisma.aiJob.findUnique({
|
const original = await this.prisma.aiJob.findUnique({
|
||||||
where: { id: jobId },
|
where: { id: jobId },
|
||||||
@ -163,9 +163,10 @@ export class AiJobService {
|
|||||||
throw new ConflictException('Original job missing targetType/targetId — cannot retry');
|
throw new ConflictException('Original job missing targetType/targetId — cannot retry');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Idempotency key: allows same admin operation to return same retry job,
|
// 5. Stable idempotency key using caller-provided operationId.
|
||||||
// but different retry operations produce different jobs
|
// Same (jobId, operationId) → same retry Job (幂等).
|
||||||
const retryIdempotencyKey = `admin-retry:${jobId}:${Date.now()}`;
|
// Different operationId → different retry Job (允许管理员多次独立重试).
|
||||||
|
const retryIdempotencyKey = `admin-retry:${jobId}:${operationId}`;
|
||||||
|
|
||||||
// 6. Delegate to AiJobCreationService (Registry + Snapshot + Outbox + atomic transaction)
|
// 6. Delegate to AiJobCreationService (Registry + Snapshot + Outbox + atomic transaction)
|
||||||
const newJob = await this.creationService.createJob({
|
const newJob = await this.creationService.createJob({
|
||||||
@ -185,7 +186,7 @@ export class AiJobService {
|
|||||||
|
|
||||||
this.logger.log(
|
this.logger.log(
|
||||||
`Admin retry: original=${jobId} → new=${newJob.id} ` +
|
`Admin retry: original=${jobId} → new=${newJob.id} ` +
|
||||||
`type=${newJob.jobType} parentJobId=${original.id}`,
|
`operationId=${operationId} parentJobId=${original.id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.toAdminResponse(newJob);
|
return this.toAdminResponse(newJob);
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user