--- id: "0007d" title: "Scheduler: cron parser y ticker" status: completado type: feature domain: [] scope: multi-app priority: media depends: [] blocks: [] related: [] created: 2026-05-17 updated: 2026-05-17 tags: [] --- # 0007d — Scheduler: cron parser y ticker ## Metadata | Campo | Valor | |-------|-------| | **ID** | 0007d | | **Estado** | pendiente | | **Prioridad** | media | | **Tipo** | feature | ## Dependencias | ID | Título | Estado | Requerido | |----|--------|--------|-----------| | 0007a | Funciones core del DAG engine | pendiente | Si | | 0007c | Execution store | pendiente | Si | **Bloqueada por:** `#0007a, #0007c` **Desbloquea:** `#0007e` --- ## Objetivo Funciones para parsear expresiones cron, calcular proximas ejecuciones, y un ticker que dispara DAGs segun su schedule. Es lo que reemplaza el scheduler de Dagu. ## Contexto - Las expresiones cron de Dagu son estandar (5 campos: min hour dom mon dow) - El ticker es un loop infinito que cada minuto evalua que DAGs deben lanzarse - Funciones puras para parseo y calculo, impura solo el ticker ## Arquitectura ``` functions/core/ ├── cron_parse.go — NEW: string → CronExpression ├── cron_parse.md ├── cron_next.go — NEW: CronExpression + time → proxima ejecucion ├── cron_next.md ├── cron_match.go — NEW: CronExpression + time → bool (coincide?) ├── cron_match.md functions/infra/ ├── dag_ticker.go — NEW: loop que evalua schedules y lanza DAGs ├── dag_ticker.md types/core/ ├── cron_expression.md — NEW: minute, hour, dom, month, dow (cada uno []int o wildcard) ``` ### Patron pure core / impure shell - `core/` — `cron_parse`, `cron_next`, `cron_match` son puras - `infra/` — `dag_ticker` es impuro (time.Sleep, lanza ejecuciones) ## Tareas ### Fase 1: Tipos - [ ] **1.1** Definir `CronExpression` — campos parseados con soporte para *, ranges (1-5), lists (1,3,5), intervals (*/5) ### Fase 2: Funciones puras - [ ] **2.1** `cron_parse` — "0 9 * * *" → CronExpression. Soportar: *, N, N-M, N/M, listas - [ ] **2.2** `cron_next` — dada una CronExpression y un time.Time, retorna el proximo time.Time que coincide - [ ] **2.3** `cron_match` — dada una CronExpression y un time.Time, retorna true si coincide (para el ticker) - [ ] **2.4** Tests exhaustivos: wildcards, ranges, listas, intervalos, edge cases (fin de mes, febrero) ### Fase 3: Ticker - [ ] **3.1** `dag_ticker` — recibe lista de (DagDefinition, path), cada minuto evalua cron_match para cada uno, lanza los que coinciden - [ ] **3.2** Soporte para cancelacion (context.Context) y graceful shutdown ### Fase 4: Cleanup - [ ] `fn index` y verificar IDs --- ## Ejemplo de uso ```go // Puro expr, _ := cron_parse("*/5 9-17 * * 1-5") // cada 5 min, 9-17h, lun-vie next := cron_next(expr, time.Now()) // proxima ejecucion matches := cron_match(expr, time.Now()) // true si ahora coincide // Impuro (el ticker) ctx, cancel := context.WithCancel(context.Background()) dag_ticker(ctx, dags, executor) // loop infinito hasta cancel ``` ## Decisiones de diseno - **No usar libreria cron externa**: las expresiones son simples, implementar desde cero es ~100 lineas y evita dependencias - **Separar parse/next/match**: parse es costoso, match es barato — parsear una vez, match cada minuto - **Ticker como funcion, no como goroutine**: el caller decide como lanzarlo ## Criterios de aceptacion - [ ] Parsea todas las expresiones cron de los DAGs existentes en `~/dagu/dags/` - [ ] `cron_next` calcula correctamente la proxima ejecucion - [ ] `cron_match` coincide correctamente para el minuto actual - [ ] Ticker lanza DAGs en el momento correcto - [ ] Tests pasan - [ ] Indexado en registry.db