---
layout: 'page'
uri: '/framework/scheduler'
position: 40
slug: 'framework-scheduler'
parent: 'framework-background'
navTitle: 'Scheduler'
title: 'Scheduler'
description: 'In-process scheduler — periodické joby, každý ve své goroutině, run-once-then-tick, per-tick recovery a drain při SIGTERM.'
---

# Scheduler

In-process scheduler pro periodickou (cron-like) práci uvnitř běžícího serveru — žádný externí OS cron, žádný samostatný proces. Každý job běží ve vlastní goroutině, poprvé hned po startu a pak každý `Interval`. Implementace je v `app/infrastructure/scheduler/scheduler.go`.

> Přehled toku. Návod „přidat periodický job" → `/gk-scheduler`; perzistentní durable engine → `/gk-runs`.


## K čemu to je

Na údržbové úlohy uvnitř procesu — úklid, synchronizace, sběr statistik. **Není** to perzistentní fronta: stav je jen v paměti (in-memory), bez retry, restart úlohy jen znovu rozběhne. Práci, která **musí** proběhnout i po pádu procesu, posílej do [Fire-and-forget](/framework/fire-and-forget) (`/gk-runs`). Joby navíc běží **bez tenanta v ctx** — pod `APP_MULTITENANCY=true` job, který sáhne na tenant-owned tabulku (`r.Tenant(ctx)`), zpanikaří při každém ticku (panika se jen loguje, do Sentry nejde); tenant-scoped práce patří do runu (tenant se razítkuje při enqueue), nebo si musí tenant vyřešit explicitně.


## Jak to teče

1. `NewScheduler` ověří joby **při startu** (fail-fast): neprázdné jméno, `Interval > 0`, `Fn != nil`, unikátní jména — jinak se aplikace nerozběhne.
2. `Run(ctx)` spustí každý job ve vlastní goroutině.
3. **Run-once-then-tick**: `Fn` se spustí hned, teprve pak se spustí `time.Ticker` — údržba tak proběhne aspoň jednou za životnost procesu, i při častých restartech.
4. Každý `tick` má vlastní `recover()` — panika v jednom jobu ostatní nepoloží. Recovery jen loguje, **nehlásí do Sentry** (deterministická panika by se jinak opakovala při každém ticku donekonečna).
5. SIGTERM zruší sdílený `ctx`, každá goroutina opustí `select`, `Run` blokuje až do `wg.Wait()` — scheduler i server tak doběhnou zároveň.


## Příklad

Joby se registrují na jednom místě — `provideSchedulerJobs` v DI:

```go
scheduler.Job{
    Name:     "cleanup:expired-refresh-tokens",
    Interval: time.Hour,
    Fn:       tokenRepo.DeleteExpired,
}
```

Aktuálně jediný job maže prošlé refresh tokeny (`WHERE julianday(expires_at) < julianday('now')` — julianday je repo-wide idiom pro datetime porovnání, viz `sqlite/sqltime.go`); díky run-once-then-tick proběhne úklid hned po startu.


## Související

- [Fire-and-forget](/framework/fire-and-forget) — fire-and-forget tvar durable enginu pro práci, která musí přežít restart.
- [Architecture](/framework/architecture) — vrstvy a startup sekvence.
- Skilly: `/gk-scheduler`, `/gk-runs`, `/gk-logging`.

---

[← Durable run](/framework/durable-run.md) | [Roadmap (GoKick) →](/framework/gokick-roadmap.md)