Documentation
¶
Overview ¶
Package di provides a generic, lock-free configuration loader and a thin Container wrapper for the two-step bootstrap pattern used by services built on core.
Configuration sources are loaded in this order, from lowest to highest priority:
defaults < yaml file < etcd static < etcd dynamic < env vars
env vars always win over every other source. Dotted keys map to underscore env vars, so http.frontend.port reads from HTTP_FRONTEND_PORT.
Reading values is done via the package-level generic functions Get[T], GetOrDefault[T], Live[T] and LiveOrDefault[T]; the *Config struct exposes only the immutable bootstrap fields (GetAppEnv, GetServiceName) captured at NewConfig time.
The dynamic etcd watcher composes the snapshot per-path (each path's last parsed map is stored separately and merged on every update), fixing the silent stale-key bug inherited from viper-remote: a PUT that drops a key now correctly removes it from the snapshot.
Etcd access uses the native etcd v3 client (go.etcd.io/etcd/client/v3) behind the small etcdKV interface — no viper, no viper-remote. fakeEtcdKV drives dynamic-update paths in unit tests without a real etcd cluster.
Path ownership: the caller passes full etcd keys via the app.config.etcd.{static,dynamic}.paths env vars. core/di does not prepend the service name or app env to the path. This keeps the loader storage-agnostic — vanilla etcd accepts arbitrary keys, while Elara and other layered stores require their own prefix conventions (e.g. "/{namespace}/...") which the deployment system is responsible for templating into the env var.
Tunables (env vars):
- APP_CONFIG_ETCD_REQUEST_TIMEOUT: Go-duration string ("5s", "500ms") bounding every etcd Get call. Defaults to 5s. Invalid value fails startup with ErrInvalidEtcdRequestTimeout.
Sentinel errors are split across files for locality:
- This file: ErrEmptyConfigPaths, ErrEtcdEndpointRequired, ErrEmptyEtcdStaticPaths, ErrEmptyEtcdDynamicPaths, ErrEtcdKeyNotFound, ErrServiceNameRequired.
- etcdclient.go: ErrInvalidEtcdRequestTimeout.
- loader_file.go: ErrNotRegularFile, ErrConfigFileTooLarge.
- loader_etcd.go: ErrUnknownConfigType.
Index ¶
- Constants
- Variables
- func Get[T ValueType](cfg *Config, key string) T
- func GetOrDefault[T ValueType](cfg *Config, key string, def T) T
- func Live[T ValueType](cfg *Config, key string) func() T
- func LiveOrDefault[T ValueType](cfg *Config, key string, def T) func() T
- type Config
- type Container
- type Rollback
- type ServicesInit
- type ValueType
Constants ¶
const AppEnvDev = "dev"
AppEnvDev is the default value for app.env when no environment is set via env or yaml.
Variables ¶
var ( ErrEmptyConfigPaths = errors.New("config paths is set but empty after parsing") ErrEtcdEndpointRequired = errors.New("etcd endpoint is required when using etcd") ErrEmptyEtcdStaticPaths = errors.New("etcd static paths is set but empty after parsing") ErrEmptyEtcdDynamicPaths = errors.New("etcd dynamic paths is set but empty after parsing") ErrEtcdKeyNotFound = errors.New("etcd key not found") ErrServiceNameRequired = errors.New("service name is required when etcd paths are configured") )
Sentinel errors returned by NewConfig at startup.
var ( ErrNotRegularFile = errors.New("config path is not a regular file") ErrConfigFileTooLarge = errors.New("config file exceeds size limit") )
Sentinel errors exposed by the file loader.
var ErrInvalidEtcdRequestTimeout = errors.New("invalid etcd request timeout")
ErrInvalidEtcdRequestTimeout is returned by NewConfig at startup when the value of APP_CONFIG_ETCD_REQUEST_TIMEOUT does not parse as a Go duration (e.g. "5s", "500ms"). Fail-fast — startup is the right place to surface a malformed env var.
var ErrUnknownConfigType = errors.New("unknown config type")
ErrUnknownConfigType is returned by parseConfigBytes when the configType argument is not one of the supported formats (yaml, yml, json).
Functions ¶
func Get ¶ added in v1.5.0
Get returns the value for key as T. If key is absent or the value cannot be coerced to T, the zero value of T is returned. Use Get for one-shot reads at startup; values pulled from etcd dynamic updates after startup will not be reflected without re-reading.
func GetOrDefault ¶ added in v1.5.0
GetOrDefault returns the value for key as T, or def if the key is absent OR the value cannot be coerced to T.
func Live ¶ added in v1.5.0
Live returns a closure that reads the current value of key on every invocation. Use Live for hot-reloadable values (rate limits, feature flags) that may change at runtime via etcd dynamic.
Note: env vars override etcd dynamic. A key bound in env is locked for the process lifetime and Live() will keep returning the env value even if etcd PUTs a different value.
func LiveOrDefault ¶ added in v1.5.0
LiveOrDefault is Live with a fallback when the key is absent or the value can't be coerced.
Types ¶
type Config ¶
type Config struct {
// contains filtered or unexported fields
}
Config holds the resolved configuration store plus the immutable bootstrap fields (app env, service name) captured at NewConfig time.
Reads happen through the package-level generic functions di.Get[T] / di.GetOrDefault[T] / di.Live[T] / di.LiveOrDefault[T]. Reads are lock-free; the underlying snapshot is replaced atomically by the dynamic-watch goroutine when an etcd PUT arrives.
func NewConfig ¶
NewConfig is the production constructor. It loads the configuration from yaml files, etcd static, and etcd dynamic in priority order, publishes the initial snapshot, and starts the dynamic-update watcher in a background goroutine bound to ctx.
Behaviour:
- app.env defaults to AppEnvDev when not set in env.
- app.service.name is required if any etcd paths are configured.
- app.config.etcd.endpoint is required if any etcd paths are configured.
- The static-load etcd client is opened and closed within the load; the dynamic watcher owns its own client and closes it on ctx cancellation.
func (*Config) GetAppEnv ¶
GetAppEnv returns the application environment captured at NewConfig time. The value is immutable for the process lifetime. Surfaces the service's bootstrap identity (logs, metrics, traces); it is no longer used to build etcd paths — see the package doc on path ownership.
func (*Config) GetServiceName ¶
GetServiceName returns the service name captured at NewConfig time. Immutable for the same reasons as GetAppEnv.
type Container ¶
type Container[C, S any] struct { Config C Services S }
Container bundles a typed Config and a typed Services graph produced by the application. It is a thin convenience wrapper over the two-step bootstrap pattern: initConfig, then initServices.
Container does NOT track teardown for the happy path. Every component that holds resources must implement lifecycle.Resource (see github.com/sergeyslonimsky/core/lifecycle) and be registered with app.App — the container is only responsible for construction, not runtime teardown.
Typical usage:
container, err := di.NewContainer(ctx,
myapp.NewConfig,
myapp.NewServices,
)
if err != nil {
log.Fatal(err)
}
cfg, svc := container.Config, container.Services
a := app.New()
svc.RegisterResources(a) // add Resources (db, redis, ...)
a.Add(svc.HTTPServer) // add Runners
log.Fatal(a.Run())
func NewContainer ¶
func NewContainer[C, S any]( ctx context.Context, initConfig func(context.Context) (C, error), initServices ServicesInit[C, S], ) (*Container[C, S], error)
NewContainer invokes initConfig first, then threads the resulting config into initServices. Returns the populated container or the first error encountered.
Failure semantics: if initServices returns an error alongside a non-nil Rollback, the Rollback is invoked before NewContainer returns. Rollback errors are joined with the init error via errors.Join for visibility.
Use ServicesInit for initServices when you construct resources (db, redis, otel providers) that need to be released on partial-failure startup paths.
type Rollback ¶ added in v1.5.0
Rollback releases resources constructed by a ServicesInit call that failed midway. It is NOT a shutdown hook — runtime teardown of healthy resources happens through lifecycle.Resource registered with app.App. NewContainer invokes Rollback only when ServicesInit returns a non-nil error and a non-nil Rollback alongside it; on success the Rollback is discarded.
type ServicesInit ¶ added in v1.3.0
type ServicesInit[C, S any] func( ctx context.Context, cfg C, ) (services S, rollback Rollback, err error)
ServicesInit is the signature expected by NewContainer. It builds the Services graph and MAY return a Rollback that releases resources constructed so far. If err is non-nil, NewContainer invokes the returned Rollback (if any) before propagating the error. On success the Rollback is discarded — runtime teardown is handled by app.App via lifecycle.Resource.
Return a nil Rollback when the initializer has nothing to release on partial construction.