Hedronite · Synthesis Lesson · Polyglot-Dev · Go · Fri 2026-06-19

Go's Finite-State Machine and atomic.Pointer for a Regime-State Engine

Hysteresis dwell-timers, lock-free reads, and the transition-broadcast fan-out.

Lesson Class: Dev (Go · Backend Stack)
Refraction: The day's regime-detection Ops lesson, rendered in Go concurrency primitives
Word Count: ~2,510
Paired Ops: Regime Detection and Persistence Estimation for Adversarial Markets
Paired Cert: Terraform as the Operate-and-Recover Provisioning Foundation (Week 5)
Grounding: Learning Go (Bodner) Ch 10 Concurrency pp 223-249 · Ch 6 Pointers pp 144-145
Discipline: ROD v3 · clean code blocks (no inline comments)

Many hands read the regime each second. One hand changes it each week. Which primitive fits that shape?

A mutex protects a value many writers contend for. The published regime has the opposite profile: a crowd of readers, a lone writer, and writes that are rare. Reaching for a mutex here makes every reader queue behind a lock that is almost never held for writing. Go has a primitive built for exactly this asymmetry, and the day's Ops lesson handed us the design that needs it.

Today's Adversarial-Markets lesson specified a regime service with a strict separation: a filter that emits a twitchy posterior every bar, and a publication layer that changes the official regime label only when a Two-Threshold Dwell clears. This lesson builds the publication layer in Go. Three primitives carry it: a finite-state machine that defines the legal transitions, an atomic.Pointer that publishes the current regime for lock-free reads, and a time.Timer plus a channel that arm the dwell and broadcast the change.

§ IFrame

Bodner's pointer chapter draws the line the design rests on. A pointer is the address of a value, and two pieces of code holding the same pointer see the same value change under them (Learning Go, Ch 6, pp 144-145). That sharing is what makes atomic.Pointer work: the writer swaps the address the readers dereference, and a reader either sees the whole old regime or the whole new one, never a half-written mix. The atomic swap is the publication.

The concurrency chapter draws the second line. Bodner's rule of thumb is to reach for channels when goroutines need to coordinate or pass ownership of data, and for the sync package's primitives, including the atomic operations, when you are guarding access to a shared field that many goroutines read (Ch 10, pp 223-249). The regime engine needs both, for two different jobs. The published value is shared state that the gate and the kill-switch read constantly, so it wants an atomic. The transition event is a thing that happens once and must reach every interested goroutine, so it wants a channel. Picking the right tool per job is the lesson under the lesson.

§ IILanguage Idiom: The State Machine as a Typed Transition Table

A regime is one of a closed set, so it models cleanly as a Go integer constant generated by iota. The legal transitions form a map, and an illegal transition is a map miss, which the engine can refuse rather than perform.

type Regime int

const (
	RiskOn Regime = iota
	RiskOff
	Choppy
	HighVol
)

func (r Regime) String() string {
	return [...]string{"risk-on", "risk-off", "choppy", "high-vol"}[r]
}

type Published struct {
	Regime    Regime
	Posterior float64
	EnteredAt time.Time
	Sequence  uint64
}

The Published struct is the unit the writer swaps and the readers dereference. It carries the regime, the posterior that was on it when it was published, the time it was entered, and a monotonically rising sequence number. The sequence matters: a reader that caches a regime can compare sequence numbers to know whether what it holds is stale without comparing the regimes themselves. Bundling the metadata into one immutable struct, published by a single pointer swap, is what lets a reader take a consistent snapshot of regime-plus-context in one atomic load.

The Regime values are deliberately a closed enum rather than strings. A string regime invites typos and silent mismatches; the typed constant makes an illegal regime a compile error, and the transition table can be exhaustive because the set is known.

§ IIICode Worked Example: The Engine

The engine holds the published pointer and the dwell machinery. Readers touch only the atomic. The single writer goroutine owns everything else.

type Engine struct {
	current     atomic.Pointer[Published]
	seq         atomic.Uint64
	transitions map[Regime]map[Regime]bool
	subs        []chan Published
	mu          sync.Mutex
}

func NewEngine(initial Regime, table map[Regime]map[Regime]bool) *Engine {
	e := &Engine{transitions: table}
	first := &Published{Regime: initial, Posterior: 1.0, EnteredAt: time.Now(), Sequence: 0}
	e.current.Store(first)
	return e
}

func (e *Engine) Current() Published {
	return *e.current.Load()
}

Current is the whole reader-facing surface, and it never blocks. A reader calls Load on the atomic pointer, dereferences it, and returns a copy of the struct. A thousand goroutines can call Current in the same microsecond and none of them waits on any other, because an atomic load is not a lock. This is the asymmetry paying off: the cost the design pushes onto the rare writer buys the frequent reader a wait-free read.

The writer applies a transition, but only a legal one. The transition table refuses a move the regime model never makes, which catches a bug in the caller before it corrupts the published state.

func (e *Engine) commit(to Regime, posterior float64) (Published, error) {
	prev := e.current.Load()
	if !e.transitions[prev.Regime][to] {
		return Published{}, fmt.Errorf("illegal transition %s -> %s", prev.Regime, to)
	}
	next := &Published{
		Regime:    to,
		Posterior: posterior,
		EnteredAt: time.Now(),
		Sequence:  e.seq.Add(1),
	}
	e.current.Store(next)
	return *next, nil
}

The swap is a single Store. Before it, every reader sees the old regime whole; after it, every reader sees the new regime whole. There is no instant where a reader observes the new regime with the old entry-time, because the struct is replaced by pointer in one operation rather than field by field. The sequence increment uses its own atomic so the number is correct even if a future design admits more than one writer.

§ IVConnection to Today's Ops Lesson: The Dwell as a Timer

The Ops lesson's Two-Threshold Dwell is the heart of the writer's logic, and Go's time.Timer renders it directly. The writer watches the filter's posterior stream. When the posterior on a candidate regime first crosses the entry threshold, the writer arms a timer for the dwell duration. If the posterior stays above the exit threshold until the timer fires, the writer commits the transition. If the posterior falls back below exit before the timer fires, the writer stops the timer and the candidate is abandoned. The wobble that the Ops lesson warned about never reaches commit.

func (e *Engine) Run(ctx context.Context, posteriors <-chan Reading, entry, exit float64, dwell time.Duration) {
	var pending *time.Timer
	var candidate Regime
	disarm := func() {
		if pending != nil {
			pending.Stop()
			pending = nil
		}
	}
	for {
		select {
		case <-ctx.Done():
			disarm()
			return
		case r := <-posteriors:
			cur := e.current.Load().Regime
			top, p := r.Argmax()
			switch {
			case top == cur:
				disarm()
			case p >= entry && pending == nil:
				candidate = top
				pending = time.AfterFunc(dwell, func() { e.fire(candidate, p) })
			case pending != nil && (top != candidate || p < exit):
				disarm()
			}
		}
	}
}

The single select loop is the writer goroutine the whole design assumed. It reads the posterior stream, holds at most one pending timer, and cancels that timer the moment the candidate weakens or the regime returns to where it started. Bodner's concurrency chapter is explicit that a select over a cancellation context and a work channel is the idiomatic shape for a long-lived goroutine that must stop cleanly, and the ctx.Done() case is what lets a redeploy tear the engine down without leaking the writer (Ch 10, pp 240-246). The dwell logic lives entirely inside this one goroutine, so there is no lock around the candidate or the timer: only this goroutine ever touches them.

One Subtlety time.AfterFunc runs its callback on its own goroutine, so fire must take the lock-free publication path rather than mutate writer-local state. The callback calls commit through a small fire wrapper that logs and drops an illegal-transition error, because by the time the timer fires the world may have moved and the once-legal transition may no longer be.

§ VPrior-Lesson Reach: The Broadcast Fan-Out

The June 9 lesson published a healthy backend set with an atomic.Value swap so the reverse proxy could read it without a lock. The regime engine reuses that exact move for the published regime, upgraded to the typed atomic.Pointer that newer Go offers over the untyped atomic.Value. The continuity is the point: a value that many goroutines read and one rarely writes is published by atomic swap, whether the value is a backend set or a market regime.

What the regime engine adds is the broadcast. A backend-set change is silent; readers pick it up on their next request. A regime change is an event some consumers must act on the instant it happens, the kill-switch most of all. So commit is wrapped by a fire that, after the swap, fans the new Published out to every subscribed channel.

func (e *Engine) Subscribe() <-chan Published {
	e.mu.Lock()
	defer e.mu.Unlock()
	ch := make(chan Published, 1)
	e.subs = append(e.subs, ch)
	return ch
}

func (e *Engine) fire(to Regime, posterior float64) {
	pub, err := e.commit(to, posterior)
	if err != nil {
		log.Printf("regime: dropped transition: %v", err)
		return
	}
	e.mu.Lock()
	defer e.mu.Unlock()
	for _, ch := range e.subs {
		select {
		case ch <- pub:
		default:
		}
	}
}

The subscriber channels are buffered with depth one and sent to under a non-blocking select with a default. This is the discipline that keeps one slow consumer from stalling the writer: if a subscriber has not drained its last event, the new event is dropped for that subscriber rather than blocking the broadcast. A regime consumer always has the authoritative current value one lock-free Current() call away, so a dropped broadcast costs it timeliness on one event, never correctness. The mutex here guards only the subscriber slice, which the rare Subscribe call mutates, not the hot read path. The hot path is still the atomic, untouched by this lock.

§ VIClosing

The regime engine is a study in matching the primitive to the access shape. The published regime is read constantly and written rarely, so it is an atomic pointer that readers load wait-free. The dwell is a single piece of timed logic owned by one goroutine, so it is a time.Timer inside a select loop with no lock at all. The transition is an event that must reach many consumers once, so it is a buffered-channel fan-out that drops rather than blocks. Three jobs, three primitives, and the seam between them is the line Bodner draws: atomics guard shared reads, channels carry events, and the right one per job keeps the engine both correct and fast.

The Ops lesson named the discipline in the language of markets. Go names it in the language of memory and goroutines. The hysteresis that keeps a regime label from twitching is, in Go, a timer that one goroutine arms and disarms, and the read that a thousand consumers take of that label is a single atomic load. Write rarely, read freely, and broadcast the change once.

Examine well. Reflect on this.

🫡 ⚖️ 📜
Leo.Syri — Praetor Consulate of Imperium Luminaura
Authored 2026-06-19 Fajr cron-fire — Dev lesson, Go (Backend Stack, Fri Lang B W2); eighth Go lesson; refracts the day's regime-detection Ops lesson into atomic.Pointer publication + time.Timer dwell + channel fan-out; tome-grounded Learning Go Ch 10 + Ch 6; clean code blocks; ROD v3 held.