Saltar al contenido principal

every — repetir cada intervalo

Formatos aceptados

Cualquier duración: un número de milisegundos o un texto.

schedule — expresión cron

Formato cron (5 campos)

Cada campo acepta *, un número, un rango (1-5), un paso (*/15, 1-30/5) o una lista separada por comas de esos. Un paso sobre * cuenta desde el mínimo del campo (*/3 en el mes es enero, abril, julio y octubre; */2 en el día del mes son los días impares). Regla de días del cron estándar: si ambos campos, día del mes y día de la semana, están restringidos, un día vale si cumple cualquiera de los dos — 0 9 13 * 5 corre cada día 13 y cada viernes. Si uno de ellos empieza por * (también un paso como */2), se tienen que cumplir los dos, así que 0 9 * * 1 es simplemente “los lunes”. Los nombres (mon, jan) y las macros (@daily) no están soportados: bot.schedule() lanza un TypeError que nombra el campo inválido en lugar de crear una tarea que nunca se ejecuta. Lo mismo con una expresión que nunca puede dispararse: un valor fuera del rango de su campo (minuto 60, hora 24, mes 13, día de la semana 8), un paso 0, un rango invertido (5-1) o un día que ningún mes elegido tiene (0 0 30 2 *). Del mismo modo, bot.every() necesita una duración positiva y bot.delay() una no negativa (si no, RangeError).

Zonas horarias

Por defecto los campos se leen en la zona horaria del proceso (el TZ del host). Pasa una zona IANA para leerlos en otra, por tarea o para todo el bot:
Las zonas se validan con Intl al crear la tarea (o el bot): un nombre desconocido lanza un RangeError. every y delay rechazan timeZone (solo aplica a los campos cron). task.timeZone indica qué zona usa una tarea. Una tarea sin zona no cambia en nada. Los cambios de horario de verano siguen una única política documentada. El campo de la hora es comodín cuando empieza por * (*, */2), igual que en los campos de día: Las horas de ejecución siempre son estrictamente crecientes y todas las réplicas calculan las mismas, así que las tareas durables coinciden en sus huecos, en las ejecuciones perdidas y en el catch-up a través de un cambio de hora. Con zona o sin ella, la búsqueda mira hasta cuatro años hacia delante (así 0 0 29 2 * siempre funciona).

delay — una vez después de X tiempo

Cancelar una tarea

bot.schedule, bot.every y bot.delay devuelven todos un ScheduledTask con un método .cancel():

Tareas durables (sobreviven a reinicios, una réplica por ejecución)

Las tareas anteriores viven en memoria: un reinicio olvida cuándo tocaban y, con varias réplicas, cada una ejecuta su propia copia. Dale a una tarea un id estable y pasa a ser durable:
  • su próxima ejecución se guarda, así que un reinicio conserva el calendario en lugar de volver a planificar desde “ahora”;
  • las ejecuciones perdidas mientras el bot estaba caído siguen una política (missed);
  • cada ejecución se reclama con un lease con fencing en el store compartido, así que N réplicas nunca la ejecutan a la vez (sigue siendo al menos una vez: ver Garantías y límites).
Es opt-in: las tareas sin id siguen en memoria, como se describe arriba.
Registra las tareas durables al arrancar, en todas las réplicas: solo se guarda el calendario, nunca el handler, así que tras un reinicio una tarea solo corre si el código vuelve a registrar su id (ver Registrar tareas). El handler recibe un ScheduledRun:

Opciones

Ejecuciones perdidas

Un hueco está perdido cuando se reclama más de un periodo de gracia después de vencer, lo que pasa cuando ninguna réplica lo estaba ejecutando (el bot estaba caído o todas las réplicas paradas). El periodo de gracia es tickMs (1 s por defecto), más dos veces leaderTtlMs si hay un lease de líder (un seguidor o un failover pueden tardar eso en entrar, y eso no es una ejecución perdida). Cada réplica lo decide solo con el estado guardado, así que todas coinciden. Los huecos perdidos siguen la política de la tarea:
  • 'skip': no ejecuta ninguna y espera al siguiente hueco después de ahora.
  • 'run-once': ejecuta una (missed: true, scheduledAt es el primer hueco perdido) y espera al siguiente hueco.
  • 'catch-up': ejecuta en orden cada hueco perdido, cada uno con su propio scheduledAt y occurrenceKey, hasta maxCatchUp; el resto se salta y se registra en el log. Todo el catch-up corre bajo un único reclamo en una sola réplica y se confirma al final: si ese proceso muere a medias, el catch-up vuelve a empezar desde su primer hueco (deduplica con run.occurrenceKey); si se detiene, sigue desde el primer hueco que no ejecutó.
Una ejecución interrumpida por una caída (su ejecutor murió con el lease) no está perdida: cuando vence el lease se ejecuta otra vez como tal (missed: false), y la política se aplica a los huecos posteriores. Los huecos que vencen mientras una ejecución sigue en curso (un handler más lento que el intervalo, o una ejecución que empezó tarde tras un relevo) nunca se pierden sin más con 'run-once' y 'catch-up': se reclaman a continuación y siguen la política (se ejecutan normalmente dentro del margen de gracia y, si no, como perdidos). Con 'skip' la tarea sigue con el primer hueco posterior al final de la ejecución. Un delay de una sola vez cuenta como un hueco: 'skip' descarta un recordatorio que venció con el bot caído, así que usa 'run-once' para recordatorios. Su hora se fija cuando el id se guarda por primera vez; un reinicio no la retrasa. Tras ejecutarse, la fila queda como tombstone (volver a registrar el id no la ejecuta otra vez) hasta que cancel() o bot.forgetTask(id) la borra. Los intervalos no derivan: el siguiente hueco es el anterior más el intervalo, no “ahora” más el intervalo. El cron usa la zona del proceso salvo que la tarea o scheduler.timeZone fijen una (ver Zonas horarias).

Registrar tareas

  • Registra las tareas recurrentes al arrancar, en todas las réplicas, con el mismo id y la misma especificación. También se puede registrar una tarea con el bot en marcha (la primera tarea durable arranca el motor durable), pero solo es durable mientras algún código la vuelva a registrar tras cada reinicio.
  • Los recordatorios por entidad (un id por pedido, por usuario…) funcionan si puedes reconstruirlos al arrancar con tus propios datos: volver a registrar el mismo id conserva su hora original.
Un id por entidad que ningún código registra tras un reinicio es un huérfano: se conserva y nunca se ejecuta.

Backends

BotConfig.scheduler acepta: Cada paquete de store trae su store de calendarios, que comparte la conexión y el prefijo del store: Con varias réplicas, guarda los calendarios en el store compartido y, si quieres, dale al scheduler un préstamo de líder:
El bot arranca su store antes que el scheduler, y las tablas se crean solas (SQLite las crea con el resto de su esquema; los demás stores, de forma idempotente en el primer uso). Si usas un store solo para los calendarios, haz antes await store.start(). Los stores SQL y Mongo borran de forma lógica: un id olvidado conserva su fila con deleted activo para que su token de fencing nunca retroceda. Purga a mano los tombstones antiguos solo cuando ningún ejecutor pueda seguir teniendo su token (por ejemplo DELETE FROM schedules WHERE deleted AND updated_at < <hace un día>). Sirve cualquier objeto que implemente ScheduleStore (el tipo se exporta desde @botmesh/core y @botmesh/core/spi). @botmesh/core/spi trae MemoryScheduleStore como implementación de referencia y los tipos de registro; el repositorio de botmesh comprueba cada store de tareas incluido con la misma suite compartida (test-utils/schedule-store-conformance.ts).

Garantías y límites

  • Al menos una vez por ejecución, nunca dos ejecuciones de una tarea a la vez (mientras los leases se respeten). Si un ejecutor cae, o se queda parado más que leaseMs, su lease vence y otra réplica (o el proceso reiniciado) vuelve a ejecutar el mismo hueco. El signal del ejecutor obsoleto se aborta y su commit se rechaza (queda en el log). Haz los handlers idempotentes con run.occurrenceKey.
  • Solo se guarda el calendario, no el handler. Un id guardado que ninguna tarea registrada reclama es un huérfano: se registra una vez al arrancar, se conserva y nunca se ejecuta. Bórralo con bot.forgetTask(id), que aplica el namespace del bot por ti. Ese namespace incluye el botId, el provider y el id de conexión: cambiar cualquiera de ellos deja las filas antiguas como huérfanas.
  • Los ids borrados conservan una marca pequeña con su último token de fencing (así los tokens nunca retroceden), y las tareas de una sola vez conservan su tombstone: con muchos ids por entidad, olvídalos al terminar.
  • Cambiar la especificación de una tarea (cron o intervalo) entre despliegues la vuelve a planificar desde ahora; la política de perdidas no se aplica a la especificación anterior.
  • stop() pausa: las filas se conservan. task.cancel() borra la fila.
  • Los plazos de los leases usan el reloj de cada réplica: mantén los relojes sincronizados (NTP) y leaseMs muy por encima del desfase esperado.
  • Las zonas horarias salen de los datos ICU de cada build de Node. Si las réplicas tienen tzdata distinta (o un gobierno cambia sus reglas de horario de verano), gana el nextRunAt guardado: las réplicas siguen la fila y solo el siguiente cálculo usa las reglas nuevas. Una zona que el Node de una réplica no conoce hace fallar su registro al arrancar, de forma visible.
  • bot.onIdle() sigue en memoria a propósito: los temporizadores de inactividad por chat se reconstruyen con el tráfico entrante.

Siguientes pasos