Config Hot Reload in Practice: config.ini/YAML Without Restarting
TECH INSIGHT / EXPLAINED
Hot reload is a configuration transaction: read a candidate, validate it as a whole, then publish one consistent version. Editing a file should never expose half-old, half-new risk settings to an order decision.
Define units before choosing INI or YAML
In this example 50 basis points means 0.50%, the interval is in milliseconds, and MaxNotional is an integer amount in the account currency. A parser must reject missing fields, invalid numbers and unknown keys according to an explicit policy. INI and YAML parsing need an appropriate library; the Go standard library does not provide these parsers. Keep secrets out of the example configuration.
[COMMON]
GapEvalIntervalMs=1000
EntryDebounceSec=30
[PAIR_01]
EntryGapBps=50
MaxNotional=100000
Enabled=truePublish a validated, immutable snapshot
This runnable Go example isolates the commit step; it does not implement the INI parser. One reload goroutine is the sole writer. The initial configuration must succeed before workers start. A rejected update leaves version 1 active. A reader copies one version at the beginning of a decision instead of loading each field separately.
package main
import (
"fmt"
"sync/atomic"
)
type Config struct {
Version uint64
EntryGapBps int64
MaxNotional int64 // integer account-currency units
Enabled bool
}
var active atomic.Pointer[Config]
// One reload goroutine owns all calls to install.
// Config has value fields only; published objects are immutable.
func install(next Config) error {
if next.Version == 0 || next.EntryGapBps <= 0 || next.MaxNotional <= 0 {
return fmt.Errorf("invalid config")
}
if old := active.Load(); old != nil && next.Version <= old.Version {
return fmt.Errorf("version must increase")
}
active.Store(&next) // next is a private value copy
return nil
}
func snapshot() (Config, bool) {
p := active.Load()
if p == nil { return Config{}, false }
return *p, true
}
func main() {
if err := install(Config{1, 50, 100000, true}); err != nil {
panic(err) // no valid startup configuration
}
err := install(Config{2, 60, 0, true})
cfg, ok := snapshot()
fmt.Println("rejected:", err != nil, "ready:", ok, "version:", cfg.Version)
}Output: rejected: true ready: true version: 1. The configuration contains only value fields. If maps, slices or pointers are added, clone their mutable contents before publication and never mutate a published snapshot. Copying only a slice header is insufficient.
A failed update is visible, not partly applied
Reject the entire candidate if any required pair or cross-field constraint fails. Silently skipping one invalid pair can orphan an existing position. Log the candidate version and a sanitized reason, expose the last accepted version and notify the operator. Keeping the last valid configuration is appropriate for an ordinary editing error, but revoked credentials or a safety shutdown need their own explicit fail-closed policy.
Separate parameters from resource changes
Changing a threshold can take effect at the next decision boundary. Changing symbols, accounts or subscriptions needs a coordinated transition: pause new entries, reconcile outstanding orders, update subscriptions and wait for fresh data. Disabling a pair should not disable management of its existing position. In a polling implementation, use a context-aware loop and stop the ticker on exit; with file watching, debounce editor events and read only a complete candidate. Atomic replacement behavior and crash durability depend on the operating system and filesystem, so test the actual deployment path.
Documentation and next steps
Content and examples reviewed: 2026-09-29
댓글
댓글 쓰기