config

package
v0.1.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 30, 2026 License: Apache-2.0 Imports: 12 Imported by: 0

Documentation

Overview

Package config composes the configuration of a GatewayDNS engine into one document, loads it, validates it, and hands it out to a running server.

Composition, not redefinition

The direction of the dependency is fixed and deliberate: config imports logging, events and metrics; none of them imports config. Each package owns the options struct for its own subsystem and owns the code that validates it, and this package only assembles those structs into a single document and aggregates their verdicts.

The alternative — a central config package that declares every field and checks every bound — drifts. The check lives in one file and the thing being checked lives in another, so a new field arrives without a check, a bound is tightened in the constructor and not in the validator, and the error message stops describing the code. Keeping logging.Options.Validate next to the handler it configures is what stops that happening, and it is why an operator error here reports "logging: unknown format" in words the logging package chose.

Two of the three sections are not yet held to that rule. The events and metrics packages define BusOptions and Options without JSON tags and without a Validate method, because both are also constructed programmatically with fields — a clock.Clock, an OnDrop hook — that no configuration file can express. EventsOptions and MetricsOptions here are the file-shaped subset of each, and they carry the validation those packages have not yet grown. They should move into their owning packages when they do; the JSON shape is designed so that the move is invisible to a configuration file.

Only what exists

Config has three sections because this module has three configurable packages. The cache, the providers, the policy engine, the server and the REST API are later milestones and have no sections here — not empty ones, not placeholder ones. A configuration key is a compatibility promise from the moment an operator writes it into a file, and a promise invented before the thing it configures exists is a promise that will be wrong. See the comment on Config for how a milestone adds its own.

Why JSON

Because encoding/json is in the standard library and this module has no external dependencies and never will. That is the whole reason, and it is worth being honest about the cost: JSON has no comments, and operators, quite reasonably, want to write down why a value is what it is. It also has no trailing commas, which turns a one-line edit into a syntax error.

The mitigation is the signature of Load: it takes an io.Reader, not a path. A configuration file is not the only source of a Config, and an application that is free to take a dependency this module cannot can parse YAML, TOML, HCL or its own format, re-encode the result as JSON and hand it here — or skip the encoding entirely and build a Config value directly, then call Config.Validate. Nothing in this package requires that a configuration ever be a file. No YAML dependency will be added here.

One JSON caveat worth knowing: Load rejects unknown fields, but that check is a property of encoding/json's decoder and does not reach inside a type with its own UnmarshalJSON. logging.SamplingOptions has one, so a typo under "logging.sampling" is silently ignored where the same typo one level up is an error.

Hot reload: what it does and does not promise

Store is the reload primitive. It holds the current Config behind an atomic pointer, so Store.Load is lock-free and safe to call on the query path, and Store.Replace swaps a whole validated document in one atomic store. There is no window in which a reader sees half of the old configuration and half of the new one.

What that does NOT mean is that changing a value changes behaviour. The rule is mechanical:

  • A value read per request takes effect on the next request. A component that calls Store.Load() inside its handler is reconfigured the instant Replace returns.
  • A value captured at construction does not take effect at all until whatever captured it is rebuilt. A listening socket bound to a port, a handler chain built by logging.New, a histogram allocated with a fixed set of buckets — none of those change because a number in a file did.

Neither this package nor the Store can tell which is which, because it depends entirely on how a consuming package chose to read its options. So the obligation is placed on the consumer: every package that reads configuration must document, per setting, whether it is reloadable. logging already does the interesting half of this — logging.New returns a *slog.LevelVar precisely so that the level is reloadable without rebuilding the handler.

The reason to insist on this is that a half-working hot reload is worse than none. An operator who changes a value, sees no error, and gets no behaviour change concludes that the software is broken; the fix is not more machinery but a written answer to "which of these can I change while it is running".

Store.Subscribe exists for the settings that are not reloadable: it tells a component that configuration changed so that it can rebuild itself, or log that a restart is required. Delivery is coalescing — see Store.Subscribe — because a reloader only ever wants the newest document.

Index

Examples

Constants

View Source
const MaxEventQueueSize = 1 << 20

MaxEventQueueSize is the largest per-subscriber queue depth a configuration document may ask for.

The bound exists because the queue is allocated up front, one slot per subscriber per event: an operator who types an extra three zeroes gets a multi-gigabyte allocation at startup rather than an error, which is the worst way to learn about a typo. The events package itself imposes no cap, because a caller constructing a Bus in Go code is presumed to mean what it wrote.

View Source
const MaxMetricsBuckets = 128

MaxMetricsBuckets is the largest number of latency histogram boundaries a configuration document may ask for.

Buckets are not free and they do not exist once: the recorder allocates a histogram per shard and per tracked provider, so the boundary count is multiplied by roughly the core count times the provider cap before it becomes memory. It is also multiplied by every scrape, since a snapshot copies them. A latency histogram that is useful has ten to twenty boundaries; a hundred is already a mistake.

View Source
const SchemaVersion = 1

SchemaVersion is the document format this build understands.

It is written into every document Config.WriteTo produces and checked by Config.Validate, so that a document written by a newer build fails loudly here rather than being read with today's meanings for tomorrow's keys. A document that omits "version" is read as the current one, because requiring a version line in a three-line configuration file is friction with no payoff.

Variables

This section is empty.

Functions

This section is empty.

Types

type Config

type Config struct {
	// Version is the schema version of the document, not the version of the
	// software and not the reload generation reported by [Store.Version]. See
	// [SchemaVersion]; zero means "whatever this build is".
	Version int `json:"version"`

	// Logging configures the engine's logger. Owned and validated by the
	// logging package.
	Logging logging.Options `json:"logging"`

	// Events configures the event bus. See [EventsOptions] for why this type
	// lives here rather than in the events package.
	Events EventsOptions `json:"events"`

	// Metrics configures the in-memory recorder. See [MetricsOptions] for why
	// this type lives here rather than in the metrics package.
	Metrics MetricsOptions `json:"metrics"`
}

Config is a complete GatewayDNS configuration.

It is a plain value: copying it is legal, comparing it is not, and nothing in it is live. Build one with Default or Load, hand it to NewStore, and let components read it from there.

Adding a section in a later milestone

This struct has three sections because three packages in this module are configurable today. Later milestones — the cache, the providers, the policy engine, the server, the REST API — add their own, and the rule is one field here per owning package:

  • the owning package declares its own Options struct with lowercase, snake_case JSON tags and a zero value that is a working configuration;
  • the owning package declares Options.Validate, reporting every problem it finds through errors.Join rather than the first one;
  • this struct gains exactly one field of that type, and Config.Validate gains one line delegating to it;
  • Default gains one line, and Config.Clone gains a deep copy of whatever pointers, slices or maps the new section introduced.

Nothing about the shape of a future section is guessed at here. A key that exists is a key an operator will write into a file and expect to keep working.

Example

A config.Config is the argument list for the packages it configures. The seams each package needs but no file can express — a clock, a drop hook — are supplied here rather than in the document.

package main

import (
	"fmt"
	"io"

	"github.com/daboss2003/dns/config"
	"github.com/daboss2003/dns/events"
	"github.com/daboss2003/dns/logging"
	"github.com/daboss2003/dns/metrics"
)

func main() {
	cfg := config.Default()

	log, level, err := logging.New(io.Discard, cfg.Logging)
	if err != nil {
		panic(err)
	}
	rec := metrics.New(cfg.Metrics.Options())

	busOpts := cfg.Events.BusOptions()
	busOpts.OnDrop = func(k events.Kind) { log.Warn("event dropped", "kind", k) }
	bus := events.NewBus(busOpts)

	fmt.Println("level:", level.Level())
	fmt.Println("bus has subscribers:", bus.Subscribed())
	fmt.Println("queries recorded:", rec.Snapshot().Queries)
}
Output:
level: INFO
bus has subscribers: false
queries recorded: 0

func Default

func Default() *Config

Default returns the configuration an operator gets by writing "{}".

The defaults target a long-running service rather than a terminal session, on the grounds that a development machine has someone present to change settings and a deployed resolver does not.

func Load

func Load(r io.Reader) (*Config, error)

Load reads a JSON configuration document from r.

It starts from Default, decodes over it, and validates the result, so a document only has to say what it wants to change and every key it omits is the documented default rather than a zero value.

Unknown fields are an error

This is the part worth arguing for. A decoder that ignores what it does not recognise turns "blocklist_enabled" spelled with one l into silence: the file says the protection is on, the operator believes it is on, and it is off. There is no message anywhere and nothing to grep for. The typo is a deployment-shaped hole in a security control, so it stops the load and the error names the field.

The check comes from encoding/json and therefore does not reach inside a type with its own UnmarshalJSON — logging.SamplingOptions is the one such type in this document today.

A document containing anything after the top-level object is also rejected, because a second object is invisible: it changes nothing and reports nothing, which is the same failure in a different disguise.

Example

A document only has to say what it wants to change; everything it omits keeps the value from config.Default.

package main

import (
	"fmt"
	"strings"

	"github.com/daboss2003/dns/config"
)

func main() {
	const doc = `{
		"logging": {"level": "DEBUG", "format": "text"},
		"metrics": {"max_devices": 4096}
	}`

	cfg, err := config.Load(strings.NewReader(doc))
	if err != nil {
		panic(err)
	}

	fmt.Println("level:      ", cfg.Logging.Level)
	fmt.Println("format:     ", cfg.Logging.Format)
	fmt.Println("max devices:", cfg.Metrics.MaxDevices)
	fmt.Println("queue size: ", cfg.Events.QueueSize, "(default, not mentioned in the document)")
}
Output:
level:       DEBUG
format:      text
max devices: 4096
queue size:  256 (default, not mentioned in the document)
Example (UnknownField)

A misspelled key is refused rather than ignored, because a setting that is silently dropped is a setting an operator believes is in force.

package main

import (
	"fmt"
	"strings"

	"github.com/daboss2003/dns/config"
)

func main() {
	_, err := config.Load(strings.NewReader(`{"logging": {"levl": "debug"}}`))
	fmt.Println(err)
}
Output:
config: json: unknown field "levl"

func (*Config) Clone

func (c *Config) Clone() *Config

Clone returns a deep copy of c.

Store hands out a pointer to the live configuration and does not copy on read, because copying per read would put an allocation on the query path. The contract that makes that safe is that the pointed-to document is never mutated — and this method is how a caller that does want to change something gets a document it owns:

next := store.Load().Clone()
next.Logging.Level = slog.LevelDebug
err := store.Replace(next)

Every slice and pointer reachable from the copy is its own, so mutating the clone — including appending to Metrics.Buckets or writing through Logging.Sampling — cannot be observed through the original.

What is deliberately shared rather than copied is the seams: the clock, the slog.Leveler and the ReplaceAttr function hanging off the logging options are collaborators supplied by the program, not values supplied by an operator. Duplicating a *slog.LevelVar would give the clone a level nobody can retune. Clone of a nil *Config is nil.

func (*Config) Validate

func (c *Config) Validate() error

Validate reports every problem in c at once, joined with errors.Join.

Every one of them, not the first. An operator editing a configuration file against a validator that stops at the first failure gets one restart per mistake, so a five-mistake file costs five deployments and forty minutes; the same file validated this way costs one. errors.Join renders one problem per line, which is exactly what a refusal to start should print.

Each leaf is a *PathError naming the setting, so the message points at the line to edit rather than at the software.

Example

Every problem in a document is reported in one pass, each tagged with the setting that caused it, so a five-mistake file costs one restart and not five.

package main

import (
	"fmt"
	"strings"

	"github.com/daboss2003/dns/config"
)

func main() {
	const doc = `{
		"logging": {"format": "yaml", "syslog": {"facility": 200}},
		"events":  {"queue_size": -1},
		"metrics": {"buckets": [1.0, 0.5]}
	}`

	_, err := config.Load(strings.NewReader(doc))
	fmt.Println(err)
}
Output:
logging: unknown format "yaml": want "text", "json" or "syslog"
logging: syslog facility 200 out of range: RFC 5424 defines 0..23
events.queue_size: -1 is negative: write 0 to select the default of 256
metrics.buckets[1]: 0.5 does not exceed the previous boundary 1; boundaries must ascend and be distinct

func (*Config) WriteTo

func (c *Config) WriteTo(w io.Writer) (int64, error)

WriteTo writes c to w as indented JSON, followed by a newline.

This is the "show me what you actually loaded" primitive, and it is the first thing anyone wants when a deployment is not behaving: the effective configuration after defaults have been filled in and the file has been merged over them, which is frequently not the configuration anyone believed was running. Wire it to a flag, a signal or an admin endpoint.

The output is a valid input to Load, so a dump can be edited and fed back. Fields that are seams rather than settings — clocks, levelers, callbacks — are tagged json:"-" by their owning packages and do not appear.

WriteTo implements io.WriterTo.

Example

WriteTo answers "what is this process actually running", which is the first question worth asking when a deployment misbehaves.

package main

import (
	"os"
	"strings"

	"github.com/daboss2003/dns/config"
)

func main() {
	cfg, err := config.Load(strings.NewReader(`{"logging": {"level": "WARN"}}`))
	if err != nil {
		panic(err)
	}
	// Trimmed so the example output stays short; a real dump writes the whole
	// document, and what it writes is a valid input to Load.
	cfg.Metrics.Buckets = []float64{0.01, 0.1, 1}

	if _, err := cfg.WriteTo(os.Stdout); err != nil {
		panic(err)
	}
}
Output:
{
  "version": 1,
  "logging": {
    "level": "WARN",
    "format": "json",
    "add_source": false,
    "syslog": {
      "facility": 3,
      "hostname": "",
      "app_name": "gatewaydns",
      "proc_id": "",
      "sd_id": ""
    }
  },
  "events": {
    "queue_size": 256
  },
  "metrics": {
    "buckets": [
      0.01,
      0.1,
      1
    ],
    "max_devices": 0,
    "max_providers": 0
  }
}

type EventsOptions

type EventsOptions struct {
	// QueueSize is the default per-subscriber queue depth. Zero selects
	// events.DefaultQueueSize.
	//
	// Deeper is not better. The queue absorbs a burst; it does not prevent loss
	// under sustained overload, it only delays it while making what finally
	// arrives staler, and it multiplies by the number of subscribers. Size it
	// for the stall you expect to ride out.
	QueueSize int `json:"queue_size"`
}

EventsOptions is the file-shaped subset of events.BusOptions.

It exists because events.BusOptions carries a clock.Clock and an OnDrop callback, neither of which a JSON document can express, and because it has no JSON tags or Validate method of its own. Everything here maps directly onto a field of events.BusOptions; see EventsOptions.BusOptions. When the events package grows a validated, JSON-tagged options struct this type should be deleted in favour of it, and the JSON shape below is exactly what that struct should use so the swap is invisible to an existing configuration file.

func (EventsOptions) BusOptions

func (o EventsOptions) BusOptions() events.BusOptions

BusOptions converts o into the struct the events package actually takes.

The two fields a document cannot carry — the clock and the drop hook — are left zero for the caller to fill in, because they are collaborators supplied by the program rather than settings supplied by an operator:

opts := cfg.Events.BusOptions()
opts.Clock = clk
opts.OnDrop = func(k events.Kind) { rec.Dropped(k) }
bus := events.NewBus(opts)

func (EventsOptions) Validate

func (o EventsOptions) Validate() error

Validate reports every problem in o, joined with errors.Join.

type MetricsOptions

type MetricsOptions struct {
	// Buckets are the latency histogram boundaries in seconds, ascending. Empty
	// selects metrics.DefaultBuckets.
	//
	// Changing them makes new data incomparable with anything already exported,
	// so appending a boundary is almost always the right edit and replacing the
	// set is almost always not.
	Buckets []float64 `json:"buckets"`

	// MaxDevices caps how many distinct device identifiers are tracked
	// individually. Zero selects the metrics package default; a negative value
	// disables per-device tracking entirely, folding everything into
	// metrics.OverflowKey, which is the correct setting for a
	// privacy-sensitive deployment.
	//
	// The cap is a security control, not a tuning knob: device identifiers are
	// derived from client addresses, so anyone who can send a packet can invent
	// one, and an uncapped map is remote memory exhaustion that looks like a
	// feature.
	MaxDevices int `json:"max_devices"`

	// MaxProviders caps distinct upstream provider names the same way. Provider
	// names come from configuration and are few, so this is a backstop rather
	// than a defence.
	MaxProviders int `json:"max_providers"`
}

MetricsOptions is the file-shaped subset of metrics.Options.

It exists for the same reason as EventsOptions: metrics.Options carries a clock.Clock, has no JSON tags and has no Validate method. Everything here maps directly onto a field of metrics.Options; see MetricsOptions.Options. It should move into the metrics package when that package grows a validated options struct.

func (MetricsOptions) Options

func (o MetricsOptions) Options() metrics.Options

Options converts o into the struct the metrics package actually takes.

The Clock is left nil for the caller to fill in — metrics.New reads it as the system clock — because it is a collaborator, not a setting. The bucket slice is not copied: metrics.New sanitises it into storage of its own and never retains the argument.

func (MetricsOptions) Validate

func (o MetricsOptions) Validate() error

Validate reports every problem in o, joined with errors.Join.

Note what it does not do: it does not sort the boundaries or drop the bad ones. metrics.NewHistogram is tolerant because it is called with values a program computed, but a value an operator typed deserves to be questioned — silently repairing "-1" hides the fact that a line in the file does not mean what its author thought.

type PathError

type PathError struct {
	// Path is the dotted JSON path of the setting, for example "logging" or
	// "metrics.buckets[3]".
	Path string

	// Err is the underlying failure.
	Err error
}

PathError attaches the configuration path of the offending setting to a validation failure.

The path is the point. An operator reading "unknown format" has to go looking; an operator reading "logging: unknown format" knows which section, and "metrics.buckets[3]" knows which line. It is also machine-readable, so a management API rejecting a proposed configuration can highlight the field rather than printing a sentence:

var pe *config.PathError
if errors.As(err, &pe) { highlight(pe.Path) }

Every leaf error from Config.Validate is a *PathError, including those the logging package produced.

func (*PathError) Error

func (e *PathError) Error() string

Error renders the path and the failure.

A message that already begins with its own path is not prefixed a second time, so an error the logging package wrote as "logging: unknown format" is not rendered as "logging: logging: unknown format". The path is still available in PathError.Path either way, which is why the check is only ever cosmetic.

func (*PathError) Unwrap

func (e *PathError) Unwrap() error

Unwrap returns the underlying failure so that errors.Is and errors.As see through the path.

type Store

type Store struct {
	// contains filtered or unexported fields
}

Store holds the configuration a running engine is using and lets it be replaced without stopping anything.

The read path

Every component on the query path may want to consult configuration, so Store.Load must cost approximately nothing and must never contend. It is a single atomic pointer load: no mutex, no copy, no allocation, and no cache line that a reader writes to. A sync.RWMutex would be correct and would also be a disaster — at ten thousand queries a second across every core, the read-lock's own atomic increment on a shared word becomes the bottleneck, and the whole server would serialise on a value that changes once a month.

The price of not copying is a contract: the *Config returned by Load is shared by every reader and must be treated as read-only. Call Config.Clone to get one you may modify.

The write path

Store.Replace validates first, copies second, and swaps last. A document that fails validation changes nothing at all: the running configuration is still the last one that was known good, which is the only acceptable behaviour for a reload triggered by an operator editing a file at three in the morning. There is no intermediate state — a reader sees the old document or the new one, never a mixture — because what is swapped is one pointer.

What reload actually reconfigures

A Store makes the current configuration available; it does not make settings take effect. A value a component reads per request changes behaviour on the next request. A value a component captured when it was constructed does not change at all until that component is rebuilt, and the Store has no way to know which is which. See the package documentation, and see Store.Subscribe for how a component learns it needs rebuilding.

A Store starts no goroutines and owns nothing that must be closed. It is safe for concurrent use by any number of goroutines. It must be created by NewStore; the zero value has no configuration and Load would return nil.

Example

A config.Store is what a running engine reads from. Load is a single atomic pointer load, so a component on the query path can call it per request.

package main

import (
	"fmt"
	"log/slog"

	"github.com/daboss2003/dns/config"
)

func main() {
	store, err := config.NewStore(config.Default())
	if err != nil {
		panic(err)
	}

	fmt.Println("version:", store.Version(), "level:", store.Load().Logging.Level)

	// Change something. Start from the live document, clone it so the running
	// engine is never mutated through the pointer it handed out, and replace.
	next := store.Load().Clone()
	next.Logging.Level = slog.LevelDebug
	if err := store.Replace(next); err != nil {
		panic(err)
	}

	fmt.Println("version:", store.Version(), "level:", store.Load().Logging.Level)

	// A document that does not validate changes nothing at all.
	bad := store.Load().Clone()
	bad.Logging.Format = "yaml"
	fmt.Println("rejected:", store.Replace(bad))
	fmt.Println("version:", store.Version(), "level:", store.Load().Logging.Level)
}
Output:
version: 1 level: INFO
version: 2 level: DEBUG
rejected: logging: unknown format "yaml": want "text", "json" or "syslog"
version: 2 level: DEBUG

func NewStore

func NewStore(c *Config) (*Store, error)

NewStore returns a Store holding c.

c is validated before it is accepted, so a Store never contains a configuration that would have been refused on reload; a process cannot start in a state it could not return to. The stored document is a deep copy, so the caller may keep using and modifying c afterwards without reaching into the running engine through the pointer it handed over.

The initial configuration is generation 1. Generation 0 is therefore never live and is available to a caller as "nothing has been loaded yet".

func (*Store) Load

func (s *Store) Load() *Config

Load returns the current configuration.

It is one atomic pointer load: lock-free, wait-free, allocation-free and safe to call on the query path as often as it likes. Hold the result for the duration of one request rather than calling it per field, so that every decision a single request makes is made against one consistent document.

The returned *Config is shared with every other reader and MUST NOT be modified. Use Config.Clone to obtain a copy you own.

func (*Store) Replace

func (s *Store) Replace(c *Config) error

Replace validates c and installs it as the current configuration.

On failure nothing changes: the previous configuration stays live, the generation does not advance, and no subscriber is notified. That is the whole point of validating before swapping — a bad edit to a file must leave a running resolver answering queries exactly as it was, never unconfigured and never half-configured. The returned error is the aggregate from Config.Validate, so an operator sees every problem in the file at once.

On success the stored document is a deep copy of c, so the caller may keep modifying c without affecting the engine.

func (*Store) Subscribe

func (s *Store) Subscribe() (<-chan *Config, func())

Subscribe returns a channel that receives the new configuration after every successful Store.Replace, and a function that cancels the subscription.

It exists for the settings hot reload cannot apply on its own: a component that captured a value when it was built — a bound port, a handler chain, a histogram's buckets — needs to be told that the document changed so it can rebuild, or log that a restart is required. A component that reads its settings per request does not need this at all and should just call Store.Load.

Coalescing

The channel holds exactly one configuration. A subscriber that is busy when several reloads land receives only the most recent one. Delivering the intermediate ones would be worse than useless: a subscriber exists to reconfigure itself, and reconfiguring itself to a document that has already been superseded is work whose only possible outcome is being undone. Queueing them would make a slow subscriber not merely late but progressively more wrong, and would turn a burst of reloads — an editor saving a file in three writes — into a backlog that never drains. The consequence to design for is that this channel is a signal, not a log: it says that configuration changed, not how many times or what it passed through on the way.

The current configuration is NOT delivered on subscribe. A subscriber's first act is to read Store.Load anyway, and sending it here would make every subscriber's startup an indistinguishable-from-a-reload rebuild.

Cancelling

The returned function unsubscribes and closes the channel, so a subscriber written as a range loop terminates:

ch, cancel := store.Subscribe()
defer cancel()
for cfg := range ch {
	rebuild(cfg)
}

It is idempotent and safe to call from any goroutine, including concurrently with itself and with Store.Replace, so `defer cancel()` next to an explicit cancel on the shutdown path is fine. Failing to call it leaks one channel and one map entry for the lifetime of the Store, and makes every subsequent Replace do a little more work — a Store is not garbage collected while anything holds it.

Example

Subscribe is for the settings hot reload cannot apply on its own: a component that captured a value when it was built needs to be told to rebuild.

package main

import (
	"fmt"

	"github.com/daboss2003/dns/config"
)

func main() {
	store, err := config.NewStore(config.Default())
	if err != nil {
		panic(err)
	}

	changed, cancel := store.Subscribe()
	defer cancel()

	// A burst of reloads — an editor saving a file in pieces — coalesces into
	// one notification carrying the newest document.
	for _, size := range []int{512, 1024, 2048} {
		next := store.Load().Clone()
		next.Events.QueueSize = size
		if err := store.Replace(next); err != nil {
			panic(err)
		}
	}

	cfg := <-changed
	fmt.Println("rebuild against queue size:", cfg.Events.QueueSize)
	fmt.Println("reload generation:", store.Version())

	select {
	case <-changed:
		fmt.Println("a superseded document was queued")
	default:
		fmt.Println("nothing stale queued behind it")
	}
}
Output:
rebuild against queue size: 2048
reload generation: 4
nothing stale queued behind it

func (*Store) Version

func (s *Store) Version() uint64

Version returns the reload generation of the current configuration.

It starts at 1 and increases by one on every successful Store.Replace. It is not a hash and not a modification time: a reload that installs an identical document still advances it, and it never goes backwards within a process. That makes it the right thing to record alongside anything derived from configuration — a component can compare the generation it built against with the current one and rebuild only when it differs. It is also what belongs in an events.ConfigReloadedEvent.

A generation read here and a document read from Store.Load are not sampled atomically together. If you need them consistent, read the version, then the config, then the version again, and retry if it moved — or simply subscribe.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL