mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
211 lines
5.9 KiB
TypeScript
211 lines
5.9 KiB
TypeScript
import { assertWorkerId, assertWorkerSessionId } from './worker';
|
|
|
|
export const RUN_DISPATCH_LEASE_STATUSES = [
|
|
'leased',
|
|
'released',
|
|
'completed',
|
|
] as const;
|
|
|
|
export type RunDispatchLeaseStatus =
|
|
(typeof RUN_DISPATCH_LEASE_STATUSES)[number];
|
|
|
|
export const RUN_DISPATCH_RELEASE_REASONS = [
|
|
'declined',
|
|
'shutdown',
|
|
'start_failed',
|
|
'capacity_changed',
|
|
'lease_expired',
|
|
] as const;
|
|
|
|
export type RunDispatchReleaseReason =
|
|
(typeof RUN_DISPATCH_RELEASE_REASONS)[number];
|
|
|
|
export const MIN_RUN_DISPATCH_LEASE_DURATION_MS = 5_000;
|
|
export const MAX_RUN_DISPATCH_LEASE_DURATION_MS = 10 * 60_000;
|
|
|
|
export interface RunDispatchLeaseRecord {
|
|
attemptId: string;
|
|
runId: string;
|
|
status: RunDispatchLeaseStatus;
|
|
version: number;
|
|
leaseGeneration: number;
|
|
workerId: string;
|
|
workerSessionId: string;
|
|
workerGeneration: number;
|
|
leaseToken: string;
|
|
acquiredAtMs: number;
|
|
renewedAtMs: number;
|
|
expiresAtMs: number;
|
|
releasedAtMs?: number;
|
|
releaseReason?: RunDispatchReleaseReason;
|
|
completedAtMs?: number;
|
|
updatedAtMs: number;
|
|
}
|
|
|
|
export type RunDispatchLeaseFenceReason =
|
|
| 'missing'
|
|
| 'run_mismatch'
|
|
| 'not_leased'
|
|
| 'worker_mismatch'
|
|
| 'worker_session_mismatch'
|
|
| 'worker_generation_mismatch'
|
|
| 'lease_generation_mismatch'
|
|
| 'lease_token_mismatch'
|
|
| 'version_mismatch'
|
|
| 'lease_expired'
|
|
| 'worker_unavailable';
|
|
|
|
export class InvalidRunDispatchLeaseValueError extends TypeError {
|
|
constructor(message: string) {
|
|
super(`Run dispatch lease value is invalid: ${message}`);
|
|
this.name = 'InvalidRunDispatchLeaseValueError';
|
|
}
|
|
}
|
|
|
|
export class RunDispatchLeaseFenceRejectedError extends Error {
|
|
constructor(
|
|
readonly attemptId: string,
|
|
readonly reason: RunDispatchLeaseFenceReason,
|
|
) {
|
|
super(`Run dispatch lease for Attempt ${attemptId} was fenced: ${reason}`);
|
|
this.name = 'RunDispatchLeaseFenceRejectedError';
|
|
}
|
|
}
|
|
|
|
export function assertRunDispatchId(name: string, value: string): void {
|
|
if (
|
|
typeof value !== 'string' ||
|
|
value.length < 1 ||
|
|
value.length > 128 ||
|
|
/[\u0000-\u001f\u007f]/.test(value)
|
|
) {
|
|
throw new InvalidRunDispatchLeaseValueError(`${name} is invalid`);
|
|
}
|
|
}
|
|
|
|
export function assertRunDispatchLeaseToken(value: string): void {
|
|
if (
|
|
typeof value !== 'string' ||
|
|
value.length < 32 ||
|
|
value.length > 128 ||
|
|
!/^[A-Za-z0-9_-]+$/.test(value)
|
|
) {
|
|
throw new InvalidRunDispatchLeaseValueError('leaseToken is invalid');
|
|
}
|
|
}
|
|
|
|
export function assertRunDispatchLeaseVersion(
|
|
name: string,
|
|
value: number,
|
|
positive = false,
|
|
): void {
|
|
if (!Number.isSafeInteger(value) || value < (positive ? 1 : 0)) {
|
|
throw new InvalidRunDispatchLeaseValueError(
|
|
`${name} must be a ${
|
|
positive ? 'positive' : 'non-negative'
|
|
} safe integer`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function assertRunDispatchWorkerFence(value: {
|
|
workerId: string;
|
|
workerSessionId: string;
|
|
workerGeneration: number;
|
|
}): void {
|
|
assertWorkerId(value.workerId);
|
|
assertWorkerSessionId(value.workerSessionId);
|
|
assertRunDispatchLeaseVersion(
|
|
'workerGeneration',
|
|
value.workerGeneration,
|
|
true,
|
|
);
|
|
}
|
|
|
|
export function assertRunDispatchLeaseDuration(value: number): void {
|
|
if (
|
|
!Number.isSafeInteger(value) ||
|
|
value < MIN_RUN_DISPATCH_LEASE_DURATION_MS ||
|
|
value > MAX_RUN_DISPATCH_LEASE_DURATION_MS
|
|
) {
|
|
throw new InvalidRunDispatchLeaseValueError(
|
|
`leaseDurationMs must be between ${MIN_RUN_DISPATCH_LEASE_DURATION_MS} and ${MAX_RUN_DISPATCH_LEASE_DURATION_MS}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export function assertRunDispatchLeaseRecord(
|
|
value: RunDispatchLeaseRecord,
|
|
): void {
|
|
assertRunDispatchId('attemptId', value.attemptId);
|
|
assertRunDispatchId('runId', value.runId);
|
|
if (!RUN_DISPATCH_LEASE_STATUSES.includes(value.status)) {
|
|
throw new InvalidRunDispatchLeaseValueError('status is invalid');
|
|
}
|
|
assertRunDispatchLeaseVersion('version', value.version);
|
|
assertRunDispatchLeaseVersion('leaseGeneration', value.leaseGeneration, true);
|
|
assertRunDispatchWorkerFence(value);
|
|
assertRunDispatchLeaseToken(value.leaseToken);
|
|
assertRunDispatchLeaseVersion('acquiredAtMs', value.acquiredAtMs);
|
|
assertRunDispatchLeaseVersion('renewedAtMs', value.renewedAtMs);
|
|
assertRunDispatchLeaseVersion('expiresAtMs', value.expiresAtMs);
|
|
assertRunDispatchLeaseVersion('updatedAtMs', value.updatedAtMs);
|
|
if (
|
|
value.renewedAtMs < value.acquiredAtMs ||
|
|
value.expiresAtMs <= value.renewedAtMs ||
|
|
value.updatedAtMs < value.acquiredAtMs
|
|
) {
|
|
throw new InvalidRunDispatchLeaseValueError('timestamps are inconsistent');
|
|
}
|
|
if (value.status === 'leased') {
|
|
if (
|
|
value.releasedAtMs !== undefined ||
|
|
value.releaseReason !== undefined ||
|
|
value.completedAtMs !== undefined
|
|
) {
|
|
throw new InvalidRunDispatchLeaseValueError(
|
|
'active lease has terminal metadata',
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
if (value.status === 'released') {
|
|
if (
|
|
value.releasedAtMs === undefined ||
|
|
value.completedAtMs !== undefined ||
|
|
value.releaseReason === undefined ||
|
|
!RUN_DISPATCH_RELEASE_REASONS.includes(value.releaseReason)
|
|
) {
|
|
throw new InvalidRunDispatchLeaseValueError(
|
|
'released lease metadata is inconsistent',
|
|
);
|
|
}
|
|
assertRunDispatchLeaseVersion('releasedAtMs', value.releasedAtMs);
|
|
return;
|
|
}
|
|
if (
|
|
value.completedAtMs === undefined ||
|
|
value.releasedAtMs !== undefined ||
|
|
value.releaseReason !== undefined
|
|
) {
|
|
throw new InvalidRunDispatchLeaseValueError(
|
|
'completed lease metadata is inconsistent',
|
|
);
|
|
}
|
|
assertRunDispatchLeaseVersion('completedAtMs', value.completedAtMs);
|
|
}
|
|
|
|
export function runDispatchLeaseExpiration(
|
|
nowMs: number,
|
|
durationMs: number,
|
|
): number {
|
|
assertRunDispatchLeaseVersion('nowMs', nowMs);
|
|
assertRunDispatchLeaseDuration(durationMs);
|
|
if (nowMs > Number.MAX_SAFE_INTEGER - durationMs) {
|
|
throw new InvalidRunDispatchLeaseValueError(
|
|
'lease expiration exceeds the safe integer range',
|
|
);
|
|
}
|
|
return nowMs + durationMs;
|
|
}
|