Cron
Un scheduler de background integrado. Cada job corre en su propio thread, sin bloquear, y ejecuta su task de verdad — los contadores de cron_list() reflejan ejecuciones reales.
-- Doc example: cron scheduler. Jobs run on background threads and EXECUTE their
-- task for real — the doctest asserts the observable effect, not just registration.
intent: "doc example: cron"
require time
require file("_doctest_cron.txt")
task write_marker()
write_file("_doctest_cron.txt", "cron ran")
print("scheduled jobs: " + text(length(cron_list())))
test "a scheduled job executes its task and the counters tell the truth"
cron_after(0.2, write_marker)
sleep(0.8)
assert_eq(read_file("_doctest_cron.txt"), "cron ran")
assert_eq(length(cron_list()), 1)
each j in cron_list()
assert_eq(j["name"], "write_marker")
assert_eq(j["run_count"], 1)
assert_eq(j["errors"], 0)
Programar
task sync_inventory()
let data be http_get("https://api.warehouse.com/stock")
share data as "inventory"
cron_every(300, sync_inventory) -- cada 5 minutos
cron_after(3600, send_reminder) -- una vez, después de 1 hora
El task debe ser de 0 parámetros y estar definido en el top-level (el job lo ejecuta por nombre). Un task con parámetros obligatorios falla en la registración con un error claro — envolvelo en un task sin argumentos. cron_every exige un intervalo positivo; cron_after acepta delay 0 (ejecuta ya mismo). El argumento task es la referencia (sync_inventory) o su nombre como texto ("sync_inventory"); ambos builtins devuelven el nombre del job (texto), que es lo que toma cron_cancel(nombre).
Gestionar
cron_cancel("sync_inventory") -- detener un job
let jobs be cron_list() -- listar todos los jobs
print(cron_status()) -- estado formateado
Cada entrada de cron_list() trae name, interval, repeating, active, run_count (ejecuciones completadas) y errors (ticks que terminaron en error). Registrar un job con el mismo nombre reemplaza al anterior (los contadores arrancan de cero).
Semántica
- Intervalos, no cron de pared. El intervalo es un delay fijo entre el fin de una ejecución y el inicio de la siguiente — no hay expresiones tipo
"0 9 MON"ni alineación al reloj. - Sin solapamiento. Un job jamás corre dos ticks a la vez: si el task tarda más que el intervalo, los ticks se serializan.
- Errores: el job sigue. Un error de runtime en el task incrementa
errors, se loguea por el log del server ([serve] [cron] job 'x' failed: …) y el job queda programado para el próximo tick. El proceso jamás se cae por un tick fallido. - Estado in-memory, sin catch-up. Un reinicio re-registra los jobs cuando el top-level vuelve a correr;
run_countarranca de 0 y las corridas perdidas no se recuperan.
Bajo serve: estado compartido
Bajo synsema serve, los jobs corren con el mismo estado compartido y las mismas capabilities que las rutas: db, state_*, memoria, blackboard. Los jobs del top-level arrancan recién cuando el server ya está sirviendo, y un cron_every registrado desde una ruta funciona y es visible globalmente.
task tick()
state_incr("latidos", 1)
cron_every(60, tick)
serve on 8080
route "GET /salud"
give state_get("latidos")
Mantener los jobs vivos
Bajo run, los jobs ejecutan mientras el programa viva y se detienen cuando termina. Los jobs comparten estado entre ellos (un job puede leer lo que otro escribió con state_* o remember). Para intercambiar datos con el resto del programa, usá efectos externos: un archivo (write_file/read_file), una base en disco, o el blackboard (share/observe). Bajo serve no hace falta nada de esto: jobs y rutas ya ven el mismo estado compartido.
Usá synsema serve para mantener el proceso — y la programación — corriendo, incluso sin rutas:
synsema serve scheduler.syn -- "Serving N cron job(s). Press Ctrl+C to stop."
Los threads esperan estacionados (cero CPU entre ticks); el intérprete de cada job se construye una vez y se reusa.