mirror of
https://github.com/whyour/qinglong.git
synced 2026-09-22 19:29:13 +08:00
feat(ql3): observe system crond runs idempotently
This commit is contained in:
@@ -22,6 +22,7 @@ const SHADOW_ORIGINS_ENV = 'QL3_SHADOW_ORIGINS';
|
||||
const SUPPORTED_SHADOW_ORIGINS = new Set<ExecutionOrigin>([
|
||||
'manual',
|
||||
'scheduled_node',
|
||||
'scheduled_system',
|
||||
'script',
|
||||
'subscription',
|
||||
'system',
|
||||
@@ -84,7 +85,7 @@ function readConfiguredOrigins(): ReadonlySet<ExecutionOrigin> {
|
||||
incrementFailure('configuration:unsupported_origin');
|
||||
try {
|
||||
Logger.warn(
|
||||
'[ql3-shadow] ignored unsupported origin; this slice supports manual,scheduled_node,script,subscription,system',
|
||||
'[ql3-shadow] ignored unsupported origin; this slice supports manual,scheduled_node,scheduled_system,script,subscription,system',
|
||||
);
|
||||
} catch {
|
||||
// Invalid compatibility configuration must not affect legacy paths.
|
||||
@@ -269,6 +270,57 @@ export function observeLegacyExecution(
|
||||
: NOOP_OBSERVATION;
|
||||
}
|
||||
|
||||
export interface LegacyShellExecutionCallbackInput {
|
||||
pid?: number;
|
||||
logPath?: string;
|
||||
atMs: number;
|
||||
phase: 'running' | 'finished';
|
||||
exitCode?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes one callback-owned execution without registering an in-memory
|
||||
* ChildProcess correlation entry. The accepted fact must carry a durable
|
||||
* requestId so callback replay can converge in the Shadow writer.
|
||||
*/
|
||||
export function observeLegacyShellExecutionCallback(
|
||||
createFact: LegacyExecutionAcceptedFactFactory,
|
||||
input: LegacyShellExecutionCallbackInput,
|
||||
): LegacyExecutionObservation | undefined {
|
||||
const origin: ExecutionOrigin = 'scheduled_system';
|
||||
let observation: LegacyExecutionObservation;
|
||||
if (override) {
|
||||
if (!override.origins.has(origin)) return undefined;
|
||||
const fact = createAcceptedFactFailOpen(origin, createFact);
|
||||
observation = fact
|
||||
? beginFailOpen(override.observer, fact)
|
||||
: NOOP_OBSERVATION;
|
||||
} else {
|
||||
if (!readConfiguredOrigins().has(origin)) return undefined;
|
||||
const fact = createAcceptedFactFailOpen(origin, createFact);
|
||||
observation = fact
|
||||
? deferredObservation(getDefaultObserver(), fact)
|
||||
: NOOP_OBSERVATION;
|
||||
}
|
||||
|
||||
observation.spawned({
|
||||
atMs: input.atMs,
|
||||
...(input.pid === undefined ? {} : { pid: input.pid }),
|
||||
...(input.logPath === undefined
|
||||
? {}
|
||||
: { logArtifactId: createLegacyLogArtifactId(input.logPath) }),
|
||||
});
|
||||
if (input.phase === 'running') {
|
||||
observation.running({ atMs: input.atMs });
|
||||
} else {
|
||||
observation.exited({
|
||||
atMs: input.atMs,
|
||||
exitCode: input.exitCode ?? 0,
|
||||
});
|
||||
}
|
||||
return observation;
|
||||
}
|
||||
|
||||
export interface LegacyExecutionCancellationInput {
|
||||
legacyCronId: number;
|
||||
pid?: number;
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import type { Crontab } from '../../data/cron';
|
||||
import type { LegacyExecutionObservation } from '../ports/legacyExecutionObserver';
|
||||
import { observeLegacyShellExecutionCallback } from './legacyExecutionBridge';
|
||||
import { createLegacyTaskRevision } from './legacyTaskRevision';
|
||||
|
||||
const EXECUTION_ID_PATTERN =
|
||||
/^legacy-system:([1-9][0-9]{0,12}):([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12})$/u;
|
||||
|
||||
export interface LegacyScheduledSystemCallbackInput {
|
||||
executionId: string;
|
||||
phase: 'running' | 'finished';
|
||||
observedAtMs: number;
|
||||
pid?: number;
|
||||
logPath?: string;
|
||||
exitCode?: number;
|
||||
}
|
||||
|
||||
export function decorateScheduledSystemCronCommand(
|
||||
command: string,
|
||||
systemScheduler: boolean,
|
||||
): string {
|
||||
return systemScheduler
|
||||
? `QL_EXECUTION_ORIGIN=scheduled_system ${command}`
|
||||
: command;
|
||||
}
|
||||
|
||||
export function parseLegacyScheduledSystemExecutionId(
|
||||
value: string,
|
||||
): { requestId: string; acceptedAtMs: number } | undefined {
|
||||
const match = EXECUTION_ID_PATTERN.exec(value);
|
||||
if (!match) return undefined;
|
||||
const acceptedAtMs = Number(match[1]) * 1000;
|
||||
if (!Number.isSafeInteger(acceptedAtMs) || acceptedAtMs < 1) return undefined;
|
||||
return Object.freeze({ requestId: value, acceptedAtMs });
|
||||
}
|
||||
|
||||
export function observeLegacyScheduledSystemExecution(
|
||||
cron: Crontab,
|
||||
input: LegacyScheduledSystemCallbackInput,
|
||||
): LegacyExecutionObservation | undefined {
|
||||
const identity = parseLegacyScheduledSystemExecutionId(input.executionId);
|
||||
if (
|
||||
!identity ||
|
||||
cron.id === undefined ||
|
||||
!Number.isSafeInteger(input.observedAtMs) ||
|
||||
input.observedAtMs < identity.acceptedAtMs
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
return observeLegacyShellExecutionCallback(
|
||||
() => ({
|
||||
origin: 'scheduled_system',
|
||||
projectId: 'default',
|
||||
taskId: `legacy-cron:${cron.id}`,
|
||||
taskRevision: createLegacyTaskRevision({
|
||||
command: cron.command,
|
||||
...(cron.schedule === undefined ? {} : { schedule: cron.schedule }),
|
||||
extraSchedules:
|
||||
cron.extra_schedules?.map((item) => item.schedule) ?? [],
|
||||
...(cron.task_before === undefined
|
||||
? {}
|
||||
: { taskBefore: cron.task_before }),
|
||||
...(cron.task_after === undefined
|
||||
? {}
|
||||
: { taskAfter: cron.task_after }),
|
||||
...(cron.work_dir === undefined
|
||||
? {}
|
||||
: { workDirectory: cron.work_dir }),
|
||||
...(cron.log_name === undefined ? {} : { logName: cron.log_name }),
|
||||
}),
|
||||
...(cron.name === undefined ? {} : { taskName: cron.name }),
|
||||
legacyCronId: cron.id,
|
||||
triggerType: 'scheduled_system',
|
||||
triggeredBy: 'legacy:system-crond',
|
||||
requestId: identity.requestId,
|
||||
scheduledForMs: identity.acceptedAtMs,
|
||||
acceptedAtMs: identity.acceptedAtMs,
|
||||
}),
|
||||
{
|
||||
phase: input.phase,
|
||||
atMs: input.observedAtMs,
|
||||
...(input.pid === undefined ? {} : { pid: input.pid }),
|
||||
...(input.logPath === undefined ? {} : { logPath: input.logPath }),
|
||||
...(input.exitCode === undefined ? {} : { exitCode: input.exitCode }),
|
||||
},
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user