A kit

Alkalmazás-életciklus

Az app csomag: signal-vezérelt graceful shutdown, amin minden gpsystem bináris ül.

PHP-ban a folyamat életciklusa nem a te problémád volt: a php-fpm elindította a workert, lefuttatta a kérést, és ha deployoltál, a supervisor gondoskodott a cseréről. Go-ban a binárisod maga a szerver: neki kell figyelnie a SIGTERM-et, kivárnia az in-flight kéréseket, lezárnia a connection poolt és kiflusholnia a telemetriát, mielőtt kilép. Ez tipikusan az a ~60 sor boilerplate, amit minden service main.go-jába bemásolnak, és amit mindig kicsit másképp rontanak el.

A kit ezt egyszer írja meg, az app csomagban, és ettől a téma megint nem-téma: a generált projektekben a graceful shutdown egyszerűen ott van, és most láthatod is, mi történik.

app.Run és a Params

Az app csomag engine-agnosztikus: se HTTP-t, se más transzportot nem ismer. Egyetlen belépési pontja van:

// Closer egy nevesített cleanup-függvény, graceful shutdownkor fut.
type Closer struct {
    Name string
    Fn   func(context.Context) error
}

// Params egy processzfutást ír le.
type Params struct {
    ListenAddr      string                          // csak a startup-loghoz
    ShutdownTimeout time.Duration                   // az in-flight drain kerete
    Serve           func() error                    // blokkoló listen; goroutine-ban fut
    Shutdown        func(context.Context) error     // nincs új kérés, in-flight drain
    Closers         []Closer                        // rendezett cleanup-lista
    Flush           func(context.Context) error     // telemetria-flush, legutolsóként
}

func Run(ctx context.Context, p Params) error

A Run blokkol, amíg a processz le nem áll, és a következő gépezetet hajtja:

  1. signal.NotifyContext(ctx, os.Interrupt, syscall.SIGTERM): a SIGINT/SIGTERM a contextet cancelezi. Mivel a szülő ctx-ből származik, a szülő context cancelje is shutdownt vált ki: tesztben így állítasz le egy futó service-t signal nélkül.
  2. Serve goroutine-ban indul. Ha magától hibával tér vissza (pl. foglalt a port), a Run app: listen: hibával lép ki, de a closerek és a flush ilyenkor is lefutnak.
  3. Signalra: a Run visszaállítja az alapértelmezett signal-kezelést (ezért egy második Ctrl-C azonnal, kegyelem nélkül öli a processzt), majd meghívja a Shutdown-t egy ShutdownTimeout-os contexttel. Itt drainelnek az in-flight kérések.
  4. Cleanup saját kerettel: a closerek és a flush egy friss, szintén ShutdownTimeout-os contextet kapnak. Ha a HTTP-drain felélte a teljes keretet, a cleanup akkor sem nulla idővel indul: a pool-lezárás és a telemetria-export nem esik áldozatul egy lassú kérésnek. (Mindkét shutdown-context context.WithoutCancel-lel válik le a szülőről: egy már cancelezett szülő nem vágja rövidre a rendezett leállást.)
  5. Closerek LIFO sorrendben, majd legvégül a Flush.
  6. Tiszta leállásnál nil a visszatérési érték; a részhibák errors.Join-nal gyűlnek, tehát egy hibázó closer nem nyeli el a többiét.

Miért LIFO, és miért utolsó a flush?

A closerek fordított regisztrációs sorrendben futnak, mert a függőségek így záródnak helyes irányban: amit előbb nyitottál (pl. a pgx pool), arra még szüksége lehet annak, amit később építettél rá (pl. egy rá támaszkodó consumer). Amit utoljára nyitottál, azt zárod először, pontosan ahogy a defer működik egy függvényen belül.

A telemetria-flush pedig azért a legutolsó, mert a closerek maguk is bocsátanak ki telemetriát: egy pool-lezárás loga, egy cleanup közben nyitott span csak akkor jut el az OTLP-endpointig vagy a Sentrybe, ha az exporterek még élnek. Ha a flush a closerek előtt futna, a leállás utolsó másodpercei vakfoltba esnének.

Ki ül rajta?

Mindhárom chassis ugyanezt az app.Run-t tölti ki, csak a Params tartalma más:

Paramsserver.Runworker.Runrealtime.Run
Servesrv.ListenAndServeasynq szerver + outbox relay + ütemezőa kapcsolatokat fogadó centrifuge node
Shutdownsrv.Shutdownloopok leállítása, in-flight taskok drainelésenode.Shutdown, tartott kapcsolatok drainelése
Closersa WithCloser opcióka WithCloser opcióka WithCloser opciók
Flushtelemetry.Setup shutdown-függvényeugyanazugyanaz

Mindhárom a telemetry.Setup-pal indít, és a visszakapott shutdown-függvényt adja át Flush-ként, így a "Sentry és OTel flush a legvégén" garancia bináris-típustól függetlenül azonos. A workernél az in-flight drain mást jelent: a WORKER_SHUTDOWN_TIMEOUT alatt be nem fejezett taskokat az asynq visszateszi a sorba, és később újra kézbesíti (at-least-once); a realtime-nál pedig azt, hogy minden tartott WebSocket-kapcsolat reconnect-jogosult close-kódot kap, mielőtt a processz kilép (Realtime).

Ha egy saját, negyedik chassis-t építenél (pl. egy gRPC-szervert), ugyanígy az app.Run-ra teheted: a csomagnak nincs HTTP-függősége.

A shop main.go-ja

A gyakorlatban az életciklushoz egyetlen ponton nyúlsz hozzá: closert regisztrálsz mindenre, amit a main-ben nyitottál. A shop mintaalkalmazás belépési pontjában ez a pgx pool:

cmd/shop/main.go
pool := pg.MustNewPool(ctx, cfg.DB)

err := server.Run(ctx, cfg.Server, func(r chi.Router) error {
    // ... modulregisztráció ...
    return nil
}, server.WithCloser("pgxpool", func(context.Context) error {
    pool.Close()
    return nil
}))

Egy SIGTERM érkezésekor tehát a sorrend: a shop nem fogad több kérést → a folyamatban lévő PlaceOrder még lefut (a tranzakciójával és az outbox-írásával együtt) → a pgxpool closer lezárja a poolt → a telemetria kiflusholja a kérés spanjeit. Kubernetes alatt ez pontosan az a viselkedés, amit egy rolling deploy elvár: a ShutdownTimeout-ot (SHUTDOWN_TIMEOUT, default 10s) érdemes a pod terminationGracePeriodSeconds-énél rövidebbre hangolni.

A closer neve ("pgxpool") nem dísz: hibázó closernél a Runapp: closer "pgxpool": ... formában adja vissza a hibát, így a logból azonnal látszik, melyik cleanup bukott el.

Mit jelent ez a mindennapokban

  • Nem írsz signal-kezelést. Soha. A server.Run / worker.Run / realtime.Run hívása maga az életciklus.
  • A main-ben nyitott erőforrásokhoz closer jár: a generátorok a pgx poolét maguktól bekötik, a sajátjaidat (pl. egy Valkey-klienst) te adod hozzá WithCloser-rel, a nyitás sorrendjében.
  • A dupla Ctrl-C menekülőútvonal marad: ha fejlesztés közben beragadna egy drain, a második signal azonnal kilő.

A HTTP-oldali lépéssort (telemetria → router-építés → regisztráció → listen → shutdown) a szerver oldal mutatja be; a worker-oldali drain részleteit a Worker oldal; a kapcsolat-drainelés részleteit a Realtime oldal.

Copyright © 2026