nx_cron.nx source
↩ module page · 465 lines · 21844 B
1// nx_cron.nx -- JOB-REGISTRY + DISPATCH-LOOP layer above the FULLY_WIRED
2// nx_race_timing.nx primitive (sealed 8-mode timing taxonomy). Replaces
3// /etc/crontab on Synology DSM.
4//
5// AUDIT-FIRST CORRECTION (2026-05-20, post-creation): user caught that I
6// built this without checking for existing prior art. nx_race_timing.nx
7// already ships the canonical sealed enum of 8 timing modes (MANUAL / EVENT /
8// SCHEDULED / CONTINUOUS / IDLE / THRESHOLD / PROBABILISTIC / QUORUM) + the
9// classifier helpers (is_reactive / needs_config / parse). My nx_cron 9-kind
10// enum maps directly:
11// NX_CRON_KIND_INTERVAL_SECONDS/MINUTES/HOURS -> NX_RACE_TIMING_CONTINUOUS
12// NX_CRON_KIND_DAILY_AT_HOUR/WEEKLY_AT_DOW/MONTHLY_AT_DOM -> NX_RACE_TIMING_SCHEDULED
13// NX_CRON_KIND_ON_EVENT -> NX_RACE_TIMING_EVENT
14// NX_CRON_KIND_ON_DEMAND -> NX_RACE_TIMING_MANUAL
15// NX_CRON_KIND_IDLE_OPPORTUNISTIC -> NX_RACE_TIMING_IDLE
16//
17// nx_cron does NOT replace nx_race_timing -- it COMPOSES against it.
18// nx_race_timing classifies a timing mode; nx_cron registers + dispatches
19// JOBS attached to those timing modes. Different layer:
20// - nx_race_timing : WHAT timing mode is this? (taxonomy + classifier)
21// - nx_cron : HOW do we run job X on schedule Y? (registry + loop)
22//
23// When graduated, nx_cron_register_job will call into nx_race_timing helpers
24// (nx_race_timing_is_valid, nx_race_timing_needs_config) to validate the
25// declared schedule before persisting the job. Also composes with the
26// already-shipped nx_timerfd.nx for the actual fire-time wakeup.
27//
28// Per feedback-no-temporary-third-party-scaffolding-when-substrate-arc-exists
29// audit-first rule: ls nxc2/runtime/ before authoring new primitives.
30//
31//
32// module: nishi-core.ops.cron
33// depends: nishi-core.race.timing + nishi-core.io.syscalls
34// disk_kb: 5
35// capability: OPS
36// wired_status: PARTIAL_WIRED
37//
38// WIRED (2026-06-13, W-RE-CRON-001 -- the in-memory DISPATCH CORE, gate-proven
39// by nx_cms_schedule_gate -> a real schedule->publish):
40// - JOB_REGISTRY (static in-memory job table; register/unregister/find
41// by name-hash; duplicate-name + bad-schedule REFUSED)
42// - SCHEDULE_DISPATCH_LOOP (nx_cron_tick(now) fires every enabled job whose
43// absolute fire_at <= now; one-shot kinds disable after
44// firing = FIRE-ONCE; interval kinds reschedule by period)
45// - SCHEDULE_VALIDATION (composes nx_race_timing_is_valid + needs_config: the
46// cron kind maps to a sealed race-timing mode, then the
47// required config -- interval>0 / fire_at>0 -- is checked)
48// - ACTION_DISPATCH (each fire pushes (action_kind, action_arg) onto a
49// fired-queue the consumer drains via nx_cron_drain_fired;
50// NX_CRON_ACTION_PUBLISH drives the CMS publish transition)
51// - MISSED_JOB_RECOVERY catch-up-once: a one-shot whose fire_at is already in
52// the past fires exactly once on the next tick.
53//
54// MISSING_CAPABILITIES (durability layer -- NOT faked here; no-floating, these
55// rest on nx_supervisor/nx_provenance_curve_store which are PARTIAL_WIRED):
56// - JOB_PERSISTENCE (job table serialized to a content-addressed manifest so
57// the crontab survives substrate restarts; depends on nx_provenance_curve_store)
58// - SUBPROCESS_SPAWN (uses nx_supervisor.nx_supervisor_spawn for capability-
59// gated subprocess launch; supervisor is PARTIAL_WIRED)
60// - LOG_CAPTURE (per-job stdout/stderr capture with size-rotation)
61// - MISSED_JOB_RECOVERY catch-up-ALL (fire every missed interval slot, not just
62// skip-to-now); only catch-up-once is wired today.
63//
64// license_tier: PUBLIC_NISHI_SUBSTRATE
65// genealogy_id: feedback-no-temporary-third-party-scaffolding-when-substrate-arc-exists_2026 +
66// feedback-bits-up-exceed-never-match +
67// feedback-substrate-does-heavy-lifting-user-is-partner-not-gate +
68// nx_race_timing (composes with sealed 8-mode timing enum) +
69// cron_unix_1975 (math + spec, not adoption)
70//
71// SCAFFOLDING_REPLACED_BY: this primitive replaces /etc/crontab entries
72// on Synology DSM. Today (2026-05-20) one cron entry exists:
73// */5 * * * * root /volume1/docker/nishi-engine/install-video.sh
74// When nx_cron + nx_supervisor + nx_deploy reach FULLY_WIRED, that crontab
75// entry + the entire install-video.sh shell script get DELETED in the same
76// commit. Nishi-native scheduler runs the substrate-native health audit.
77//
78// Reuse set:
79// - Replace /etc/crontab nishi_video_watchdog cron entry
80// - Replace /etc/crontab install-video.sh cron entry (this commit added it
81// as temporary scaffolding; sunset by this primitive)
82// - Schedule periodic nx_calibration_drift_monitor runs
83// - Schedule periodic nx_external_reach_probe runs
84// - Schedule periodic nx_daemon_health_audit runs
85// - Every future scheduled operation in any Nishi-hosted product
86
87import "nx_syscalls.nx"
88import "nx_race_timing.nx"
89
90// ===== Schedule kind sealed enum =================================
91//
92// Composes with nx_race_timing sealed enum from
93// feedback-racing-timing-modes-sealed-enum. Each kind here maps to a
94// nx_race_timing mode for actual dispatch.
95
96const NX_CRON_KIND_INTERVAL_SECONDS: i64 = 1 // every N seconds (CONTINUOUS)
97const NX_CRON_KIND_INTERVAL_MINUTES: i64 = 2
98const NX_CRON_KIND_INTERVAL_HOURS: i64 = 3
99const NX_CRON_KIND_DAILY_AT_HOUR: i64 = 4 // SCHEDULED at HH:MM
100const NX_CRON_KIND_WEEKLY_AT_DOW: i64 = 5 // SCHEDULED day-of-week
101const NX_CRON_KIND_MONTHLY_AT_DOM: i64 = 6
102const NX_CRON_KIND_ON_EVENT: i64 = 7 // EVENT-driven; not periodic
103const NX_CRON_KIND_ON_DEMAND: i64 = 8 // MANUAL; explicit trigger only
104const NX_CRON_KIND_IDLE_OPPORTUNISTIC: i64 = 9 // IDLE; substrate runs when
105 // no foreground load
106const NX_CRON_KIND_AT_UNIX_ONCE: i64 = 10 // SCHEDULED one-shot at absolute
107 // unix T (the CMS publish kind)
108
109func nx_cron_kind_name(k: i64) -> *u8 {
110 if k == NX_CRON_KIND_AT_UNIX_ONCE { return "AT_UNIX_ONCE" }
111 if k == NX_CRON_KIND_INTERVAL_SECONDS { return "INTERVAL_SECONDS" }
112 if k == NX_CRON_KIND_INTERVAL_MINUTES { return "INTERVAL_MINUTES" }
113 if k == NX_CRON_KIND_INTERVAL_HOURS { return "INTERVAL_HOURS" }
114 if k == NX_CRON_KIND_DAILY_AT_HOUR { return "DAILY_AT_HOUR" }
115 if k == NX_CRON_KIND_WEEKLY_AT_DOW { return "WEEKLY_AT_DOW" }
116 if k == NX_CRON_KIND_MONTHLY_AT_DOM { return "MONTHLY_AT_DOM" }
117 if k == NX_CRON_KIND_ON_EVENT { return "ON_EVENT" }
118 if k == NX_CRON_KIND_ON_DEMAND { return "ON_DEMAND" }
119 if k == NX_CRON_KIND_IDLE_OPPORTUNISTIC { return "IDLE_OPPORTUNISTIC" }
120 return "UNKNOWN_CRON_KIND"
121}
122
123// ===== Missed-job policy sealed enum =============================
124
125const NX_CRON_MISSED_SKIP: i64 = 1 // missed fires forgotten
126const NX_CRON_MISSED_CATCH_UP_ONCE: i64 = 2 // fire ONCE if any missed
127const NX_CRON_MISSED_CATCH_UP_ALL: i64 = 3 // fire every missed (rare;
128 // only for accumulator jobs)
129
130// ===== Job verdicts ==============================================
131
132const NX_CRON_VERDICT_OK: i64 = 0
133const NX_CRON_VERDICT_JOB_REGISTERED: i64 = 1
134const NX_CRON_VERDICT_JOB_UPDATED: i64 = 2
135const NX_CRON_VERDICT_JOB_UNREGISTERED: i64 = 3
136const NX_CRON_VERDICT_JOB_FIRED: i64 = 4
137const NX_CRON_VERDICT_JOB_FAILED: i64 = 5
138const NX_CRON_VERDICT_FAIL_BAD_SCHEDULE: i64 = 6
139const NX_CRON_VERDICT_FAIL_DUPLICATE_NAME: i64 = 7
140const NX_CRON_VERDICT_FAIL_SUPERVISOR_MISSING: i64 = 8
141const NX_CRON_VERDICT_FAIL_DEPENDENCY_MISSING: i64 = 9 // PARTIAL_WIRED default
142
143func nx_cron_verdict_name(v: i64) -> *u8 {
144 if v == NX_CRON_VERDICT_OK { return "OK" }
145 if v == NX_CRON_VERDICT_JOB_REGISTERED { return "JOB_REGISTERED" }
146 if v == NX_CRON_VERDICT_JOB_UPDATED { return "JOB_UPDATED" }
147 if v == NX_CRON_VERDICT_JOB_UNREGISTERED { return "JOB_UNREGISTERED" }
148 if v == NX_CRON_VERDICT_JOB_FIRED { return "JOB_FIRED" }
149 if v == NX_CRON_VERDICT_JOB_FAILED { return "JOB_FAILED" }
150 if v == NX_CRON_VERDICT_FAIL_BAD_SCHEDULE { return "FAIL_BAD_SCHEDULE" }
151 if v == NX_CRON_VERDICT_FAIL_DUPLICATE_NAME { return "FAIL_DUPLICATE_NAME" }
152 if v == NX_CRON_VERDICT_FAIL_SUPERVISOR_MISSING { return "FAIL_SUPERVISOR_MISSING" }
153 if v == NX_CRON_VERDICT_FAIL_DEPENDENCY_MISSING { return "FAIL_DEPENDENCY_MISSING" }
154 return "UNKNOWN_CRON_VERDICT"
155}
156
157// ===== Job descriptor struct =====================================
158//
159// Bundled per feedback-nishilang-16-arg-function-limit.
160
161struct NxCronJob {
162 job_name_ptr: *u8
163 job_name_len: i64
164 schedule_kind: i64 // NX_CRON_KIND_*
165 interval_seconds: i64 // for INTERVAL_* kinds
166 fire_at_hour: i64 // for DAILY_AT_HOUR (0..23)
167 fire_at_minute: i64
168 day_of_week: i64 // for WEEKLY_AT_DOW (0..6)
169 day_of_month: i64 // for MONTHLY_AT_DOM (1..31)
170 missed_policy: i64 // NX_CRON_MISSED_*
171 supervisor_handle_id: i64 // which nx_supervisor capability runs the job
172 log_path_hash_ptr: *u8
173 log_path_hash_len: i64
174 enabled: i64 // 0/1
175 fire_at_unix: i64 // resolved absolute next-fire (SCHEDULED/AT_UNIX_ONCE)
176 action_kind: i64 // NX_CRON_ACTION_* -- what the fire DOES
177 action_arg: i64 // action payload (e.g. CMS post id to publish)
178}
179
180// ===== Action kinds (what a fire DOES) ===========================
181//
182// nx_cron stays domain-agnostic: it records (action_kind, action_arg) and
183// pushes them on the fired-queue. The CONSUMER (the CMS, a supervisor, ...)
184// interprets the kind. PUBLISH = "promote CMS post #action_arg to live".
185
186const NX_CRON_ACTION_NONE: i64 = 0
187const NX_CRON_ACTION_PUBLISH: i64 = 1
188
189// ===== In-memory job table =======================================
190//
191// Same idiom as nx_bit_provenance: a static mmap'd table of fixed-size
192// i64 records, lazily initialised, shared across the entry functions.
193// (Disk persistence is the additive durability layer -- see header
194// MISSING_CAPABILITIES; it rests on nx_supervisor/provenance which are
195// PARTIAL_WIRED, so it is NOT faked here.)
196
197const NX_CRON_MAX_JOBS: i64 = 64
198const NX_CRON_REC_BYTES: i64 = 128 // 16 i64 slots per job
199const NX_CRON_TABLE_BYTES: i64 = 8192 // 64 * 128
200const NX_CRON_FIRED_CAP: i64 = 64
201
202// record slot indices (i64)
203const CJ_IN_USE: i64 = 0
204const CJ_NAME_HASH: i64 = 1
205const CJ_NAME_LEN: i64 = 2
206const CJ_KIND: i64 = 3
207const CJ_INTERVAL: i64 = 4
208const CJ_FIRE_AT: i64 = 5 // absolute next-fire unix seconds
209const CJ_MISSED: i64 = 6
210const CJ_ACTION_K: i64 = 7
211const CJ_ACTION_A: i64 = 8
212const CJ_ENABLED: i64 = 9
213const CJ_FIRED_CNT: i64 = 10
214const CJ_LAST_FIRE: i64 = 11
215const CJ_RECURRING: i64 = 12
216
217static NX_CRON_TABLE_PTR: i64
218static NX_CRON_N_JOBS: i64 // high-water slot count (iterate 0..N)
219static NX_CRON_FIRED_PTR: i64 // fired-action ring: pairs (kind,arg)
220static NX_CRON_FIRED_HEAD: i64
221static NX_CRON_FIRED_TAIL: i64
222static NX_CRON_LAST_VERDICT: i64
223
224func nx_cron_init() -> i64 {
225 if NX_CRON_TABLE_PTR != 0 { return 0 }
226 let raw: *u8 = sys_mmap(NX_CRON_TABLE_BYTES)
227 if (raw as i64) == 0 { return 1 }
228 if (raw as i64) == 0 - 1 { return 2 }
229 let fq: *u8 = sys_mmap(NX_CRON_FIRED_CAP * 16)
230 if (fq as i64) == 0 { return 1 }
231 if (fq as i64) == 0 - 1 { return 2 }
232 NX_CRON_TABLE_PTR = raw as i64
233 NX_CRON_N_JOBS = 0
234 NX_CRON_FIRED_PTR = fq as i64
235 NX_CRON_FIRED_HEAD = 0
236 NX_CRON_FIRED_TAIL = 0
237 NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_OK
238 return 0
239}
240
241// ----- helpers ----------------------------------------------------
242
243// map a cron kind to its nx_race_timing sealed mode, or -1 if unknown.
244func nx_cron_kind_race_mode(k: i64) -> i64 {
245 if k == NX_CRON_KIND_INTERVAL_SECONDS { return NX_RACE_CONTINUOUS }
246 if k == NX_CRON_KIND_INTERVAL_MINUTES { return NX_RACE_CONTINUOUS }
247 if k == NX_CRON_KIND_INTERVAL_HOURS { return NX_RACE_CONTINUOUS }
248 if k == NX_CRON_KIND_DAILY_AT_HOUR { return NX_RACE_SCHEDULED }
249 if k == NX_CRON_KIND_WEEKLY_AT_DOW { return NX_RACE_SCHEDULED }
250 if k == NX_CRON_KIND_MONTHLY_AT_DOM { return NX_RACE_SCHEDULED }
251 if k == NX_CRON_KIND_AT_UNIX_ONCE { return NX_RACE_SCHEDULED }
252 if k == NX_CRON_KIND_ON_EVENT { return NX_RACE_EVENT }
253 if k == NX_CRON_KIND_ON_DEMAND { return NX_RACE_MANUAL }
254 if k == NX_CRON_KIND_IDLE_OPPORTUNISTIC { return NX_RACE_IDLE }
255 return 0 - 1
256}
257
258// schedule well-formed? composes nx_race_timing_is_valid + the per-mode
259// required config (interval>0 for CONTINUOUS, resolved fire_at>0 for SCHEDULED).
260func nx_cron_schedule_ok(job_ptr: *NxCronJob) -> i64 {
261 let mode: i64 = nx_cron_kind_race_mode(job_ptr.schedule_kind)
262 if mode < 0 { return 0 }
263 if nx_race_timing_is_valid(mode) == 0 { return 0 }
264 if mode == NX_RACE_CONTINUOUS { if job_ptr.interval_seconds <= 0 { return 0 } }
265 if mode == NX_RACE_SCHEDULED { if job_ptr.fire_at_unix <= 0 { return 0 } }
266 return 1
267}
268
269// recurring kinds reschedule after a fire; one-shot/manual/event kinds do not.
270func nx_cron_recurring(k: i64) -> i64 {
271 if k == NX_CRON_KIND_INTERVAL_SECONDS { return 1 }
272 if k == NX_CRON_KIND_INTERVAL_MINUTES { return 1 }
273 if k == NX_CRON_KIND_INTERVAL_HOURS { return 1 }
274 if k == NX_CRON_KIND_DAILY_AT_HOUR { return 1 }
275 if k == NX_CRON_KIND_WEEKLY_AT_DOW { return 1 }
276 if k == NX_CRON_KIND_MONTHLY_AT_DOM { return 1 }
277 return 0
278}
279
280// deterministic polynomial name hash (no float, no xor); paired with name_len
281// for duplicate detection + fire_now/unregister lookup.
282func nx_cron_name_hash(p: *u8, n: i64) -> i64 {
283 var h: i64 = 0
284 var i: i64 = 0
285 while i < n { h = (h * 131) + (p[i] as i64); i = i + 1 }
286 if h < 0 { h = 0 - h }
287 return h
288}
289
290func nx_cron_rec(slot: i64) -> *i64 {
291 return (NX_CRON_TABLE_PTR + (slot * NX_CRON_REC_BYTES)) as *i64
292}
293
294// find an in-use job by (name_hash, name_len); -1 if none.
295func nx_cron_find(nh: i64, nl: i64) -> i64 {
296 nx_cron_init()
297 var i: i64 = 0
298 while i < NX_CRON_N_JOBS {
299 let rec: *i64 = nx_cron_rec(i)
300 if rec[CJ_IN_USE] == 1 { if rec[CJ_NAME_HASH] == nh { if rec[CJ_NAME_LEN] == nl { return i } } }
301 i = i + 1
302 }
303 return 0 - 1
304}
305
306func nx_cron_fired_push(kind: i64, arg: i64) -> i64 {
307 let nslot: i64 = NX_CRON_FIRED_TAIL % NX_CRON_FIRED_CAP
308 let q: *i64 = (NX_CRON_FIRED_PTR + (nslot * 16)) as *i64
309 q[0] = kind
310 q[1] = arg
311 NX_CRON_FIRED_TAIL = NX_CRON_FIRED_TAIL + 1
312 return 0
313}
314
315// drain one fired action (FIFO); returns 1 if one was popped, 0 if empty.
316func nx_cron_drain_fired(out_kind: *i64, out_arg: *i64) -> i64 {
317 nx_cron_init()
318 if NX_CRON_FIRED_HEAD >= NX_CRON_FIRED_TAIL { return 0 }
319 let nslot: i64 = NX_CRON_FIRED_HEAD % NX_CRON_FIRED_CAP
320 let q: *i64 = (NX_CRON_FIRED_PTR + (nslot * 16)) as *i64
321 out_kind[0] = q[0]
322 out_arg[0] = q[1]
323 NX_CRON_FIRED_HEAD = NX_CRON_FIRED_HEAD + 1
324 return 1
325}
326
327// fire a due job: record it, push its action, then reschedule (interval) or
328// disable (one-shot = FIRE-ONCE).
329func nx_cron_fire_slot(rec: *i64, now: i64) -> i64 {
330 rec[CJ_FIRED_CNT] = rec[CJ_FIRED_CNT] + 1
331 rec[CJ_LAST_FIRE] = now
332 nx_cron_fired_push(rec[CJ_ACTION_K], rec[CJ_ACTION_A])
333 if rec[CJ_RECURRING] == 1 {
334 if rec[CJ_INTERVAL] > 0 { rec[CJ_FIRE_AT] = now + rec[CJ_INTERVAL] } else { rec[CJ_ENABLED] = 0 }
335 } else {
336 rec[CJ_ENABLED] = 0
337 }
338 return 0
339}
340
341// ===== Top-level entry functions =================================
342
343// nx_cron_register_job -- register a job to run on the given schedule.
344// Refuses a malformed schedule (FAIL_BAD_SCHEDULE) or a duplicate name
345// (FAIL_DUPLICATE_NAME). On success the job is live in the dispatch table.
346func nx_cron_register_job(job_ptr: *NxCronJob) -> i64 {
347 nx_cron_init()
348 if job_ptr == 0 as *NxCronJob { NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_FAIL_BAD_SCHEDULE; return NX_CRON_VERDICT_FAIL_BAD_SCHEDULE }
349 if job_ptr.job_name_len <= 0 { NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_FAIL_BAD_SCHEDULE; return NX_CRON_VERDICT_FAIL_BAD_SCHEDULE }
350 if nx_cron_schedule_ok(job_ptr) == 0 { NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_FAIL_BAD_SCHEDULE; return NX_CRON_VERDICT_FAIL_BAD_SCHEDULE }
351 let nh: i64 = nx_cron_name_hash(job_ptr.job_name_ptr, job_ptr.job_name_len)
352 let nl: i64 = job_ptr.job_name_len
353 if nx_cron_find(nh, nl) >= 0 { NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_FAIL_DUPLICATE_NAME; return NX_CRON_VERDICT_FAIL_DUPLICATE_NAME }
354 // reuse a freed slot, else grow
355 var slot: i64 = 0 - 1
356 var i: i64 = 0
357 while i < NX_CRON_N_JOBS {
358 let r0: *i64 = nx_cron_rec(i)
359 if r0[CJ_IN_USE] == 0 { slot = i; i = NX_CRON_N_JOBS } else { i = i + 1 }
360 }
361 if slot < 0 {
362 if NX_CRON_N_JOBS >= NX_CRON_MAX_JOBS { NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_JOB_FAILED; return NX_CRON_VERDICT_JOB_FAILED }
363 slot = NX_CRON_N_JOBS
364 NX_CRON_N_JOBS = NX_CRON_N_JOBS + 1
365 }
366 let rec: *i64 = nx_cron_rec(slot)
367 rec[CJ_IN_USE] = 1
368 rec[CJ_NAME_HASH] = nh
369 rec[CJ_NAME_LEN] = nl
370 rec[CJ_KIND] = job_ptr.schedule_kind
371 rec[CJ_INTERVAL] = job_ptr.interval_seconds
372 rec[CJ_FIRE_AT] = job_ptr.fire_at_unix
373 rec[CJ_MISSED] = job_ptr.missed_policy
374 rec[CJ_ACTION_K] = job_ptr.action_kind
375 rec[CJ_ACTION_A] = job_ptr.action_arg
376 rec[CJ_ENABLED] = job_ptr.enabled
377 rec[CJ_FIRED_CNT] = 0
378 rec[CJ_LAST_FIRE] = 0
379 rec[CJ_RECURRING] = nx_cron_recurring(job_ptr.schedule_kind)
380 NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_JOB_REGISTERED
381 return NX_CRON_VERDICT_JOB_REGISTERED
382}
383
384// nx_cron_unregister_job -- remove a registered job (frees its slot).
385func nx_cron_unregister_job(job_name_ptr: *u8, job_name_len: i64) -> i64 {
386 nx_cron_init()
387 if job_name_len <= 0 { NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_FAIL_BAD_SCHEDULE; return NX_CRON_VERDICT_FAIL_BAD_SCHEDULE }
388 let nh: i64 = nx_cron_name_hash(job_name_ptr, job_name_len)
389 let slot: i64 = nx_cron_find(nh, job_name_len)
390 if slot < 0 { NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_JOB_FAILED; return NX_CRON_VERDICT_JOB_FAILED }
391 let rec: *i64 = nx_cron_rec(slot)
392 rec[CJ_IN_USE] = 0
393 NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_JOB_UNREGISTERED
394 return NX_CRON_VERDICT_JOB_UNREGISTERED
395}
396
397// nx_cron_fire_now -- explicit out-of-schedule trigger (ON_DEMAND jobs or a
398// manual re-run). Fires the action immediately; does NOT disable the job
399// (repeatable), unlike a scheduled one-shot tick.
400func nx_cron_fire_now(job_name_ptr: *u8, job_name_len: i64) -> i64 {
401 nx_cron_init()
402 if job_name_len <= 0 { NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_FAIL_BAD_SCHEDULE; return NX_CRON_VERDICT_FAIL_BAD_SCHEDULE }
403 let nh: i64 = nx_cron_name_hash(job_name_ptr, job_name_len)
404 let slot: i64 = nx_cron_find(nh, job_name_len)
405 if slot < 0 { NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_JOB_FAILED; return NX_CRON_VERDICT_JOB_FAILED }
406 let rec: *i64 = nx_cron_rec(slot)
407 rec[CJ_FIRED_CNT] = rec[CJ_FIRED_CNT] + 1
408 nx_cron_fired_push(rec[CJ_ACTION_K], rec[CJ_ACTION_A])
409 NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_JOB_FIRED
410 return NX_CRON_VERDICT_JOB_FIRED
411}
412
413// nx_cron_tick -- the dispatch loop. Fires every enabled job whose absolute
414// fire_at has passed (a past-due one-shot fires once = catch-up-once).
415// Returns the number of jobs fired this tick, or -1 on a bad clock value.
416func nx_cron_tick(current_unix_seconds: i64) -> i64 {
417 nx_cron_init()
418 if current_unix_seconds <= 0 { NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_FAIL_BAD_SCHEDULE; return 0 - 1 }
419 var fired: i64 = 0
420 var i: i64 = 0
421 while i < NX_CRON_N_JOBS {
422 let rec: *i64 = nx_cron_rec(i)
423 if rec[CJ_IN_USE] == 1 { if rec[CJ_ENABLED] == 1 {
424 // arm an interval job that has no absolute fire yet
425 if rec[CJ_FIRE_AT] == 0 { if rec[CJ_INTERVAL] > 0 { rec[CJ_FIRE_AT] = current_unix_seconds + rec[CJ_INTERVAL] } }
426 if rec[CJ_FIRE_AT] > 0 { if rec[CJ_FIRE_AT] <= current_unix_seconds {
427 nx_cron_fire_slot(rec, current_unix_seconds)
428 fired = fired + 1
429 } }
430 } }
431 i = i + 1
432 }
433 if fired > 0 { NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_JOB_FIRED } else { NX_CRON_LAST_VERDICT = NX_CRON_VERDICT_OK }
434 return fired
435}
436
437// nx_cron_count_jobs -- inspector: number of in-use jobs.
438func nx_cron_count_jobs() -> i64 {
439 nx_cron_init()
440 var c: i64 = 0
441 var i: i64 = 0
442 while i < NX_CRON_N_JOBS {
443 let rec: *i64 = nx_cron_rec(i)
444 if rec[CJ_IN_USE] == 1 { c = c + 1 }
445 i = i + 1
446 }
447 return c
448}
449
450// nx_cron_job_fired_count -- inspector: how many times a named job has fired
451// (-1 if not registered).
452func nx_cron_job_fired_count(job_name_ptr: *u8, job_name_len: i64) -> i64 {
453 nx_cron_init()
454 let nh: i64 = nx_cron_name_hash(job_name_ptr, job_name_len)
455 let slot: i64 = nx_cron_find(nh, job_name_len)
456 if slot < 0 { return 0 - 1 }
457 let rec: *i64 = nx_cron_rec(slot)
458 return rec[CJ_FIRED_CNT]
459}
460
461// nx_cron_get_last_verdict -- inspector.
462func nx_cron_get_last_verdict() -> i64 {
463 nx_cron_init()
464 return NX_CRON_LAST_VERDICT
465}