---
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
