Documentation
¶
Overview ¶
Package hoplock provides lease-based leader election over a pluggable backend.
A Backend persists a single lease record. Mutual exclusion relies on the backend's compare-and-swap semantics: every write either succeeds because the previous handle still matches, or fails with ErrLeaseHeld. The Handle is opaque (e.g. an S3 ETag) and serves as proof that no concurrent writer has touched the record since the caller last observed it.
Callers typically use Elector, which runs the acquire/renew loop and notifies on leadership changes. Backend can also be used directly for one-shot or custom flows.
Index ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrNoLease means no lease record exists at the configured key. ErrNoLease = errors.New("hoplock: no lease") // ErrLeaseHeld means the conditional write failed because the stored // handle differs from the one supplied. Either someone else holds the // lease, or the caller's view is stale. ErrLeaseHeld = errors.New("hoplock: lease held by another owner") )
Errors returned by Backend implementations.
Functions ¶
This section is empty.
Types ¶
type Backend ¶
type Backend interface {
// Read returns the current lease state and the handle observed at read
// time. Returns (nil, "", ErrNoLease) if no record exists.
Read(ctx context.Context) (*State, string, error)
// Write commits state. If prevHandle is empty, succeeds only if no
// record exists. Otherwise, succeeds only if the stored handle equals
// prevHandle. Returns ErrLeaseHeld on precondition failure.
Write(ctx context.Context, prevHandle string, state *State) (handle string, err error)
// Delete removes the record only if handle still matches. Returns
// ErrNoLease if the record is already gone, ErrLeaseHeld if a different
// holder has taken over.
Delete(ctx context.Context, handle string) error
}
Backend is the storage abstraction. Implementations must guarantee that Write and Delete are linearisable: at most one concurrent caller can succeed against the same prior state.
All mutations go through Write, which is a compare-and-swap keyed on prevHandle. The empty string means "no record must exist" (used to create a fresh lease); any other value must equal the handle returned by the most recent successful Read or Write (used to renew or take over). On precondition mismatch, Write returns ErrLeaseHeld.
type Elector ¶
type Elector struct {
// Backend is the storage abstraction. Required.
Backend Backend
// Owner is the identifier written into State.Owner. Defaults to
// "<hostname>:<pid>" if empty.
Owner string
// TTL is the duration each acquired or renewed lease is valid for.
// Defaults to 30s.
TTL time.Duration
// Renew is how often the loop attempts to refresh a held lease.
// Defaults to TTL/3. Should be safely below TTL to absorb transient
// backend errors.
Renew time.Duration
// Poll is how often the loop retries while waiting for a lease held
// by someone else (or while recovering from transient errors).
// Defaults to TTL/15.
Poll time.Duration
// ReleaseTimeout bounds the best-effort Delete attempt issued when the
// Run context is cancelled while holding. Defaults to 2s. Set to a
// negative value to skip release on shutdown.
ReleaseTimeout time.Duration
// Now overrides the wall-clock source for ExpiresAt computations and
// expiry checks. Defaults to time.Now. Timer-based sleeping (Renew,
// Poll) still uses real time — Now injection is for deterministic
// decision-logic tests, not full clock virtualisation.
Now func() time.Time
// Logger receives debug/warning messages. Defaults to slog.Default().
Logger *slog.Logger
}
Elector runs a single leader-election loop against a Backend. It is goroutine-safe to construct, but Run should be called at most once per Elector instance.
func (*Elector) Lead ¶
Lead acquires the lease and calls fn with a context that is cancelled when leadership is lost. fn must return promptly when its context is cancelled. If fn returns a non-nil error, Lead stops trying and returns that error. If fn returns nil, Lead waits and tries to re-acquire, looping until the outer ctx is cancelled. Lead returns ctx.Err() on graceful shutdown.
func (*Elector) Run ¶
Run starts the election loop and returns a channel that receives Events until ctx is cancelled. The channel is closed after the loop exits.
Send semantics: EventAcquired and EventLost block on send (callers must observe transitions); EventRenewed is non-blocking and dropped on a full buffer (idempotent — the next renewal will catch you up).
type Event ¶
type Event struct {
Kind EventKind
Lease Lease
// Err is populated on EventLost when the loss was caused by a renewal
// failure or context cancellation. Nil for EventAcquired/EventRenewed.
Err error
}
Event describes a leadership transition or renewal observed by an Elector.
type EventKind ¶
type EventKind int
EventKind classifies an Event emitted by an Elector.
const ( // EventAcquired fires when this Elector starts holding the lease, // either by creating it from scratch or by taking over an expired one. EventAcquired EventKind = iota + 1 // EventRenewed fires after each successful renewal of an already-held // lease. Useful for surfacing the new ExpiresAt. EventRenewed // EventLost fires when this Elector stops holding the lease, either // because renewal failed (Err is set) or because Run is exiting due // to context cancellation. EventLost )
type Lease ¶
Lease is a State together with a Handle proving ownership. The Handle is backend-specific (e.g. an S3 ETag) and must be supplied to Replace or Delete to authorise the write.
type State ¶
type State struct {
// Generation is incremented every time leadership changes. Strictly
// monotonic across the lifetime of a lease key. Use this as a fencing
// token for downstream operations that must reject stale leaders.
Generation int64 `json:"generation"`
// ExpiresAt is the absolute time at which the lease becomes invalid
// if not renewed. Compared against the local clock, so all participants
// must have reasonably synchronised time.
ExpiresAt time.Time `json:"expires_at"`
// Owner is a free-form identifier of the holder. Informational only —
// mutual exclusion does not depend on it.
Owner string `json:"owner,omitempty"`
}
State is the lease record persisted in a Backend.
Directories
¶
| Path | Synopsis |
|---|---|
|
examples
|
|
|
minimal
command
Minimal example of using hoplock.Elector with the in-memory backend.
|
Minimal example of using hoplock.Elector with the in-memory backend. |
|
Package mem provides an in-memory hoplock.Backend for tests and single-process use.
|
Package mem provides an in-memory hoplock.Backend for tests and single-process use. |
|
Package s3 provides an S3-backed hoplock.Backend: the lease layer on top of github.com/xinix00/lean/leans3.
|
Package s3 provides an S3-backed hoplock.Backend: the lease layer on top of github.com/xinix00/lean/leans3. |