code wiki / (root) / nx_cron.nx

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}