mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
feat(ql3): establish 3.0 incubation baseline
This commit is contained in:
@@ -0,0 +1,130 @@
|
||||
import type { CompletionReceiptJournal } from '../ports/completionReceiptJournal';
|
||||
import type { CompletionReceiptStore } from '../ports/completionReceiptStore';
|
||||
import type { PrimaryRunRecoveryCursor } from '../ports/primaryRunRecoverySource';
|
||||
import { isTerminalRunAttemptStatus } from '../domain/runStateMachine';
|
||||
import type { PrimaryCompletionReceiptConsumer } from './primaryCompletionReceiptConsumer';
|
||||
import type { PrimaryCompletionReceiptScanSummary } from './primaryCompletionReceiptScanner';
|
||||
|
||||
export interface PrimaryCompletionReceiptJournalScannerOptions {
|
||||
terminalMissingRetentionMs?: number;
|
||||
clock?: { now(): number };
|
||||
}
|
||||
|
||||
/**
|
||||
* Database-indexed receipt retention. The supervisor cursor is only a transport
|
||||
* shape here: createdAtMs carries journal.updatedAtMs and runId carries Attempt
|
||||
* id. No directory enumeration is performed.
|
||||
*/
|
||||
export class PrimaryCompletionReceiptJournalScanner {
|
||||
private readonly terminalMissingRetentionMs: number;
|
||||
private readonly clock: { now(): number };
|
||||
|
||||
constructor(
|
||||
private readonly journal: CompletionReceiptJournal,
|
||||
private readonly store: CompletionReceiptStore,
|
||||
private readonly consumer: Pick<
|
||||
PrimaryCompletionReceiptConsumer,
|
||||
'consume'
|
||||
>,
|
||||
options: PrimaryCompletionReceiptJournalScannerOptions = {},
|
||||
) {
|
||||
this.terminalMissingRetentionMs =
|
||||
options.terminalMissingRetentionMs ?? 60_000;
|
||||
this.clock = options.clock ?? { now: Date.now };
|
||||
if (
|
||||
!Number.isSafeInteger(this.terminalMissingRetentionMs) ||
|
||||
this.terminalMissingRetentionMs < 0 ||
|
||||
this.terminalMissingRetentionMs > 24 * 60 * 60_000
|
||||
) {
|
||||
throw new RangeError(
|
||||
'terminalMissingRetentionMs must be between 0 and 24 hours',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async scanBatch(
|
||||
options: { cursor?: PrimaryRunRecoveryCursor; limit?: number } = {},
|
||||
): Promise<PrimaryCompletionReceiptScanSummary> {
|
||||
const observedAtMs = this.clock.now();
|
||||
if (!Number.isSafeInteger(observedAtMs) || observedAtMs < 0) {
|
||||
throw new RangeError('Completion receipt observation time is invalid');
|
||||
}
|
||||
const page = await this.journal.listCandidates({
|
||||
observedAtMs,
|
||||
...(options.limit === undefined ? {} : { limit: options.limit }),
|
||||
...(options.cursor === undefined
|
||||
? {}
|
||||
: {
|
||||
cursor: {
|
||||
updatedAtMs: options.cursor.createdAtMs,
|
||||
attemptId: options.cursor.runId,
|
||||
},
|
||||
}),
|
||||
});
|
||||
const summary: PrimaryCompletionReceiptScanSummary = {
|
||||
scanned: page.candidates.length,
|
||||
applied: 0,
|
||||
alreadyTerminal: 0,
|
||||
quarantined: 0,
|
||||
purgedQuarantines: 0,
|
||||
expiredMissing: 0,
|
||||
missing: 0,
|
||||
cleanupPending: 0,
|
||||
skipped: 0,
|
||||
ambiguous: 0,
|
||||
failed: 0,
|
||||
truncated: page.truncated,
|
||||
unsafeAttemptOverflow: false,
|
||||
...(page.nextCursor === undefined
|
||||
? {}
|
||||
: {
|
||||
nextCursor: {
|
||||
createdAtMs: page.nextCursor.updatedAtMs,
|
||||
runId: page.nextCursor.attemptId,
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
for (const candidate of page.candidates) {
|
||||
if (candidate.executorType !== 'local_process') {
|
||||
summary.skipped += 1;
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
if (candidate.state === 'quarantined') {
|
||||
await this.store.quarantine(candidate.attemptId);
|
||||
await this.store.purgeQuarantine(candidate.attemptId);
|
||||
await this.journal.resolve(candidate.attemptId);
|
||||
summary.purgedQuarantines += 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
const result = await this.consumer.consume(candidate.attemptId);
|
||||
if (result.status === 'quarantined') {
|
||||
summary.quarantined += 1;
|
||||
continue;
|
||||
}
|
||||
if (result.status === 'missing') {
|
||||
if (
|
||||
isTerminalRunAttemptStatus(candidate.attemptStatus) &&
|
||||
candidate.finishedAtMs !== undefined &&
|
||||
candidate.finishedAtMs + this.terminalMissingRetentionMs <=
|
||||
observedAtMs
|
||||
) {
|
||||
await this.journal.resolve(candidate.attemptId);
|
||||
summary.expiredMissing += 1;
|
||||
} else {
|
||||
summary.missing += 1;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (result.status === 'applied') summary.applied += 1;
|
||||
else summary.alreadyTerminal += 1;
|
||||
if (!result.cleaned) summary.cleanupPending += 1;
|
||||
} catch {
|
||||
summary.failed += 1;
|
||||
}
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user