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.

Three adult colleagues explain config reload
AI-generated teaching illustration, not an actual operating screen or performance record.

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=true

Publish 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

댓글