Skip to content
Zever
Home

Lifecycle

Follow one Zever process from config load to container close, and which failures belong to which phase.

3 min readEdit on GitHub

A Zever process runs four phases: load config, build the container, resolve services on first use, and close. Knowing the phase tells you whether a failure is fail-fast, retryable, or a shutdown problem.

config.LoadEnv -> container.New -> resolve on first use -> Close(ctx)
env one per c.DB(), c.Cache()... dependents
process retry on error first, leaves later

Build one container per process; no globals or init() wiring; container.New(nil) is valid.

  • 1. Load config: env merges into one Config; failures are InvalidOptionsError or unparsable values. Validation runs at load, so an empty adapter name or invalid option value fails in phase 1, not mid-request. An adapter name nobody registered passes load and fails on first resolution with UnknownAdapterError. config.FromEnv() / config.LoadEnv() reads instance-aware env vars; legacy flat format triggers warnLegacyConfigFile logging; see the Config reference for keys. Default() dev secrets belong in tests only.
  • 2. Build container: one Container owns future connections; New never dials and nil cfg is valid.
  • 3a. First resolution: adapter constructed and cached; on error the cache clears and the next call retries.
  • 3b. Steady serving: cached instance reused per accessor; per-call errors only, no rebuild.
  • 3c. Shared resolution: Scheduler() resolves Job() then Queue(); one shared connection.
  • 4. Close: resolved services shut down in order; errors.Join of per-service errors.

Config layers as defaults < env: env is best-effort (stray variables ignored). Validation runs at load, so an empty adapter name or invalid option value fails in phase 1, not mid-request. An adapter name nobody registered passes load and fails on first resolution with UnknownAdapterError. config.FromEnv() / config.LoadEnv() reads instance-aware env vars (<SERVICE>__<NAME>__<FIELD>); legacy flat format (<SERVICE>_<FIELD>) triggers warnLegacyConfigFile logging; see the Config reference for keys. Default() dev secrets belong in tests only.

Resolve inside handlers and reuse the cached instance. On error the cache clears, so the next call retries instead of pinning a broken handle. Job() dispatches over the resolved Queue; Scheduler() shares it.

Stop the HTTP server first, then close the container under a timeout parent (context.WithTimeout, about 10s). See Deployment for the full shutdown wiring.

Close never opens anything, so an unused service cannot fail shutdown. Dependents (scheduler, job, agent, rag) close first, then leaves (cache, queue), then db and shared pools, and each service gets a 5s bound without a parent deadline; shared instances close once.

  • Panic during close: container.ClosePanicError; the adapter panicked and shutdown continued.
  • Per-service timeout: container.CloseTimeoutError; the adapter exceeded its bound.
  • Several failures: errors.Join; inspect each with errors.As.

Error values name the service only, never secrets; display config through config.RedactedServices. Both close error types are values, so a pointer errors.As target never matches.

The process fails at boot before serving

An unparsable env value. The LoadEnv error names the service and field.

The first request fails but the next succeeds

A transient backend error cleared the cache. No fix needed unless it repeats.

Shutdown hangs

Close got a bare context while an adapter blocks. Use context.WithTimeout (about 10s).

errors.As for close errors never matches

A pointer target never matches a value type. Declare a value variable.

A goroutine leak is reported after a lifecycle test

A goroutine outlived Close (go test -tags=leak ./container/). Stop it in the adapter’s Close.