↑↓ para moverte · Enter para abrir · Esc para cerrar

    Todas las entradas
    Artículo, · 5 min de lectura

    Lunes y jueves

    Un usuario avisó de que las rutinas personalizadas creaban tareas en días equivocados. Dos piezas que le faltaban al scheduler de RedCheck, el mismo bug copiado en el calendario y unos tests que le daban la razón.

    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

    Every midnight,
    for each active routine

    Never generated: next = today
    Otherwise: next = the pattern's
    next day after the last task

    Create a task if next
    has already passed

    …unless the last task
    is still pending

    Today's task is created

    Every midnight,
    for each active routine

    Never generated: next = today
    Otherwise: next = the pattern's
    next day after the last task

    Create a task if next
    has already passed

    …unless the last task
    is still pending

    Today's task is created

    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

    Every midnight,
    for each active routine

    Never generated: next = the pattern's
    first day from today
    Otherwise: step forward from the
    last task until next isn't in the past

    Create a task only if
    next is today

    …unless the last task
    is still pending

    Today's task is created

    Every midnight,
    for each active routine

    Never generated: next = the pattern's
    first day from today
    Otherwise: step forward from the
    last task until next isn't in the past

    Create a task only if
    next is today

    …unless the last task
    is still pending

    Today's task is created

    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

    Cada medianoche,
    para cada rutina activa

    Nunca generada: próxima = hoy
    Si no: próxima = el siguiente día
    del patrón tras la última tarea

    Crear la tarea si la próxima
    fecha ya ha pasado

    …salvo que la última tarea
    siga pendiente

    Se crea la tarea de hoy

    Cada medianoche,
    para cada rutina activa

    Nunca generada: próxima = hoy
    Si no: próxima = el siguiente día
    del patrón tras la última tarea

    Crear la tarea si la próxima
    fecha ya ha pasado

    …salvo que la última tarea
    siga pendiente

    Se crea la tarea de hoy

    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

    Cada medianoche,
    para cada rutina activa

    Nunca generada: próxima = el primer
    día del patrón desde hoy
    Si no: avanzar desde la última tarea
    hasta que no quede en el pasado

    Crear la tarea solo si
    la próxima fecha es hoy

    …salvo que la última tarea
    siga pendiente

    Se crea la tarea de hoy

    Cada medianoche,
    para cada rutina activa

    Nunca generada: próxima = el primer
    día del patrón desde hoy
    Si no: avanzar desde la última tarea
    hasta que no quede en el pasado

    Crear la tarea solo si
    la próxima fecha es hoy

    …salvo que la última tarea
    siga pendiente

    Se crea la tarea de hoy

    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