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)
*, 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 (elTZ del
host). Pasa una zona IANA para leerlos en otra, por tarea o para todo el bot:
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 unid
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).
id siguen en memoria, como se describe arriba.
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 estickMs
(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,scheduledAtes el primer hueco perdido) y espera al siguiente hueco.'catch-up': ejecuta en orden cada hueco perdido, cada uno con su propioscheduledAtyoccurrenceKey, hastamaxCatchUp; 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 conrun.occurrenceKey); si se detiene, sigue desde el primer hueco que no ejecutó.
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.
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:
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. Elsignaldel ejecutor obsoleto se aborta y su commit se rechaza (queda en el log). Haz los handlers idempotentes conrun.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 elbotId, 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
leaseMsmuy 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
nextRunAtguardado: 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
- Sub-APIs — grupos, perfil, chats, etc.
- Despliegue — configuración de producción