Documentation
¶
Overview ¶
Package ha is Hanzo's high-availability coordination primitive: single-writer election over a live membership set, with 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, so N stateless replicas agree on one owner per key and fail over deterministically with no recomputation.
ha decides WHO writes; it never touches HOW state is stored or replicated. That separation is the point: compose ha with hanzoai/vfs (per-org SQLite + object store) for HA storage, or with a cron tick, a queue consumer, or any singleton that must run exactly once across a multi-replica Deployment. The same election guards all of them.
The seam is Membership: election is a pure function of (key, []Member), and a Membership supplies the live set. Wire a cluster source (hanzoai/ha/k8s) in production, or Static for a single process and tests.
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
Types ¶
type Lease ¶ added in v0.1.1
type Lease struct {
Key string // the ownership unit this lease fences (e.g. an org id).
Owner Member // the elected writer that holds the role.
Round Round // the monotone round the owner stamps onto its writes.
}
Lease binds an elected Owner to the Round at which it holds the single-writer role for Key. It is a value, not a held lock: possession is never enforced by carrying a Lease but by the store admitting a write stamped with its Round. A Lease whose Round has been superseded is inert — the store rejects it — which is exactly what makes a still-running deposed writer harmless.
type Leases ¶ added in v0.2.0
type Leases interface {
// Acquire returns the caller's Lease for key: itself as Owner, bound to the
// round at which it currently holds the writer role. Implementations advance
// the round when the role changes hands TO the caller and keep it on renewal,
// so repeated Acquire calls by a stable owner return a stable round while a
// takeover returns a strictly higher one. It returns an error when the caller
// is not — or can no longer be — the writer for key; the caller then does not
// write (and typically forwards to, or steps aside for, the true owner).
Acquire(ctx context.Context, key string) (Lease, error)
}
Leases issues the caller's current Lease for a key from a linearizable, monotone round source. It is the seam a consensus-backed round drops into with no change at call sites; the interim implementation reads and advances a round in a single linearizable store. This package defines the seam and the value only — never the source.
A caller MUST fail CLOSED on an error: no Lease means no established round, and writing without a fresh round risks two writers at one epoch. A nil error is the only signal it is safe to write under.
func StaticLeases ¶ added in v0.2.0
StaticLeases is the single-process Leases: the sole process is the sole writer, so every key is held by self at a fixed Round of 1 — exactly-once by construction, with no round source to consult. It is the correct Leases for local dev, a standalone binary, and tests, and the safe default when no linearizable source is wired — mirroring Static for Membership. A single process cannot have a deposed second writer, so a constant round is sound; the moment there are two writers, a real (linearizable) Leases is required.
type Member ¶
Member is one replica in the live membership set. ID must be stable for the life of the replica (e.g. its pod name / a persistent node id): HRW weights derive from it. Addr is the reachable address for write-forwarding (host:port).
type Membership ¶
type Membership interface {
// Self is this replica's stable ID — the value HRW weights derive from. It must
// be stable for the replica's life (e.g. its pod name / a persistent node id).
Self() string
// Members is the live writer-eligible set, or an error the caller fails closed
// on. A nil error with a non-empty set is the only signal safe to proceed on.
Members(ctx context.Context) ([]Member, error)
}
Membership is the seam election elects over: the live, writer-eligible replica set plus this replica's own stable ID. It is the ONE injectable source every consumer shares — production wires a cluster source (e.g. hanzoai/ha/k8s), tests and single-process mode wire Static.
A consumer MUST fail CLOSED on a Members error or an empty set: election over an unknown or empty membership has no safe owner, so the caller skips rather than risk two writers. A nil error with a non-empty set is the only "proceed" signal.
func Static ¶
func Static(id string) Membership
Static is the single-process Membership: one member, itself, never an error — the sole process is the sole writer. It is the correct source for local dev, a standalone binary, and tests, and the safe default when no cluster source is wired (a single process is exactly-once by construction). id is this member's ID.
type Round ¶ added in v0.1.1
type Round uint64
Round is a monotone fencing epoch for a key. It is the SOLE boundary value between coordination (this package) and storage (the fenced object store): the store knows only that rounds increase and that a lower round is stale — nothing about members, leases, elections, or how the round was decided.