Alarms: el objeto que se despierta a sí mismo
Un Durable Object puede programar su propio futuro. Con setAlarm fija un instante y con el método alarm la plataforma lo despierta cuando llega la hora, aunque estuviera dormido, para hacer trabajo sin ningún cron externo. Vemos la API de alarmas —setAlarm, getAlarm, deleteAlarm—, el manejador alarm y sus reintentos, el patrón de reprogramación para tareas periódicas, y por qué una alarma por entidad es más precisa y más barata que un planificador global.
Hasta ahora un objeto solo actuaba cuando alguien lo llamaba: llegaba una petición, respondía, y volvía a dormir. Las alarmas le dan algo nuevo, casi inquietante: la capacidad de citarse consigo mismo en el futuro. Con setAlarm fija un instante; cuando ese instante llega, la plataforma materializa el objeto —aunque llevara horas dormido y descargado de memoria— e invoca su método alarm. No hace falta un cron externo, ni un proceso vigilando un reloj, ni una cola de trabajos diferidos: el propio objeto es quien recuerda que tenía algo pendiente y quien se despierta para hacerlo.
- Manejar la API de alarmas del almacén:
setAlarm,getAlarmydeleteAlarm. - Escribir el manejador
alarmy entender cuándo y cómo lo invoca la plataforma. - Implementar el patrón de reprogramación para convertir una alarma única en trabajo periódico.
- Contrastar una alarma por objeto con un cron global y ver por qué es más precisa y más barata.
setAlarm y el manejador alarm
La alarma vive en el mismo almacén durable que el estado. Con state.storage.setAlarm fijas un único instante futuro, expresado en milisegundos desde la época Unix; con getAlarm consultas cuál hay pendiente, y con deleteAlarm la cancelas. Cada objeto tiene como mucho una alarma a la vez: fijar una nueva sustituye a la anterior. Cuando la hora llega, la plataforma invoca el método alarm de la clase, sin petición ni respuesta, igual que un fetch pero disparado por el tiempo y no por un cliente.
export class Recordatorio {
constructor(private state: DurableObjectState, private env: Env) {}
async programar(dentroDeMs: number, mensaje: string): Promise<void> {
await this.state.storage.put("mensaje", mensaje);
// citate contigo mismo en el futuro
await this.state.storage.setAlarm(Date.now() + dentroDeMs);
}
// la plataforma llama a esto cuando llega la hora, aunque estuvieras dormido
async alarm(): Promise<void> {
const mensaje = await this.state.storage.get<string>("mensaje");
await enviarNotificacion(mensaje ?? "");
}
}
Observa que el mensaje se guardó en el almacén, no en un campo de memoria. Es deliberado: entre programar y alarm pueden pasar horas, el objeto puede reciclarse y perder todo su estado volátil, y aun así la alarma persiste junto a los datos que necesita. La alarma es durable como el resto del almacén.
Los otros dos métodos completan el juego. Con getAlarm consultas si hay una cita pendiente y a qué hora —devuelve el instante en milisegundos o null—, lo que te permite decidir si programar una nueva o respetar la que ya existe. Con deleteAlarm la cancelas cuando el trabajo que motivaba la cita deja de tener sentido. Entre los tres modelas todo el ciclo de vida de un temporizador: fijar, consultar, cancelar.
// consulta la cita pendiente y cancelala si el trabajo ya no hace falta
const cuando = await this.state.storage.getAlarm(); // instante en ms, o null
if (cuando !== null && this.trabajoYaResuelto) {
await this.state.storage.deleteAlarm();
}
Durabilidad y reintentos
Una alarma no es un setTimeout de JavaScript, que muere con el proceso. Se persiste en el almacén y sobrevive a desalojos, reinicios y despliegues: si el objeto no está en memoria cuando llega la hora, la plataforma lo trae de vuelta a la existencia solo para ejecutar alarm. Y si el manejador lanza una excepción, no se pierde el evento: la plataforma lo reintenta con retroceso exponencial hasta que termina sin error.
Esa política de reintento tiene una consecuencia que conviene grabar desde ya: la alarma es una entrega al menos una vez, no exactamente una vez. Un fallo tras haber hecho parte del trabajo, o incluso tras haberlo hecho entero pero antes de confirmarlo, provocará una segunda ejecución. Por eso el manejador debe ser idempotente, y el almacén consistente del objeto es justo la herramienta para lograrlo, como vemos a continuación.
Una por objeto
Cada objeto tiene a lo sumo una alarma pendiente. setAlarm reemplaza la anterior; getAlarm te dice el instante fijado o nulo; deleteAlarm la retira.
Durable, no en memoria
La alarma se persiste con el estado. Sobrevive a reinicios y despliegues, y despierta al objeto aunque estuviera descargado. No depende de un proceso vivo.
Con reintento
Si alarm lanza, la plataforma reintenta con retroceso exponencial. El trabajo diferido se ejecuta al menos una vez; conviene escribirlo idempotente.
Como la plataforma reintenta ante un fallo, tu codigo alarm puede llegar a correr mas de una vez para el mismo evento. Escribelo idempotente: comprueba en el almacen si el trabajo ya se hizo antes de repetir un efecto con consecuencias, como cobrar o enviar un correo. Apoyate en que el estado del objeto es fuertemente consistente para marcar el progreso y no duplicarlo.
Alarmas periodicas por reprogramacion
La API da una sola alarma futura, no un horario que se repite. Para obtener trabajo periódico usas un patrón simple y muy potente: al final de alarm, vuelves a llamar a setAlarm para el siguiente instante. El objeto queda así latiendo por su cuenta, tick tras tick, y cada tick decide cuándo será el próximo.
async alarm(): Promise<void> {
await this.hacerTrabajoPeriodico();
const activo = (await this.state.storage.get<boolean>("activo")) ?? true;
if (activo) {
// vuelve a citarse: un latido cada minuto
await this.state.storage.setAlarm(Date.now() + 60_000);
}
// si no se reprograma, el objeto vuelve a dormir sin gastar nada
}
Este autolatido tiene una virtud sobre un setInterval: como cada tick decide el siguiente, el intervalo puede cambiar con los datos —más frecuente cuando hay trabajo, más espaciado cuando el objeto está tranquilo— y, sobre todo, sobrevive a que el objeto se descargue entre tick y tick. No hay un temporizador vivo en memoria que un reinicio pueda matar; hay una cita persistida que la plataforma siempre honra.
flowchart LR M[metodo llama setAlarm t mas 60s] --> P[alarma persistida en el almacen] P --> W[el objeto duerme y se descarga] W -.llega la hora.-> A[la plataforma invoca alarm] A --> WORK[hace el trabajo diferido] A -.setAlarm de nuevo.-> P style A fill:#a6e3a1,color:#11111b
La diferencia con un cron global es de naturaleza, no de grado. Un cron vive en la configuración del Worker: es un puñado de horarios fijos, iguales para todos, escritos a mano. Una alarma vive en un objeto: hay una por entidad, se fija en tiempo de ejecución con precisión de milisegundos, y su instante puede depender de los datos de esa entidad concreta. Un cron te da unas pocas citas para todo el sistema; las alarmas te dan millones de temporizadores independientes, uno por cada objeto que los necesite, y solo cuestan cuando disparan.
Un temporizador por entidad
La verdadera potencia no es tener una alarma, sino tener una por objeto, a la escala de tus entidades. Modela un carrito de compra como un objeto: cada vez que el usuario añade algo, empujas hacia delante el instante de expiración; si la hora llega sin más actividad, el carrito se recuerda a sí mismo y avisa. No hay un proceso central escaneando carritos: cada uno lleva su propio temporizador.
export class Carrito {
constructor(private state: DurableObjectState, private env: Env) {}
async agregar(item: Item): Promise<void> {
const items = (await this.state.storage.get<Item[]>("items")) ?? [];
items.push(item);
await this.state.storage.put("items", items);
// reinicia el reloj de abandono: una hora desde el ultimo cambio
await this.state.storage.setAlarm(Date.now() + 60 * 60 * 1000);
}
async alarm(): Promise<void> {
const items = (await this.state.storage.get<Item[]>("items")) ?? [];
if (items.length > 0) await recordarCarritoAbandonado(items);
}
}
Fíjate en lo que no existe aquí: ninguna tabla de carritos que un cron recorra cada minuto preguntando cuáles llevan una hora quietos. Cada carrito porta su propio despertador, reiniciado en cada interacción, dormido y gratis hasta que suena. Multiplica eso por millones de carritos y sigues pagando solo por los que de verdad expiran, no por el barrido constante de todos.
Piensa en cómo se ha programado siempre el trabajo diferido. Fuera del proceso que tiene los datos vivía un planificador: un demonio cron, un servicio de colas, un orquestador que recorría tablas preguntando quién tiene algo pendiente. El estado era pasivo —esperaba, inerte, a que un vigía externo lo despertara—, y ese vigía tenía que estar siempre encendido, escrutando el reloj, aunque el noventa y nueve por ciento del tiempo no hubiera nada que hacer. Las alarmas invierten esa relación y ahí está el salto. El planificador desaparece como pieza aparte: la capacidad de citarse en el futuro se mete dentro del objeto, junto a los datos que la motivan. El estado deja de ser pasivo y gana agencia sobre el tiempo; ya no espera a que lo despierten, sino que decide él mismo cuándo despertar, y lo hace a partir de lo que sabe. Un carrito abandonado que se recuerda a sí mismo dentro de una hora; una suscripción que se cita el día de su renovación; un buffer que se promete vaciarse en diez segundos. La consecuencia arquitectónica es que dejas de necesitar un plano central de planificación —esa tabla de trabajos pendientes que todo backend termina construyendo, con su cron que la sondea y sus carreras cuando dos procesos toman el mismo trabajo—. Cada entidad lleva su propio reloj y su propio despertador, coubicados con su memoria. Y como la alarma solo consume recursos en el instante en que dispara, tener millones de temporizadores dormidos no cuesta nada: la escala del tiempo se vuelve tan barata y tan granular como la escala del estado. El ingeniero que interioriza esto deja de preguntarse dónde pongo el cron que revise esto y empieza a preguntarse cuándo debe este objeto despertarse solo.
- Escribe un objeto
Recordatoriocon un métodoprogramarque guarde un mensaje y fijesetAlarma treinta segundos, y unalarmque lo registre por consola. - Dispara la programación, cierra todo y comprueba que
alarmse ejecuta solo al llegar la hora, sin que nadie vuelva a llamar al objeto. - Convierte la alarma en periódica reprogramándola al final de
alarm. Añade una banderaactivoen el almacén que permita detener el latido sin desplegar código. - Haz que
alarmsea idempotente: marca en el almacén que el trabajo se hizo y razona qué pasaría, sin esa marca, cuando la plataforma reintenta tras un fallo. - Contrasta esta solución con un cron global: enumera tres cosas que una alarma por objeto hace y un puñado de horarios fijos del Worker no.