mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-20 08:05:22 +08:00
119 lines
3.5 KiB
TypeScript
119 lines
3.5 KiB
TypeScript
import { QueryTypes, Sequelize } from 'sequelize';
|
|
import {
|
|
RUN_ATTEMPT_TABLE,
|
|
RUN_TABLE,
|
|
} from '../../../migrations/0002-run-schema';
|
|
import {
|
|
RUN_DISPATCH_LEASE_EXPIRY_INDEX,
|
|
RUN_DISPATCH_LEASE_TABLE,
|
|
} from '../../../migrations/0009-run-dispatch-lease';
|
|
import {
|
|
assertRunDispatchId,
|
|
assertRunDispatchLeaseVersion,
|
|
} from '../../domain/runDispatchLease';
|
|
import {
|
|
MAX_RUN_DISPATCH_LEASE_EXPIRY_PAGE_SIZE,
|
|
type ExpiredRunDispatchLeaseCandidate,
|
|
type ListExpiredRunDispatchLeasesOptions,
|
|
type RunDispatchLeaseExpirySource,
|
|
} from '../../ports/runDispatchLeaseExpirySource';
|
|
|
|
interface ExpiredLeaseRow {
|
|
runId: string;
|
|
attemptId: string;
|
|
expiresAtMs: number | string;
|
|
}
|
|
|
|
function assertLimit(limit: number): void {
|
|
if (
|
|
!Number.isSafeInteger(limit) ||
|
|
limit < 1 ||
|
|
limit > MAX_RUN_DISPATCH_LEASE_EXPIRY_PAGE_SIZE
|
|
) {
|
|
throw new RangeError(
|
|
`limit must be between 1 and ${MAX_RUN_DISPATCH_LEASE_EXPIRY_PAGE_SIZE}`,
|
|
);
|
|
}
|
|
}
|
|
|
|
export class LegacySequelizeRunDispatchLeaseExpirySource
|
|
implements RunDispatchLeaseExpirySource
|
|
{
|
|
constructor(private readonly database: Sequelize) {
|
|
if (database.getDialect() !== 'sqlite') {
|
|
throw new TypeError(
|
|
'Legacy Run dispatch lease expiry source is SQLite-only; cluster-control requires a PostgreSQL adapter',
|
|
);
|
|
}
|
|
}
|
|
|
|
async listExpired({
|
|
observedAtMs,
|
|
after,
|
|
limit = 16,
|
|
}: ListExpiredRunDispatchLeasesOptions): Promise<
|
|
readonly ExpiredRunDispatchLeaseCandidate[]
|
|
> {
|
|
assertRunDispatchLeaseVersion('observedAtMs', observedAtMs);
|
|
assertLimit(limit);
|
|
if (after) {
|
|
assertRunDispatchLeaseVersion('after.expiresAtMs', after.expiresAtMs);
|
|
assertRunDispatchId('after.attemptId', after.attemptId);
|
|
}
|
|
const cursorPredicate = after
|
|
? `AND (
|
|
l.expires_at_ms > :afterExpiresAtMs
|
|
OR (
|
|
l.expires_at_ms = :afterExpiresAtMs
|
|
AND l.attempt_id > :afterAttemptId
|
|
)
|
|
)`
|
|
: '';
|
|
const rows = await this.database.query<ExpiredLeaseRow>(
|
|
`SELECT
|
|
l.run_id AS runId,
|
|
l.attempt_id AS attemptId,
|
|
l.expires_at_ms AS expiresAtMs
|
|
FROM ${RUN_DISPATCH_LEASE_TABLE} l INDEXED BY ${RUN_DISPATCH_LEASE_EXPIRY_INDEX}
|
|
INNER JOIN ${RUN_TABLE} r ON r.id = l.run_id
|
|
INNER JOIN ${RUN_ATTEMPT_TABLE} a ON a.id = l.attempt_id
|
|
WHERE l.status = 'leased'
|
|
AND l.expires_at_ms <= :observedAtMs
|
|
AND r.execution_owner = 'runtime'
|
|
AND r.status IN ('dispatching', 'running')
|
|
AND a.run_id = r.id
|
|
AND a.status IN ('claimed', 'starting', 'running')
|
|
${cursorPredicate}
|
|
ORDER BY l.expires_at_ms ASC, l.attempt_id ASC
|
|
LIMIT :limit`,
|
|
{
|
|
type: QueryTypes.SELECT,
|
|
replacements: {
|
|
observedAtMs,
|
|
limit,
|
|
...(after
|
|
? {
|
|
afterExpiresAtMs: after.expiresAtMs,
|
|
afterAttemptId: after.attemptId,
|
|
}
|
|
: {}),
|
|
},
|
|
},
|
|
);
|
|
return rows.map((row) => {
|
|
const candidate = {
|
|
runId: row.runId,
|
|
attemptId: row.attemptId,
|
|
expiresAtMs: Number(row.expiresAtMs),
|
|
};
|
|
assertRunDispatchId('runId', candidate.runId);
|
|
assertRunDispatchId('attemptId', candidate.attemptId);
|
|
assertRunDispatchLeaseVersion('expiresAtMs', candidate.expiresAtMs);
|
|
if (candidate.expiresAtMs > observedAtMs) {
|
|
throw new TypeError('Expiry source returned a live Run lease');
|
|
}
|
|
return candidate;
|
|
});
|
|
}
|
|
}
|