Documentation
¶
Overview ¶
Package writerlease answers ONE question for ONE process: must I take the pod's writer lease before anything on this volume is opened for write?
The lease states a POD-level fact — "this pod owns this volume" — using a PROCESS-level primitive, flock(2) on {DataDir}/.writer.lock. Those are only the same sentence when exactly one process per pod ever reaches for the lock, and cloud is a plugin host: the router in cmd/cloud spawns every subsystem (kms, pubsub, kafka, …) as its own child process, all sharing one DataDir.
On 2026-08-04 they were not the same sentence. The acquire lived in cloud.Listen — the body every PLUGIN binary runs, and the one thing the router never calls. Turning CLOUD_WRITER_LEASE on therefore aimed the interlock at the siblings instead of at the other pod: kms won the flock, pubsub and kafka blocked until the 90s fail-closed deadline, nothing bound the listener, the liveness probe killed the pod, and its replacement deadlocked identically. api.hanzo.ai served 503 for four minutes at the hands of its own safety mechanism.
The repair that followed excluded the children but never handed the lock to the router, so no process in the pod took it at all: an interlock that reads as armed and holds nothing. That is the more dangerous of the two states. The deadlock announced itself; a lock held by nobody is quiet right up until someone believes the log line, switches the Deployment to RollingUpdate, and two pods open the same files.
So the duty is decided here, once, from the process's own position in the tree, and every caller does as it is told:
Take — this process is the root of its pod. Take the lock BEFORE it
spawns anything; release it AFTER the last child is gone.
Inherit — a parent already holds it for this whole pod. Do not touch it.
Off — this deployment asked for no lease. The default, and correct
under strategy: Recreate, where Kubernetes retires the old pod
before starting the new one, so no two writers ever coexist.
This package imports only the stdlib and role, so the light router binary can link it without dragging in the request tier it deliberately does not build.
Index ¶
Constants ¶
const ( // Enable is the operator switch. Unset ⇒ Off ⇒ byte-identical to a // deployment that never heard of a lease. Enable = "CLOUD_WRITER_LEASE" // Held is how the answer travels DOWN. The pod root writes its own PID here // once it holds the lock, and zip spawns every child with // append(os.Environ(), …) — so each child is born already knowing the volume // is spoken for, rather than discovering it by losing a race. // // The value is the holder's PID rather than a bare "1" so a child can CHECK // the claim instead of believing it: the stamp counts only when it names the // child's own parent. That closes the single way this design could fail open, // which is a stray CLOUD_WRITER_LEASE_HELD in a manifest talking a pod root // out of taking the lock. Held = "CLOUD_WRITER_LEASE_HELD" // LockName is the flock anchor under DataDir: zero length, never a store, so // it is safe to create and to leave on the volume. LockName = ".writer.lock" // DefaultWait is how long a pod root waits for the PREVIOUS pod to let go of // the volume before refusing to boot. A handoff budget, not a retry budget: // the predecessor is closing stores, and if it has not finished within this // the fault is in its shutdown, which starting anyway would only compound. // One definition, because the router and the standalone binary must wait the // same amount or the pair has two different ideas of when a roll has failed. DefaultWait = 90 * time.Second )
Variables ¶
This section is empty.
Functions ¶
func Acquire ¶
func Acquire(dataDir string, timeout time.Duration, logf func(string, ...any)) (func() error, error)
Acquire takes the exclusive lock, polling a non-blocking flock until it is free or timeout elapses.
On timeout it fails CLOSED: a writer that cannot prove it is the only opener refuses to open the stores rather than risk a double open. That is the right trade for two POD GENERATIONS — it costs a slow rollout and saves the database. It was the wrong trade only because it used to be applied between siblings, where the wait could never end.
func Hold ¶
Hold is the ONE call every entrypoint makes. It assesses, takes the lock when this process is the pod's root, and stamps the environment so that every child spawned afterwards inherits the answer instead of racing for it.
It returns a release func that is ALWAYS non-nil, so no caller needs a branch to decide whether it has something to release. Release the lock only once the stores are closed and the children are gone: the lock is the statement "the volume is mine", and a successor is entitled to open everything the moment it is withdrawn.
logf is the caller's logger as a plain function, so this package stays free of any particular logging dependency and the light router links no more than it already does.