mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 16:07:11 +08:00
736 lines
23 KiB
TypeScript
736 lines
23 KiB
TypeScript
import {
|
|
DataTypes,
|
|
Model,
|
|
ModelStatic,
|
|
Sequelize,
|
|
Transaction,
|
|
UniqueConstraintError,
|
|
} from 'sequelize';
|
|
import {
|
|
RUN_EVENT_TABLE,
|
|
RUN_TABLE,
|
|
RUN_ATTEMPT_TABLE,
|
|
} from '../../../migrations/0002-run-schema';
|
|
import { RUN_CANCELLATION_DISPATCH_TABLE } from '../../../migrations/0005-run-cancellation-dispatch';
|
|
import {
|
|
CANCELLATION_DISPATCH_RESULTS,
|
|
CANCELLATION_DISPATCH_STATUSES,
|
|
type CancellationDispatchRecord,
|
|
type CancellationDispatchResult,
|
|
type CancellationDispatchStatus,
|
|
} from '../../domain/cancellationDispatch';
|
|
import {
|
|
CancellationDispatchBindingConflictError,
|
|
CancellationDispatchFenceRejectedError,
|
|
CancellationDispatchRepositoryError,
|
|
InvalidCancellationDispatchCommandError,
|
|
} from '../../domain/cancellationDispatchErrors';
|
|
import type { RunEventRecord, RunStatus } from '../../domain/run';
|
|
import type {
|
|
CancellationDispatchRepository,
|
|
ClaimCancellationDispatchCommand,
|
|
ClaimCancellationDispatchResult,
|
|
RecordCancellationDispatchResult,
|
|
RecordCancellationDispatchResultCommand,
|
|
} from '../../ports/cancellationDispatchRepository';
|
|
|
|
const ACTIVE_RUN_STATUSES: readonly RunStatus[] = [
|
|
'created',
|
|
'queued',
|
|
'dispatching',
|
|
'running',
|
|
'waiting_approval',
|
|
'retry_wait',
|
|
'lost',
|
|
];
|
|
const ACTIVE_ATTEMPT_STATUSES = ['claimed', 'starting', 'running'] as const;
|
|
const RETRYABLE_RESULTS: readonly CancellationDispatchResult[] = [
|
|
'controller_missing',
|
|
'handle_missing',
|
|
'dispatch_error',
|
|
];
|
|
const BLOCKING_RESULTS: readonly CancellationDispatchResult[] = [
|
|
'identity_mismatch',
|
|
'pid_mismatch',
|
|
'unsupported',
|
|
'invalid',
|
|
];
|
|
|
|
interface CancellationDispatchRow {
|
|
runId: string;
|
|
attemptId: string;
|
|
status: string;
|
|
version: number;
|
|
dispatchCount: number;
|
|
nextAttemptAtMs: number | null;
|
|
leaseOwner: string | null;
|
|
leaseToken: string | null;
|
|
leaseExpiresAtMs: number | null;
|
|
lastResult: string | null;
|
|
lastDispatchedAtMs: number | null;
|
|
createdAtMs: number;
|
|
updatedAtMs: number;
|
|
}
|
|
|
|
interface CancellationDispatchRunRow {
|
|
id: string;
|
|
executionOwner: string;
|
|
status: string;
|
|
version: number;
|
|
eventSequence: number;
|
|
cancelRequestedAtMs: number | null;
|
|
}
|
|
|
|
interface CancellationDispatchAttemptRow {
|
|
id: string;
|
|
runId: string;
|
|
status: string;
|
|
}
|
|
|
|
interface CancellationDispatchEventRow {
|
|
id: string;
|
|
runId: string;
|
|
sequence: number;
|
|
type: string;
|
|
dedupeKey: string;
|
|
actorType: string;
|
|
actorId: string;
|
|
attemptId: string;
|
|
payload: Readonly<Record<string, unknown>>;
|
|
createdAtMs: number;
|
|
}
|
|
|
|
interface CancellationDispatchInstance
|
|
extends Model<CancellationDispatchRow, CancellationDispatchRow>,
|
|
CancellationDispatchRow {}
|
|
interface CancellationDispatchRunInstance
|
|
extends Model<CancellationDispatchRunRow, CancellationDispatchRunRow>,
|
|
CancellationDispatchRunRow {}
|
|
interface CancellationDispatchAttemptInstance
|
|
extends Model<CancellationDispatchAttemptRow, CancellationDispatchAttemptRow>,
|
|
CancellationDispatchAttemptRow {}
|
|
interface CancellationDispatchEventInstance
|
|
extends Model<CancellationDispatchEventRow, CancellationDispatchEventRow>,
|
|
CancellationDispatchEventRow {}
|
|
|
|
function defineDispatchModel(
|
|
database: Sequelize,
|
|
): ModelStatic<CancellationDispatchInstance> {
|
|
return database.define<CancellationDispatchInstance>(
|
|
'Ql3CancellationDispatch',
|
|
{
|
|
runId: { field: 'run_id', type: DataTypes.STRING(36), primaryKey: true },
|
|
attemptId: {
|
|
field: 'attempt_id',
|
|
type: DataTypes.STRING(36),
|
|
allowNull: false,
|
|
},
|
|
status: { type: DataTypes.STRING(32), allowNull: false },
|
|
version: { type: DataTypes.INTEGER, allowNull: false },
|
|
dispatchCount: {
|
|
field: 'dispatch_count',
|
|
type: DataTypes.INTEGER,
|
|
allowNull: false,
|
|
},
|
|
nextAttemptAtMs: {
|
|
field: 'next_attempt_at_ms',
|
|
type: DataTypes.BIGINT,
|
|
allowNull: true,
|
|
},
|
|
leaseOwner: {
|
|
field: 'lease_owner',
|
|
type: DataTypes.STRING(128),
|
|
allowNull: true,
|
|
},
|
|
leaseToken: {
|
|
field: 'lease_token',
|
|
type: DataTypes.STRING(128),
|
|
allowNull: true,
|
|
},
|
|
leaseExpiresAtMs: {
|
|
field: 'lease_expires_at_ms',
|
|
type: DataTypes.BIGINT,
|
|
allowNull: true,
|
|
},
|
|
lastResult: {
|
|
field: 'last_result',
|
|
type: DataTypes.STRING(64),
|
|
allowNull: true,
|
|
},
|
|
lastDispatchedAtMs: {
|
|
field: 'last_dispatched_at_ms',
|
|
type: DataTypes.BIGINT,
|
|
allowNull: true,
|
|
},
|
|
createdAtMs: {
|
|
field: 'created_at_ms',
|
|
type: DataTypes.BIGINT,
|
|
allowNull: false,
|
|
},
|
|
updatedAtMs: {
|
|
field: 'updated_at_ms',
|
|
type: DataTypes.BIGINT,
|
|
allowNull: false,
|
|
},
|
|
},
|
|
{
|
|
tableName: RUN_CANCELLATION_DISPATCH_TABLE,
|
|
timestamps: false,
|
|
freezeTableName: true,
|
|
},
|
|
);
|
|
}
|
|
|
|
function defineRunModel(
|
|
database: Sequelize,
|
|
): ModelStatic<CancellationDispatchRunInstance> {
|
|
return database.define<CancellationDispatchRunInstance>(
|
|
'Ql3CancellationDispatchRun',
|
|
{
|
|
id: { type: DataTypes.STRING(36), primaryKey: true },
|
|
executionOwner: {
|
|
field: 'execution_owner',
|
|
type: DataTypes.STRING(16),
|
|
allowNull: false,
|
|
},
|
|
status: { type: DataTypes.STRING(32), allowNull: false },
|
|
version: { type: DataTypes.INTEGER, allowNull: false },
|
|
eventSequence: {
|
|
field: 'event_sequence',
|
|
type: DataTypes.INTEGER,
|
|
allowNull: false,
|
|
},
|
|
cancelRequestedAtMs: {
|
|
field: 'cancel_requested_at_ms',
|
|
type: DataTypes.BIGINT,
|
|
allowNull: true,
|
|
},
|
|
},
|
|
{ tableName: RUN_TABLE, timestamps: false, freezeTableName: true },
|
|
);
|
|
}
|
|
|
|
function defineAttemptModel(
|
|
database: Sequelize,
|
|
): ModelStatic<CancellationDispatchAttemptInstance> {
|
|
return database.define<CancellationDispatchAttemptInstance>(
|
|
'Ql3CancellationDispatchAttempt',
|
|
{
|
|
id: { type: DataTypes.STRING(36), primaryKey: true },
|
|
runId: { field: 'run_id', type: DataTypes.STRING(36), allowNull: false },
|
|
status: { type: DataTypes.STRING(32), allowNull: false },
|
|
},
|
|
{ tableName: RUN_ATTEMPT_TABLE, timestamps: false, freezeTableName: true },
|
|
);
|
|
}
|
|
|
|
function defineEventModel(
|
|
database: Sequelize,
|
|
): ModelStatic<CancellationDispatchEventInstance> {
|
|
return database.define<CancellationDispatchEventInstance>(
|
|
'Ql3CancellationDispatchEvent',
|
|
{
|
|
id: { type: DataTypes.STRING(36), primaryKey: true },
|
|
runId: { field: 'run_id', type: DataTypes.STRING(36), allowNull: false },
|
|
sequence: { type: DataTypes.INTEGER, allowNull: false },
|
|
type: { type: DataTypes.STRING(128), allowNull: false },
|
|
dedupeKey: {
|
|
field: 'dedupe_key',
|
|
type: DataTypes.STRING(255),
|
|
allowNull: false,
|
|
},
|
|
actorType: {
|
|
field: 'actor_type',
|
|
type: DataTypes.STRING(64),
|
|
allowNull: false,
|
|
},
|
|
actorId: {
|
|
field: 'actor_id',
|
|
type: DataTypes.STRING(255),
|
|
allowNull: false,
|
|
},
|
|
attemptId: {
|
|
field: 'attempt_id',
|
|
type: DataTypes.STRING(36),
|
|
allowNull: false,
|
|
},
|
|
payload: { type: DataTypes.JSON, allowNull: false },
|
|
createdAtMs: {
|
|
field: 'created_at_ms',
|
|
type: DataTypes.BIGINT,
|
|
allowNull: false,
|
|
},
|
|
},
|
|
{ tableName: RUN_EVENT_TABLE, timestamps: false, freezeTableName: true },
|
|
);
|
|
}
|
|
|
|
function assertId(name: string, value: string, maxLength = 36): void {
|
|
if (!value || value.length > maxLength) {
|
|
throw new InvalidCancellationDispatchCommandError(
|
|
`${name} must be between 1 and ${maxLength} characters`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertTimestamp(name: string, value: number): void {
|
|
if (!Number.isSafeInteger(value) || value < 0) {
|
|
throw new InvalidCancellationDispatchCommandError(
|
|
`${name} must be a non-negative safe integer`,
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertClaim(command: ClaimCancellationDispatchCommand): void {
|
|
assertId('runId', command.runId);
|
|
assertId('attemptId', command.attemptId);
|
|
assertId('owner', command.owner, 128);
|
|
assertId('leaseToken', command.leaseToken, 128);
|
|
assertTimestamp('requestedAtMs', command.requestedAtMs);
|
|
assertTimestamp('nowMs', command.nowMs);
|
|
if (
|
|
!Number.isSafeInteger(command.leaseDurationMs) ||
|
|
command.leaseDurationMs < 1
|
|
) {
|
|
throw new InvalidCancellationDispatchCommandError(
|
|
'leaseDurationMs must be a positive safe integer',
|
|
);
|
|
}
|
|
if (!Number.isSafeInteger(command.nowMs + command.leaseDurationMs)) {
|
|
throw new InvalidCancellationDispatchCommandError(
|
|
'lease expiry exceeds the supported range',
|
|
);
|
|
}
|
|
}
|
|
|
|
function assertRecordResult(
|
|
command: RecordCancellationDispatchResultCommand,
|
|
): void {
|
|
assertId('runId', command.runId);
|
|
assertId('attemptId', command.attemptId);
|
|
assertId('owner', command.owner, 128);
|
|
assertId('leaseToken', command.leaseToken, 128);
|
|
assertId('eventId', command.eventId);
|
|
assertTimestamp('atMs', command.atMs);
|
|
if (
|
|
!Number.isSafeInteger(command.expectedVersion) ||
|
|
command.expectedVersion < 1
|
|
) {
|
|
throw new InvalidCancellationDispatchCommandError(
|
|
'expectedVersion must be a positive safe integer',
|
|
);
|
|
}
|
|
if (!CANCELLATION_DISPATCH_RESULTS.includes(command.result)) {
|
|
throw new InvalidCancellationDispatchCommandError(
|
|
'result is not supported',
|
|
);
|
|
}
|
|
if (RETRYABLE_RESULTS.includes(command.result)) {
|
|
if (
|
|
command.nextAttemptAtMs === undefined ||
|
|
!Number.isSafeInteger(command.nextAttemptAtMs) ||
|
|
command.nextAttemptAtMs <= command.atMs
|
|
) {
|
|
throw new InvalidCancellationDispatchCommandError(
|
|
'retryable results require nextAttemptAtMs greater than atMs',
|
|
);
|
|
}
|
|
} else if (command.nextAttemptAtMs !== undefined) {
|
|
throw new InvalidCancellationDispatchCommandError(
|
|
'terminal results must not include nextAttemptAtMs',
|
|
);
|
|
}
|
|
}
|
|
|
|
function rowToDispatch(
|
|
row: CancellationDispatchRow,
|
|
): CancellationDispatchRecord {
|
|
if (
|
|
!CANCELLATION_DISPATCH_STATUSES.includes(
|
|
row.status as CancellationDispatchStatus,
|
|
)
|
|
) {
|
|
throw new CancellationDispatchRepositoryError(
|
|
new Error(`Unsupported cancellation dispatch status: ${row.status}`),
|
|
);
|
|
}
|
|
if (
|
|
row.lastResult !== null &&
|
|
!CANCELLATION_DISPATCH_RESULTS.includes(
|
|
row.lastResult as CancellationDispatchResult,
|
|
)
|
|
) {
|
|
throw new CancellationDispatchRepositoryError(
|
|
new Error(`Unsupported cancellation dispatch result: ${row.lastResult}`),
|
|
);
|
|
}
|
|
for (const [name, value] of [
|
|
['version', row.version],
|
|
['dispatchCount', row.dispatchCount],
|
|
['createdAtMs', row.createdAtMs],
|
|
['updatedAtMs', row.updatedAtMs],
|
|
['nextAttemptAtMs', row.nextAttemptAtMs],
|
|
['leaseExpiresAtMs', row.leaseExpiresAtMs],
|
|
['lastDispatchedAtMs', row.lastDispatchedAtMs],
|
|
] as const) {
|
|
if (
|
|
value !== null &&
|
|
(!Number.isSafeInteger(Number(value)) || Number(value) < 0)
|
|
) {
|
|
throw new CancellationDispatchRepositoryError(
|
|
new Error(`Invalid cancellation dispatch ${name}`),
|
|
);
|
|
}
|
|
}
|
|
const status = row.status as CancellationDispatchStatus;
|
|
const hasCompleteLease =
|
|
row.leaseOwner !== null &&
|
|
row.leaseToken !== null &&
|
|
row.leaseExpiresAtMs !== null;
|
|
const hasAnyLease =
|
|
row.leaseOwner !== null ||
|
|
row.leaseToken !== null ||
|
|
row.leaseExpiresAtMs !== null;
|
|
if (
|
|
(status === 'leased' && !hasCompleteLease) ||
|
|
(status !== 'leased' && hasAnyLease) ||
|
|
((status === 'pending' || status === 'retry_wait') &&
|
|
row.nextAttemptAtMs === null) ||
|
|
((status === 'dispatched' || status === 'blocked' || status === 'leased') &&
|
|
row.nextAttemptAtMs !== null) ||
|
|
((status === 'dispatched' || status === 'blocked') &&
|
|
row.lastResult === null)
|
|
) {
|
|
throw new CancellationDispatchRepositoryError(
|
|
new Error('Cancellation dispatch lease/status fields are inconsistent'),
|
|
);
|
|
}
|
|
return {
|
|
runId: row.runId,
|
|
attemptId: row.attemptId,
|
|
status,
|
|
version: Number(row.version),
|
|
dispatchCount: Number(row.dispatchCount),
|
|
createdAtMs: Number(row.createdAtMs),
|
|
updatedAtMs: Number(row.updatedAtMs),
|
|
...(row.nextAttemptAtMs === null
|
|
? {}
|
|
: { nextAttemptAtMs: Number(row.nextAttemptAtMs) }),
|
|
...(row.leaseOwner === null ? {} : { leaseOwner: row.leaseOwner }),
|
|
...(row.leaseToken === null ? {} : { leaseToken: row.leaseToken }),
|
|
...(row.leaseExpiresAtMs === null
|
|
? {}
|
|
: { leaseExpiresAtMs: Number(row.leaseExpiresAtMs) }),
|
|
...(row.lastResult === null
|
|
? {}
|
|
: { lastResult: row.lastResult as CancellationDispatchResult }),
|
|
...(row.lastDispatchedAtMs === null
|
|
? {}
|
|
: { lastDispatchedAtMs: Number(row.lastDispatchedAtMs) }),
|
|
};
|
|
}
|
|
|
|
function resultState(result: CancellationDispatchResult): {
|
|
status: CancellationDispatchStatus;
|
|
eventType: string;
|
|
} {
|
|
if (RETRYABLE_RESULTS.includes(result)) {
|
|
return { status: 'retry_wait', eventType: 'run.cancel_dispatch_failed' };
|
|
}
|
|
if (BLOCKING_RESULTS.includes(result)) {
|
|
return { status: 'blocked', eventType: 'run.cancel_dispatch_blocked' };
|
|
}
|
|
return { status: 'dispatched', eventType: 'run.cancel_dispatched' };
|
|
}
|
|
|
|
function withoutScheduleAndLease(
|
|
dispatch: CancellationDispatchRecord,
|
|
): Omit<
|
|
CancellationDispatchRecord,
|
|
'nextAttemptAtMs' | 'leaseOwner' | 'leaseToken' | 'leaseExpiresAtMs'
|
|
> {
|
|
const {
|
|
nextAttemptAtMs: _nextAttemptAtMs,
|
|
leaseOwner: _leaseOwner,
|
|
leaseToken: _leaseToken,
|
|
leaseExpiresAtMs: _leaseExpiresAtMs,
|
|
...rest
|
|
} = dispatch;
|
|
return rest;
|
|
}
|
|
|
|
export class LegacySequelizeCancellationDispatchRepository
|
|
implements CancellationDispatchRepository
|
|
{
|
|
private readonly dispatch: ModelStatic<CancellationDispatchInstance>;
|
|
private readonly run: ModelStatic<CancellationDispatchRunInstance>;
|
|
private readonly attempt: ModelStatic<CancellationDispatchAttemptInstance>;
|
|
private readonly event: ModelStatic<CancellationDispatchEventInstance>;
|
|
|
|
constructor(private readonly database: Sequelize) {
|
|
this.dispatch = defineDispatchModel(database);
|
|
this.run = defineRunModel(database);
|
|
this.attempt = defineAttemptModel(database);
|
|
this.event = defineEventModel(database);
|
|
}
|
|
|
|
async findByRunId(runId: string): Promise<CancellationDispatchRecord | null> {
|
|
assertId('runId', runId);
|
|
const row = (await this.dispatch.findByPk(runId, {
|
|
raw: true,
|
|
})) as unknown as CancellationDispatchRow | null;
|
|
return row === null ? null : rowToDispatch(row);
|
|
}
|
|
|
|
async claim(
|
|
command: ClaimCancellationDispatchCommand,
|
|
): Promise<ClaimCancellationDispatchResult> {
|
|
assertClaim(command);
|
|
return this.database.transaction(
|
|
{ type: Transaction.TYPES.IMMEDIATE },
|
|
async (transaction) => {
|
|
const [run, attempt] = await Promise.all([
|
|
this.run.findByPk(command.runId, { raw: true, transaction }),
|
|
this.attempt.findByPk(command.attemptId, { raw: true, transaction }),
|
|
]);
|
|
const runRow = run as unknown as CancellationDispatchRunRow | null;
|
|
const attemptRow =
|
|
attempt as unknown as CancellationDispatchAttemptRow | null;
|
|
if (
|
|
runRow === null ||
|
|
attemptRow === null ||
|
|
runRow.executionOwner !== 'runtime' ||
|
|
!ACTIVE_RUN_STATUSES.includes(runRow.status as RunStatus) ||
|
|
runRow.cancelRequestedAtMs === null ||
|
|
Number(runRow.cancelRequestedAtMs) !== command.requestedAtMs ||
|
|
attemptRow.runId !== command.runId ||
|
|
!ACTIVE_ATTEMPT_STATUSES.includes(
|
|
attemptRow.status as (typeof ACTIVE_ATTEMPT_STATUSES)[number],
|
|
)
|
|
) {
|
|
return { status: 'not_eligible' as const };
|
|
}
|
|
|
|
let row = (await this.dispatch.findByPk(command.runId, {
|
|
raw: true,
|
|
transaction,
|
|
})) as unknown as CancellationDispatchRow | null;
|
|
if (row === null) {
|
|
try {
|
|
const created = await this.dispatch.create(
|
|
{
|
|
runId: command.runId,
|
|
attemptId: command.attemptId,
|
|
status: 'pending',
|
|
version: 0,
|
|
dispatchCount: 0,
|
|
nextAttemptAtMs: command.requestedAtMs,
|
|
leaseOwner: null,
|
|
leaseToken: null,
|
|
leaseExpiresAtMs: null,
|
|
lastResult: null,
|
|
lastDispatchedAtMs: null,
|
|
createdAtMs: command.nowMs,
|
|
updatedAtMs: command.nowMs,
|
|
},
|
|
{ transaction },
|
|
);
|
|
row = created.get({ plain: true }) as CancellationDispatchRow;
|
|
} catch (error) {
|
|
if (error instanceof UniqueConstraintError) {
|
|
throw new CancellationDispatchRepositoryError(error);
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
if (row.attemptId !== command.attemptId) {
|
|
throw new CancellationDispatchBindingConflictError(
|
|
command.runId,
|
|
command.attemptId,
|
|
);
|
|
}
|
|
const dispatch = rowToDispatch(row);
|
|
if (dispatch.status === 'dispatched' || dispatch.status === 'blocked') {
|
|
return { status: dispatch.status, dispatch };
|
|
}
|
|
if (
|
|
dispatch.status === 'leased' &&
|
|
dispatch.leaseExpiresAtMs !== undefined &&
|
|
dispatch.leaseExpiresAtMs > command.nowMs
|
|
) {
|
|
return { status: 'leased', dispatch };
|
|
}
|
|
if (
|
|
dispatch.status !== 'leased' &&
|
|
dispatch.nextAttemptAtMs !== undefined &&
|
|
dispatch.nextAttemptAtMs > command.nowMs
|
|
) {
|
|
return { status: 'not_due', dispatch };
|
|
}
|
|
|
|
const nextVersion = dispatch.version + 1;
|
|
const nextCount = dispatch.dispatchCount + 1;
|
|
const leaseExpiresAtMs = command.nowMs + command.leaseDurationMs;
|
|
const [affected] = await this.dispatch.update(
|
|
{
|
|
status: 'leased',
|
|
version: nextVersion,
|
|
dispatchCount: nextCount,
|
|
nextAttemptAtMs: null,
|
|
leaseOwner: command.owner,
|
|
leaseToken: command.leaseToken,
|
|
leaseExpiresAtMs,
|
|
updatedAtMs: command.nowMs,
|
|
},
|
|
{
|
|
where: { runId: command.runId, version: dispatch.version },
|
|
transaction,
|
|
},
|
|
);
|
|
if (affected !== 1) {
|
|
throw new CancellationDispatchFenceRejectedError(command.runId);
|
|
}
|
|
return {
|
|
status: 'claimed',
|
|
dispatch: {
|
|
...withoutScheduleAndLease(dispatch),
|
|
status: 'leased',
|
|
version: nextVersion,
|
|
dispatchCount: nextCount,
|
|
leaseOwner: command.owner,
|
|
leaseToken: command.leaseToken,
|
|
leaseExpiresAtMs,
|
|
updatedAtMs: command.nowMs,
|
|
},
|
|
};
|
|
},
|
|
);
|
|
}
|
|
|
|
async recordResult(
|
|
command: RecordCancellationDispatchResultCommand,
|
|
): Promise<RecordCancellationDispatchResult> {
|
|
assertRecordResult(command);
|
|
return this.database.transaction(
|
|
{ type: Transaction.TYPES.IMMEDIATE },
|
|
async (transaction) => {
|
|
const row = (await this.dispatch.findByPk(command.runId, {
|
|
raw: true,
|
|
transaction,
|
|
})) as unknown as CancellationDispatchRow | null;
|
|
if (
|
|
row === null ||
|
|
row.attemptId !== command.attemptId ||
|
|
row.status !== 'leased' ||
|
|
row.version !== command.expectedVersion ||
|
|
row.leaseOwner !== command.owner ||
|
|
row.leaseToken !== command.leaseToken
|
|
) {
|
|
throw new CancellationDispatchFenceRejectedError(command.runId);
|
|
}
|
|
const run = (await this.run.findByPk(command.runId, {
|
|
raw: true,
|
|
transaction,
|
|
})) as unknown as CancellationDispatchRunRow | null;
|
|
if (run === null) {
|
|
throw new CancellationDispatchRepositoryError(
|
|
new Error('Run disappeared while recording cancellation result'),
|
|
);
|
|
}
|
|
const state = resultState(command.result);
|
|
const controllerInvoked = ![
|
|
'controller_missing',
|
|
'handle_missing',
|
|
].includes(command.result);
|
|
const nextVersion = row.version + 1;
|
|
const nextSequence = Number(run.eventSequence) + 1;
|
|
const [runAffected] = await this.run.update(
|
|
{ version: Number(run.version) + 1, eventSequence: nextSequence },
|
|
{ where: { id: command.runId, version: run.version }, transaction },
|
|
);
|
|
if (runAffected !== 1) {
|
|
throw new CancellationDispatchFenceRejectedError(command.runId);
|
|
}
|
|
const [dispatchAffected] = await this.dispatch.update(
|
|
{
|
|
status: state.status,
|
|
version: nextVersion,
|
|
nextAttemptAtMs: command.nextAttemptAtMs ?? null,
|
|
leaseOwner: null,
|
|
leaseToken: null,
|
|
leaseExpiresAtMs: null,
|
|
lastResult: command.result,
|
|
lastDispatchedAtMs: controllerInvoked
|
|
? command.atMs
|
|
: row.lastDispatchedAtMs,
|
|
updatedAtMs: command.atMs,
|
|
},
|
|
{
|
|
where: {
|
|
runId: command.runId,
|
|
attemptId: command.attemptId,
|
|
status: 'leased',
|
|
version: command.expectedVersion,
|
|
leaseOwner: command.owner,
|
|
leaseToken: command.leaseToken,
|
|
},
|
|
transaction,
|
|
},
|
|
);
|
|
if (dispatchAffected !== 1) {
|
|
throw new CancellationDispatchFenceRejectedError(command.runId);
|
|
}
|
|
const event: RunEventRecord = {
|
|
id: command.eventId,
|
|
runId: command.runId,
|
|
sequence: nextSequence,
|
|
type: state.eventType,
|
|
dedupeKey: `cancel-dispatch:${command.attemptId}:${row.dispatchCount}`,
|
|
actorType: 'worker',
|
|
actorId: command.owner,
|
|
attemptId: command.attemptId,
|
|
payload: {
|
|
attempt_id: command.attemptId,
|
|
dispatch_count: row.dispatchCount,
|
|
result: command.result,
|
|
},
|
|
createdAtMs: command.atMs,
|
|
};
|
|
await this.event.create(
|
|
{
|
|
id: event.id,
|
|
runId: event.runId,
|
|
sequence: event.sequence,
|
|
type: event.type,
|
|
dedupeKey: event.dedupeKey!,
|
|
actorType: event.actorType,
|
|
actorId: event.actorId!,
|
|
attemptId: event.attemptId!,
|
|
payload: event.payload,
|
|
createdAtMs: event.createdAtMs,
|
|
},
|
|
{ transaction },
|
|
);
|
|
return {
|
|
dispatch: {
|
|
...withoutScheduleAndLease(rowToDispatch(row)),
|
|
status: state.status,
|
|
version: nextVersion,
|
|
...(command.nextAttemptAtMs === undefined
|
|
? {}
|
|
: { nextAttemptAtMs: command.nextAttemptAtMs }),
|
|
lastResult: command.result,
|
|
...(controllerInvoked
|
|
? { lastDispatchedAtMs: command.atMs }
|
|
: row.lastDispatchedAtMs === null
|
|
? {}
|
|
: { lastDispatchedAtMs: Number(row.lastDispatchedAtMs) }),
|
|
updatedAtMs: command.atMs,
|
|
},
|
|
event,
|
|
};
|
|
},
|
|
);
|
|
}
|
|
}
|