Mondays and Thursdays
A user reported that custom routines created tasks on the wrong days. Two missing pieces in RedCheck's scheduler, the same bug copied into the calendar, and tests that agreed with it.
A RedCheck user reported that routines with custom days were creating tasks on the wrong days. The report was short and completely right: a routine meant for, say, Mondays and Thursdays could produce a task on a day nobody had picked.
This post explains how routines work, the two pieces the scheduler was missing, and the fix. The fix was one rule moved into one place.
How a routine becomes a task
A routine is a template, not a task (that decision goes back to part 1). Every night at midnight, a scheduler goes through the active routines and decides, for each one, whether today gets a task.
The frequency is either a preset (daily, weekly, every two weeks,
monthly, yearly) or a custom pattern: specific weekdays, or a day of the
month like “the 15th” or “the last day”. Custom weekdays are stored as a
deliberately restricted cron expression, 0 0 0 * * 1,4 for Mondays and
Thursdays (part 16 explains why it’s
restricted). Each routine also remembers when it last generated a task,
and one rule from the very beginning still holds: if the previous task
is still pending, no new one is created, so a missed week never turns
into a pile of duplicates.
How it worked before
Two of those steps were wrong, and both only showed up with custom patterns. Presets have no “wrong day”: any day is a valid start for “daily” or “weekly”.
The first task ignored the pattern. A routine that had never generated anything got a task on the very next midnight. For a daily routine, that’s correct. For a “Mondays and Thursdays” routine created on a Tuesday, it meant a task on Wednesday.
“Has it passed?” is not “is it today?”. For every later task, the scheduler worked out the pattern’s next day after the last task and generated as soon as that moment had passed. On a normal week, those are the same question: the next day arrives, it has just passed, a task appears. They stop being the same after a gap. Pause a routine for two weeks and turn it back on during a Saturday: at midnight, the “next day” is a Thursday from two weeks ago. It has certainly passed, so a task is created on Sunday. Editing a routine’s days, or a scheduler run missed during a deploy, had the same effect.
How it works now
Same steps, two of them changed:
- The first task waits for the pattern. A new routine starts on the first day its pattern selects. A preset still starts on the first midnight, as before.
- The condition is “is it today?”. After a gap, the scheduler keeps stepping through the pattern from the last task until it reaches a day that isn’t in the past. If that day is today, there’s a task. If it’s Monday, it waits for Monday. It also compares calendar days now, not timestamps down to the millisecond.
The pending rule is untouched. The old version also moved the “last generated” date forward whenever it skipped a pending task; with the new stepping that’s no longer needed, so it’s gone.
The same “Mondays and Thursdays” routine, before and after:
| Situation | Before | Now |
|---|---|---|
| Created on a Tuesday | First task on Wednesday | First task on Thursday |
| Paused for two weeks, back on a Saturday | Task on Sunday | Next task on Monday |
| A normal week | Monday and Thursday | Monday and Thursday |
That last row is why it took a while to notice: on an ordinary week, both versions behave exactly the same.
The same bug, twice more
Reviewing the fix turned up two more copies of the same logic.
The calendar. Recurring calendar events, like a weekly class or a yearly birthday, were added later (part 14). Their scheduler was written by mirroring the task scheduler, and it inherited both bugs. A “Mondays and Thursdays” event created on a Tuesday also appeared on Wednesday.
The previews. The routine editor shows the next few dates as you pick days, and each routine shows its next occurrence. Both previews had been written to mirror the scheduler, faithfully: the preview said “Wednesday” too. The interface was consistent with the bug, which is exactly why it didn’t look like one.
Instead of fixing four places, the rule now lives in one function on the backend: given a frequency, the date of the last task and a day, it returns the routine’s next occurrence on or after that day. Both schedulers ask it “is the next occurrence today?”, and both previews ask it “what’s the next occurrence from tomorrow?”. The frontend’s live preview in the editor follows the same rule, with tests on both sides.
The tests agreed with the bug
Some of the existing tests had to change, and not because they were flaky. They asserted the old behavior. One expected the preview of a “Mondays and Thursdays” routine to start on a Friday, with a comment explaining that the first occurrence ignored the pattern on purpose.
A test proves the code does what the test says. If the test was written from the code’s behavior instead of from what users expect, it locks the bug in. The fix added the cases that were missing: a first occurrence that lands on a selected day, a gap that never fires on an unselected one, a routine that already ran today, day 31 in a short month, and a calendar event that follows the same rules as a task.
What I took away
- “Has the date passed?” and “is the date today?” are the same question only until something interrupts the routine.
- When two pieces of code are written by copying one another, a bug copies itself too. One shared function is cheaper than remembering there are several.
- A preview that mirrors the logic will mirror its bugs, and make them look intentional.
- Read old tests as claims about behavior, not as proof that the behavior is right.
Routines with custom days now create tasks on the days you picked. If yours ever doesn’t, the feedback form in Settings is the fastest way to tell me.
Un usuario de RedCheck avisó de que las rutinas con días personalizados creaban tareas en días que no tocaban. El aviso era breve y tenía toda la razón: una rutina pensada, por ejemplo, para los lunes y los jueves podía generar una tarea en un día que nadie había elegido.
Este post explica cómo funcionan las rutinas, las dos piezas que le faltaban al scheduler y el arreglo. El arreglo fue mover una regla a un solo sitio.
Cómo una rutina se convierte en tarea
Una rutina es una plantilla, no una tarea (esa decisión viene de la parte 1). Cada noche a las doce, un scheduler recorre las rutinas activas y decide, para cada una, si hoy le toca tarea.
La frecuencia es o bien un preset (diaria, semanal, cada dos semanas,
mensual, anual) o bien un patrón personalizado: días concretos de la
semana, o un día del mes como “el 15” o “el último día”. Los días de la
semana personalizados se guardan como una expresión cron restringida a
propósito, 0 0 0 * * 1,4 para lunes y jueves (la
parte 16 explica por qué está
restringida). Cada rutina recuerda además cuándo generó su última tarea,
y una regla del principio sigue en pie: si la tarea anterior sigue
pendiente, no se crea otra, para que una semana olvidada no se convierta
en un montón de duplicados.
Cómo funcionaba antes
Dos de esos pasos estaban mal, y los dos solo se notaban con patrones personalizados. Los presets no tienen un “día equivocado”: cualquier día vale para empezar algo diario o semanal.
La primera tarea ignoraba el patrón. Una rutina que nunca había generado nada recibía una tarea la siguiente medianoche. Para una rutina diaria, eso es correcto. Para una de “lunes y jueves” creada un martes, significaba una tarea el miércoles.
“¿Ya ha pasado?” no es “¿es hoy?”. Para el resto de tareas, el scheduler calculaba el siguiente día del patrón tras la última tarea y generaba en cuanto ese momento había pasado. En una semana normal son la misma pregunta: llega el día, acaba de pasar, aparece la tarea. Dejan de serlo después de un hueco. Pausa una rutina dos semanas y vuelve a activarla un sábado: a medianoche, la “próxima fecha” es un jueves de hace dos semanas. Desde luego ya ha pasado, así que se crea una tarea el domingo. Cambiar los días de una rutina, o una ejecución perdida durante un despliegue, tenían el mismo efecto.
Cómo funciona ahora
Los mismos pasos, con dos cambiados:
- La primera tarea espera al patrón. Una rutina nueva empieza el primer día que elige su patrón. Un preset sigue empezando la primera medianoche, como antes.
- La condición es “¿es hoy?”. Después de un hueco, el scheduler va avanzando por el patrón desde la última tarea hasta llegar a un día que no esté en el pasado. Si ese día es hoy, hay tarea. Si es el lunes, espera al lunes. Además compara días del calendario, no marcas de tiempo al milisegundo.
La regla de la tarea pendiente no cambia. La versión anterior también movía hacia delante la fecha de “última generación” cada vez que se saltaba una tarea pendiente; con el nuevo avance ya no hace falta, así que ha desaparecido.
La misma rutina de “lunes y jueves”, antes y ahora:
| Situación | Antes | Ahora |
|---|---|---|
| Creada un martes | Primera tarea el miércoles | Primera tarea el jueves |
| Pausada dos semanas, reactivada un sábado | Tarea el domingo | Siguiente tarea el lunes |
| Una semana normal | Lunes y jueves | Lunes y jueves |
La última fila explica por qué costó verlo: en una semana corriente, las dos versiones se comportan exactamente igual.
El mismo bug, dos veces más
Al revisar el arreglo aparecieron dos copias más de la misma lógica.
El calendario. Los eventos recurrentes del calendario, como una clase semanal o un cumpleaños anual, llegaron después (parte 14). Su scheduler se escribió imitando al de tareas, y heredó los dos fallos. Un evento de “lunes y jueves” creado un martes también aparecía el miércoles.
Las vistas previas. El editor de rutinas muestra las próximas fechas mientras eliges los días, y cada rutina muestra su próxima ocurrencia. Las dos vistas previas se escribieron imitando al scheduler, y con toda fidelidad: la vista previa también decía “miércoles”. La interfaz era coherente con el bug, que es justo por lo que no parecía un bug.
En lugar de arreglar cuatro sitios, la regla vive ahora en una sola función del backend: dada una frecuencia, la fecha de la última tarea y un día, devuelve la próxima ocurrencia de la rutina a partir de ese día. Los dos schedulers le preguntan “¿la próxima ocurrencia es hoy?”, y las dos vistas previas le preguntan “¿cuál es la próxima a partir de mañana?”. La vista previa en directo del editor, en el frontend, sigue la misma regla, con tests en los dos lados.
Los tests le daban la razón al bug
Hubo que cambiar algunos de los tests que ya existían, y no porque fallaran a ratos. Comprobaban el comportamiento antiguo. Uno esperaba que la vista previa de una rutina de “lunes y jueves” empezara en viernes, con un comentario que explicaba que la primera ocurrencia ignoraba el patrón a propósito.
Un test demuestra que el código hace lo que dice el test. Si el test se escribió a partir de lo que hacía el código y no de lo que esperan los usuarios, deja el bug fijado. El arreglo añadió los casos que faltaban: una primera ocurrencia que cae en un día elegido, un hueco que nunca genera en un día no elegido, una rutina que ya se ejecutó hoy, el día 31 en un mes corto y un evento del calendario que sigue las mismas reglas que una tarea.
Con qué me quedo
- “¿Ya ha pasado la fecha?” y “¿la fecha es hoy?” son la misma pregunta solo hasta que algo interrumpe la rutina.
- Cuando dos trozos de código se escriben copiando uno del otro, el bug también se copia. Una función compartida sale más barata que acordarse de que hay varias.
- Una vista previa que imita la lógica imita también sus bugs, y los hace parecer intencionados.
- Lee los tests antiguos como afirmaciones sobre el comportamiento, no como prueba de que el comportamiento es correcto.
Las rutinas con días personalizados crean ahora sus tareas los días que elegiste. Si alguna vez la tuya no lo hace, el formulario de feedback de Ajustes es la forma más rápida de contármelo.
- Backend
- Frontend