---
layout: 'page'
uri: '/framework/infrastructure/job-queue'
position: 6
slug: 'framework-infrastructure-job-queue'
parent: 'framework-infrastructure'
navTitle: 'Job Queue'
title: 'Job Queue'
description: 'Perzistentní fronta pro background práci -- jak rozjet úlohu, která musí proběhnout i když proces mezitím spadne.'
---
# Job Queue
## K čemu ti to je
[Events](/framework/application/events) řeší synchronní reakci v request goroutině -- skvělé pro rychlé side-effects. Pomalý handler ale prodlouží HTTP response a SIGTERM mu uřízne inflight práci. Pro odeslání emailu přes SMTP, volání externího API nebo cokoli retry-prone potřebuješ **perzistenci**.
Job Queue je SQLite tabulka `jobs`. Command handler zavolá `Enqueue("welcome:send", payload)` -- zapíše se řádek **ve stejné transakci** jako business write. Worker (goroutina nebo samostatný `./bin/app worker` proces) si ho vyzvedne a zavolá handler. Když spadne, retry s exponential backoff. Když crashne celý proces, restart pokračuje tam, kde skončil.
Tři garance:
1. **Atomicita business write + enqueue.** Uložení uživatele a enqueue welcome jobu jdou v jedné DB transakci. Buď obojí, nebo nic.
2. **At-least-once delivery.** Job proběhne minimálně jednou. Handler musí být idempotentní pro **externí** side effects (poslat dva maily je špatně).
3. **Mark-complete v handlerově tx.** "Job hotový" se zapíše ve stejné transakci jako handler's DB writes. Handler-fail = celá tx rollback, žádné částečné stavy.
## Krok za krokem
Scénář: po `CreateUser` poslat welcome email přes SMTP (pomalé, může selhat).
### 1. Handler funkce
`application/user/job/send_welcome.go`:
```go
type WelcomePayload struct {
UserID string `json:"user_id"`
Email string `json:"email"`
}
func (h *SendWelcomeHandler) Handle(ctx context.Context, payload []byte) error {
var p WelcomePayload
if err := json.Unmarshal(payload, &p); err != nil {
return err
}
return h.mailer.Send(p.Email, "Welcome!", /* ... */)
}
```
Vrátí error → worker zařadí retry. Vrátí nil → job complete.
### 2. Zaregistruj v `provideJobHandlerRegistry`
`infrastructure/di/container_provider.go` -- stejný pattern jako [events](/framework/application/events) a [scheduler](/framework/infrastructure/scheduler):
```go
func provideJobHandlerRegistry(welcome *jobcmd.SendWelcomeHandler) (*jobapp.HandlerRegistry, error) {
return jobapp.NewHandlerRegistry(map[string]jobapp.HandlerFunc{
"welcome:send": welcome.Handle,
})
}
```
### 3. Enqueue z command handleru
`Enqueue` má povinné `maxRetries` jako poziční parametr -- žádný magický default. `0` = "vykonej jednou, žádný retry" (welcome mail, audit log). Vyšší pro flaky externí volání (`3` = 3 retries po prvním selhání = až 4 attempts).
```go
if err := h.users.Save(ctx, u); err != nil {
return err
}
return shared.JobDispatcherFromContext(ctx).Enqueue(ctx, "welcome:send", 0, WelcomePayload{
UserID: u.ID, Email: u.Email,
})
```
Dispatcher zkontroluje, že kind je v registru (chytíš překlep v testu, ne v produkci), payload zapíše do `jobs` -- ve stejné transakci jako `users.Save` výše.
### 4. `make di`
Hotovo. SMTP nefunguje? Když máš `maxRetries=3`, worker to za 5s zkusí znovu, pak 10, 20 ... a po 4. selhání (1 původní + 3 retries) označí `failed` (řádek zůstane pro debug). Pokud máš `maxRetries=0`, po prvním selhání rovnou `failed` -- žádný retry.
## Co se ti hodí vědět
- **Mark-complete v handlerově tx.** Bez toho by crash mezi handler-commit a mark-complete způsobil duplicate side effect. Náš pattern: handler authors přemýšlejí o idempotenci jen pro **externí** side effects -- DB writes se rollbacknou společně s "job hotový" flagem.
- **Default 1 worker.** SQLite serializuje writery (WAL: jeden writer na celou DB). Víc workerů nezvýší throughput DB-bound handlerů. Bumpnout má smysl, jen když jsou handlery I/O-bound mimo SQLite.
- **Standalone `./bin/app worker` proces** spustí jen worker bez HTTP serveru -- vhodné pro split deploy (1× serve + N× worker, sdílená SQLite).
- **Cascade jobs OK, cascade events ne (strojově vynuceno).** Job handler může enqueueovat další jobs (`JobDispatcher` je v ctx). Když ale zavolá `EventCollectorFromContext.Collect(...)`, runtime panic — sběrač eventů se flushuje jen v command request goroutině, ne ve workeru. Worker chybu zachytí, zaloguje, job se reschedule.
## Co lze nastavit
| Co | Kde | Default | Jak změnit |
|---|---|---|---|
| Které kindy worker zná | `provideJobHandlerRegistry()` v `container_provider.go` | prázdná mapa | Přidej `kind → handler.Handle` entry |
| `maxRetries` | Povinný poziční parametr `Enqueue` | bez defaultu (musíš zvolit) | `disp.Enqueue(ctx, "welcome:send", 0, payload)` -- `0` = no retry, `3` = až 3 retries po prvním selhání |
| Odložené spuštění | `shared.WithDelay(d)` při `Enqueue` | spustit ihned | `disp.Enqueue(ctx, kind, 0, payload, shared.WithDelay(time.Hour))` |
| Worker concurrency | `provideWorker` v `container_provider.go` | `1` | Zvyš parametr (pozor na SQLite serializaci) |
| Poll interval / backoff / lock timeout | Konstanty ve `infrastructure/worker/worker.go` | `1s` / `5s base, 1h cap` / `5min` | Pro reálné nasazení vytáhni do configu |
---
[← Scheduler](/framework/infrastructure/scheduler.md) | [Observability →](/framework/infrastructure/observability.md)