zeekayandGitHub 92b4e1ecb4 ha: add Lease/Round fencing value + Fencer seam (v0.1.1) (#1)
Election is coordination-free, so two replicas with divergent membership
views can each elect themselves the single writer for a key (split-brain),
and election has no way to make a deposed writer stop. Add the composable
fencing VALUE and SEAM that close this, with no I/O and no consensus folded in:

  - Round uint64            monotone fencing epoch (the only value into storage)
  - Lease{Key,Owner,Round}  binds an elected owner to the round it writes at
  - Fencer.Acquire seam     round source plugs in behind it (linearizable store
                            today, Lux BFT round tomorrow) — never in this package
  - StaticFencer(self)      single-process fencer (round 1), mirrors Static

Invariant: round monotone per key, strictly increases on handoff, so a new
lease outranks every prior holder and the store (vfs/replica.FencedStore)
fences the old writer by refusing any lower round.
2026-07-09 13:07:19 -07:00

ha

Hanzo's high-availability coordination primitive: single-writer election over a live membership set — no coordinator, no lock service, no Postgres, no Redis.

It answers one question, identically on every replica:

which replica is the single writer for key K right now?

via Rendezvous (Highest-Random-Weight) hashing. N stateless replicas agree on one owner per key and fail over deterministically, with no recomputation and nothing to coordinate.

Why it is its own package

ha decides who writes. It never touches how state is stored or replicated — that is hanzoai/vfs (per-org SQLite + object store) or any other backing. Keeping election separate means a cron tick, a queue consumer, or any singleton can be made exactly-once without importing a storage library to get it. One primitive, one home, every consumer.

ha            who is the single writer          ← this package (pure Go, zero deps)
vfs           how the SQLite state replicates    → composes ha for its writer gate
your service  what the single writer does        → cron, billing sweep, queue drain

Use

import "github.com/hanzoai/ha"

// The per-write hot path: only the elected owner of key proceeds.
if ha.IsOwner(orgID, self, members) {
    // ... this replica is the single writer for orgID; do the write.
}

members comes from a Membership — the injectable seam:

// Single process / local dev / tests: itself is the sole writer.
m := ha.Static(podName)

// Production: a cluster source (e.g. hanzoai/ha/k8s) lists live, ready peers.
self := m.Self()
members, err := m.Members(ctx)
if err != nil || len(members) == 0 {
    // FAIL CLOSED — unknown or empty membership has no safe owner; skip.
    return
}
if ha.IsOwner(orgID, self, members) { /* write */ }

API

Symbol Purpose
Member{ID, Addr} one replica; ID is the stable HRW identity, Addr for write-forwarding
Owner(key, members) (Member, bool) the elected single writer for key; ok=false on empty set (fail-closed)
IsOwner(key, self, members) bool the per-write gate every replica runs
Replicas(key, members, n) []Member owner first, then ordered failover successors (pre-warm order)
Membership seam: Self() + Members(ctx) — the live set election elects over
Static(id) single-process Membership (one member, itself, never an error)

Guarantees

  • Deterministic & order-independent — same (key, members) ⇒ same owner on every replica, regardless of set order (ties break on ID).
  • Fail-closed — empty membership yields no owner; a consumer that cannot read its peers must skip, never assume ownership.
  • Even spread — distinct keys distribute across replicas (HRW), not all pinned to one, so ownership (and its write load) balances.
  • Cheap — a per-write check is a handful of SHA-256s; see BenchmarkOwnerElection.

Apache-2.0.

S
Description
Single-writer election over a live membership set (HRW/Rendezvous) — Hanzo's HA coordination primitive. No coordinator, lock service, Postgres, or Redis.
Readme
50 KiB
Languages
Go 100%