|
2 | 2 | import { resolveGlobalSingleton } from "../shared/global-singleton.js"; |
3 | 3 |
|
4 | 4 | type CronActiveJobState = { |
5 | | - activeJobRuns: Map<string, Map<string, ActiveCronJobRun>>; |
| 5 | + activeJobIds: Set<string>; |
6 | 6 | }; |
7 | 7 |
|
8 | 8 | const CRON_ACTIVE_JOB_STATE_KEY = Symbol.for("openclaw.cron.activeJobs"); |
9 | | -const DEFAULT_RUN_KEY = "__cron-job__"; |
10 | | - |
11 | | -type ActiveCronJobRun = { |
12 | | - runId?: string; |
13 | | - abortController?: AbortController; |
14 | | -}; |
15 | | - |
16 | | -type MarkCronJobActiveOptions = { |
17 | | - runId?: string; |
18 | | - abortController?: AbortController; |
19 | | -}; |
20 | | - |
21 | | -type CancelCronJobRunResult = |
22 | | - | { found: false; cancelled: false; reason: string } |
23 | | - | { found: true; cancelled: false; reason: string } |
24 | | - | { found: true; cancelled: true }; |
25 | 9 |
|
26 | 10 | function getCronActiveJobState(): CronActiveJobState { |
27 | 11 | // Cron runs can cross module reload boundaries in tests and dev watch; keep |
28 | | - // the in-flight job map process-global so duplicate-run guards share state. |
| 12 | + // the in-flight job set process-global so duplicate-run guards share state. |
29 | 13 | return resolveGlobalSingleton<CronActiveJobState>(CRON_ACTIVE_JOB_STATE_KEY, () => ({ |
30 | | - activeJobRuns: new Map<string, Map<string, ActiveCronJobRun>>(), |
| 14 | + activeJobIds: new Set<string>(), |
31 | 15 | })); |
32 | 16 | } |
33 | 17 |
|
34 | | -function normalizeRunKey(runId: string | undefined): string { |
35 | | - return runId?.trim() || DEFAULT_RUN_KEY; |
36 | | -} |
37 | | - |
38 | 18 | /** Marks a cron job id as currently executing for duplicate-run suppression. */ |
39 | | -export function markCronJobActive(jobId: string, opts?: MarkCronJobActiveOptions) { |
| 19 | +export function markCronJobActive(jobId: string) { |
40 | 20 | if (!jobId) { |
41 | 21 | return; |
42 | 22 | } |
43 | | - const state = getCronActiveJobState(); |
44 | | - const runs = state.activeJobRuns.get(jobId) ?? new Map<string, ActiveCronJobRun>(); |
45 | | - runs.set(normalizeRunKey(opts?.runId), { |
46 | | - ...(opts?.runId ? { runId: opts.runId } : {}), |
47 | | - ...(opts?.abortController ? { abortController: opts.abortController } : {}), |
48 | | - }); |
49 | | - state.activeJobRuns.set(jobId, runs); |
| 23 | + getCronActiveJobState().activeJobIds.add(jobId); |
50 | 24 | } |
51 | 25 |
|
52 | 26 | /** Clears the active marker when a cron run exits or is abandoned. */ |
53 | | -export function clearCronJobActive(jobId: string, runId?: string) { |
| 27 | +export function clearCronJobActive(jobId: string) { |
54 | 28 | if (!jobId) { |
55 | 29 | return; |
56 | 30 | } |
57 | | - const state = getCronActiveJobState(); |
58 | | - if (runId === undefined) { |
59 | | - state.activeJobRuns.delete(jobId); |
60 | | - return; |
61 | | - } |
62 | | - const runs = state.activeJobRuns.get(jobId); |
63 | | - if (!runs) { |
64 | | - return; |
65 | | - } |
66 | | - runs.delete(normalizeRunKey(runId)); |
67 | | - if (runs.size === 0) { |
68 | | - state.activeJobRuns.delete(jobId); |
69 | | - } |
| 31 | + getCronActiveJobState().activeJobIds.delete(jobId); |
70 | 32 | } |
71 | 33 |
|
72 | 34 | /** Returns whether the given cron job id is currently executing in this process. */ |
73 | 35 | export function isCronJobActive(jobId: string) { |
74 | 36 | if (!jobId) { |
75 | 37 | return false; |
76 | 38 | } |
77 | | - return (getCronActiveJobState().activeJobRuns.get(jobId)?.size ?? 0) > 0; |
| 39 | + return getCronActiveJobState().activeJobIds.has(jobId); |
78 | 40 | } |
79 | 41 |
|
80 | 42 | /** Returns whether any cron run is active in this process. */ |
81 | 43 | export function hasActiveCronJobs() { |
82 | | - return getCronActiveJobState().activeJobRuns.size > 0; |
83 | | -} |
84 | | - |
85 | | -/** Aborts an active cron run in the current process when one owns the task row. */ |
86 | | -export function cancelCronJobRun(params: { |
87 | | - jobId?: string; |
88 | | - runId?: string; |
89 | | - reason?: string; |
90 | | -}): CancelCronJobRunResult { |
91 | | - const jobId = params.jobId?.trim(); |
92 | | - if (!jobId) { |
93 | | - return { |
94 | | - found: false, |
95 | | - cancelled: false, |
96 | | - reason: "Cron task has no cancellable job id.", |
97 | | - }; |
98 | | - } |
99 | | - const runs = getCronActiveJobState().activeJobRuns.get(jobId); |
100 | | - if (!runs || runs.size === 0) { |
101 | | - return { |
102 | | - found: false, |
103 | | - cancelled: false, |
104 | | - reason: "Cron task is not active in this gateway process.", |
105 | | - }; |
106 | | - } |
107 | | - let run: ActiveCronJobRun | undefined; |
108 | | - if (params.runId) { |
109 | | - run = runs.get(normalizeRunKey(params.runId)); |
110 | | - } else { |
111 | | - const first = runs.values().next(); |
112 | | - run = first.done ? undefined : first.value; |
113 | | - } |
114 | | - if (!run) { |
115 | | - return { |
116 | | - found: false, |
117 | | - cancelled: false, |
118 | | - reason: "Cron task run is not active in this gateway process.", |
119 | | - }; |
120 | | - } |
121 | | - const controller = run.abortController; |
122 | | - if (!controller) { |
123 | | - return { |
124 | | - found: true, |
125 | | - cancelled: false, |
126 | | - reason: "Cron task has no active cancellation handle.", |
127 | | - }; |
128 | | - } |
129 | | - if (controller.signal.aborted) { |
130 | | - return { |
131 | | - found: true, |
132 | | - cancelled: false, |
133 | | - reason: "Cron task is already cancelling.", |
134 | | - }; |
135 | | - } |
136 | | - controller.abort(params.reason?.trim() || "Cancelled by operator."); |
137 | | - return { |
138 | | - found: true, |
139 | | - cancelled: true, |
140 | | - }; |
| 44 | + return getCronActiveJobState().activeJobIds.size > 0; |
141 | 45 | } |
142 | 46 |
|
143 | 47 | /** Clears process-global cron active-job state between tests. */ |
144 | 48 | export function resetCronActiveJobsForTests() { |
145 | | - getCronActiveJobState().activeJobRuns.clear(); |
| 49 | + getCronActiveJobState().activeJobIds.clear(); |
146 | 50 | } |
0 commit comments