Documentation
¶
Overview ¶
Package idempotencycfg assembles an idempotency.Manager from environment configuration.
It selects nothing itself. What it configures is two other seams — a cachecfg.Config for the record store and a distributedlockcfg.Config for the lock guarding the claim — and the guarantee holds only if both are chosen for a fleet: the memory cache is per-process, so replicas would not see each other's records, and the noop locker acquires unconditionally, which leaves replay working while quietly removing mutual exclusion.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func NewManager ¶
func NewManager[T any]( ctx context.Context, cfg *Config, db database.Client, opts ...Option, ) (*idempotency.Manager[T], error)
NewManager builds a Manager for T from configuration.
T must be supplied explicitly — NewManager[Receipt](...) — because this constructor builds the record store itself, so nothing in the argument list mentions T. That single annotation is the whole cost: idempotency.Option carries no type parameter, so the options passed here need none.
db is required only when the lock provider is postgres; pass nil otherwise.
The transport adapters have their own NewManager, and callers wiring up HTTP or gRPC should prefer those: they apply the recordable rule for that transport, which this one cannot know. Reach for this when the guarded work is neither — a job runner, a queue consumer.
func RegisterManager ¶
RegisterManager registers an *idempotency.Manager[T] with the injector. It is generic because a Manager stores results of one concrete type; each result type the application replays is registered separately.
Prerequisites: *Config must be registered in the injector before the Manager is invoked. A database.Client is only required when the config's lock provider is postgres — which is what NewManager documents, and what this used to contradict by invoking one unconditionally: a redis-locked container that had registered no database panicked at build.
Types ¶
type Config ¶
type Config struct {
// KeyPrefix namespaces store and lock keys.
KeyPrefix string `env:"KEY_PREFIX" json:"keyPrefix,omitempty" yaml:"keyPrefix,omitempty"`
// Lock configures the locker guarding the claim. It has no safe default —
// the noop provider acquires unconditionally, which leaves replay working
// while quietly removing mutual exclusion.
Lock distributedlockcfg.Config `env:",init" envPrefix:"LOCK_" json:"lock,omitzero" yaml:"lock,omitempty"`
// Cache configures the record store. Use the redis provider in
// production: the memory provider is per-process, so replicas would not
// see each other's records, and it holds a long TTL entirely in this
// process's heap.
Cache cachecfg.Config `env:",init" envPrefix:"CACHE_" json:"cache,omitzero" yaml:"cache,omitempty"`
// TTL is how long a completed record stays replayable.
TTL time.Duration `env:"TTL" json:"ttl,omitempty" yaml:"ttl,omitempty"`
// InFlightTTL bounds how long a claim survives without completing. It is a
// deadline for the guarded work, not a tuning knob: set it above the worst
// case, since anything slower can produce a duplicate effect.
InFlightTTL time.Duration `env:"IN_FLIGHT_TTL" json:"inFlightTTL,omitempty" yaml:"inFlightTTL,omitempty"`
// MaxKeyLength is the longest client key accepted.
MaxKeyLength int `env:"MAX_KEY_LENGTH" json:"maxKeyLength,omitempty" yaml:"maxKeyLength,omitempty"`
// FailOpen runs the work when the record store cannot be read, instead of
// refusing the request. It trades the guarantee for availability and is
// wrong wherever a duplicate effect costs money — see
// idempotency.StoreFailurePolicy.
FailOpen bool `env:"FAIL_OPEN" json:"failOpen,omitempty" yaml:"failOpen,omitempty"`
// contains filtered or unexported fields
}
Config assembles an idempotency.Manager from environment configuration.
func (*Config) EnsureDefaults ¶
func (cfg *Config) EnsureDefaults()
EnsureDefaults fills in zero fields.
func (*Config) ValidateWithContext ¶
ValidateWithContext validates a Config struct.
The nested configs are validated through validation.By closures because ozzo dereferences a struct-value field before checking ValidatableWithContext, so it would otherwise skip them.
type Option ¶
type Option func(*options)
Option configures how NewManager assembles its manager.
The observability dependencies are options rather than parameters because every one of them is genuinely optional: an absent logger logs nowhere, an absent tracer provider traces nowhere, and an absent metrics provider records nothing. Requiring them positionally made a caller that wanted none of the three name all three anyway, usually as noops.
func WithLogger ¶
WithLogger attaches a logger. An absent logger logs nowhere.
func WithManagerOptions ¶
func WithManagerOptions(opts ...idempotency.Option) Option
WithManagerOptions passes opts to the Manager, which applies them after the options it derives from configuration — so a caller can override anything.
They cannot be a second variadic on NewManager: Go allows one per function, and that slot is what makes the observability optional.
func WithMetricsProvider ¶
WithMetricsProvider attaches a metrics provider. An absent provider records nothing.
func WithPillars ¶
func WithPillars(p *observability.Pillars) Option
WithPillars attaches a logger, tracer provider, and metrics provider in one go, for the common case where a caller has already built them together. A nil Pillars attaches nothing.
It is applied in order with the individual options, so a caller can hand over its pillars and then override one of them.
func WithTracerProvider ¶
WithTracerProvider attaches a tracer provider, enabling spans on the instrumented operations. An absent tracer provider traces nowhere.