mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-21 01:32:44 +08:00
128 lines
3.5 KiB
TypeScript
128 lines
3.5 KiB
TypeScript
import type { ExecutionOutcome } from '../domain/execution';
|
|
|
|
export interface ManualPrimaryCronSnapshot {
|
|
id: number;
|
|
name?: string;
|
|
command: string;
|
|
schedule?: string;
|
|
extraSchedules: readonly string[];
|
|
taskBefore?: string;
|
|
taskAfter?: string;
|
|
workDirectory?: string;
|
|
logName?: string;
|
|
}
|
|
|
|
export interface ManualPrimaryStartInput {
|
|
cron: ManualPrimaryCronSnapshot;
|
|
acceptedAtMs: number;
|
|
}
|
|
|
|
export interface ManualPrimaryCompletion {
|
|
runId: string;
|
|
attemptId: string;
|
|
outcome: ExecutionOutcome;
|
|
exitCode?: number;
|
|
}
|
|
|
|
export interface ManualPrimaryStartedExecution {
|
|
runId: string;
|
|
attemptId: string;
|
|
pid?: number;
|
|
logPath: string;
|
|
completion: Promise<ManualPrimaryCompletion>;
|
|
}
|
|
|
|
export interface ManualPrimaryStopResult {
|
|
matched: number;
|
|
failed: number;
|
|
}
|
|
|
|
export interface ManualPrimaryExecutionRouter {
|
|
/** Immutable selection for new manual triggers; off/shadow return false. */
|
|
ownsNewRuns(): boolean;
|
|
start(input: ManualPrimaryStartInput): Promise<ManualPrimaryStartedExecution>;
|
|
stopCron(
|
|
cronId: number,
|
|
requestedAtMs: number,
|
|
): Promise<ManualPrimaryStopResult>;
|
|
stopAttempt(
|
|
attemptId: string,
|
|
requestedAtMs: number,
|
|
): Promise<ManualPrimaryStopResult>;
|
|
}
|
|
|
|
let triggerRouter: ManualPrimaryExecutionRouter | undefined;
|
|
const ownerRouters = new Map<ManualPrimaryExecutionRouter, number>();
|
|
|
|
/**
|
|
* Returns the selected owner object so a concurrent config change cannot switch
|
|
* owner between the decision and start calls.
|
|
*/
|
|
export function selectManualPrimaryExecutionRouter():
|
|
| ManualPrimaryExecutionRouter
|
|
| undefined {
|
|
const selected = triggerRouter;
|
|
if (!selected) return undefined;
|
|
try {
|
|
return selected.ownsNewRuns() ? selected : undefined;
|
|
} catch {
|
|
// Invalid/unavailable configuration is off before Runtime accepts a Run.
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
export async function stopManualPrimaryCron(
|
|
cronId: number,
|
|
requestedAtMs: number,
|
|
): Promise<ManualPrimaryStopResult> {
|
|
return stopAcrossOwners((owner) => owner.stopCron(cronId, requestedAtMs));
|
|
}
|
|
|
|
export async function stopManualPrimaryAttempt(
|
|
attemptId: string,
|
|
requestedAtMs: number,
|
|
): Promise<ManualPrimaryStopResult> {
|
|
return stopAcrossOwners((owner) =>
|
|
owner.stopAttempt(attemptId, requestedAtMs),
|
|
);
|
|
}
|
|
|
|
async function stopAcrossOwners(
|
|
stop: (
|
|
router: ManualPrimaryExecutionRouter,
|
|
) => Promise<ManualPrimaryStopResult>,
|
|
): Promise<ManualPrimaryStopResult> {
|
|
const results = await Promise.allSettled([...ownerRouters.keys()].map(stop));
|
|
let matched = 0;
|
|
let failed = 0;
|
|
for (const result of results) {
|
|
if (result.status === 'fulfilled') {
|
|
matched += result.value.matched;
|
|
failed += result.value.failed;
|
|
} else {
|
|
// Conservatively protect a possibly-owned execution from PID fallback.
|
|
matched += 1;
|
|
failed += 1;
|
|
}
|
|
}
|
|
return { matched, failed };
|
|
}
|
|
|
|
/**
|
|
* Installs one manifest-gated owner. Default boot has no router unless the
|
|
* lightweight HTTP bootstrap accepts an explicit manual Primary manifest.
|
|
*/
|
|
export function installManualPrimaryExecutionRouter(
|
|
next: ManualPrimaryExecutionRouter,
|
|
): () => void {
|
|
const previous = triggerRouter;
|
|
triggerRouter = next;
|
|
ownerRouters.set(next, (ownerRouters.get(next) ?? 0) + 1);
|
|
return () => {
|
|
if (triggerRouter === next) triggerRouter = previous;
|
|
const references = ownerRouters.get(next);
|
|
if (references === 1) ownerRouters.delete(next);
|
|
else if (references !== undefined) ownerRouters.set(next, references - 1);
|
|
};
|
|
}
|