worklease

package module
v0.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 29, 2026 License: Apache-2.0 Imports: 7 Imported by: 0

README

worklease

CI Go Reference Go Report Card License

worklease is a Go library for lease-based work coordination in distributed systems.

It is not a distributed lock library.

Distributed locks answer: who owns this resource right now? worklease answers a different question: when ownership changes, what does the new owner need to continue the work the previous owner started?


The Problem

Worker A acquires a lease, begins processing a multi-step job, makes partial progress, then crashes. Worker B acquires the lease — and starts from scratch.

Depending on the operation, this means duplicated writes, lost progress, or inconsistent state. Idempotency helps only if restarting from the beginning is cheap. For long-running work — provider onboarding, async lifecycle management, batch processing with intermediate state — it isn't.

The correct fix is checkpointed lease handoff: the outgoing owner writes progress state atomically with its last lease renewal. The incoming owner reads that state before starting work. Fencing tokens prevent zombie writes from expired owners corrupting the checkpoint after they've been superseded.

Every Go distributed locking library (distlock, pglock, dynamodb-lock-go, etcd leases, client-go/leaderelection) solves presence — mutual exclusion, TTL expiry, heartbeat renewal. None solve continuity. worklease fills that gap.

Is worklease the right fit?

worklease is designed for a specific shape of work: stateful, multi-step operations where restarting from scratch is expensive or incorrect, and where adopting a full workflow engine is not the right trade-off.

It is a good fit if you are running Go workers with PostgreSQL already in the stack, doing long-running jobs with meaningful intermediate state, and need correct handoff semantics without restructuring your application.

It is not the right fit if your jobs are small enough to restart cheaply — a job queue (River, Asynq, FOR UPDATE SKIP LOCKED) is simpler and sufficient. If your work is complex enough to need replay semantics and activity scheduling, Temporal is the right tool.


Prior Art

This pattern is not new. AWS's Kinesis Client Library has implemented it since 2013 via its LeaseRefresher. KCL maintains a DynamoDB table where each lease entry holds an explicit checkpoint column and a leaseCounter for fencing. A worker atomically renews its lease while writing its progress checkpoint. When a worker's lease expires, the successor reads the checkpoint column and resumes from exactly that point. Fencing is enforced via conditional writes on leaseCounter — structurally identical to worklease's fencing_token.

The pattern is proven and has been running production Kinesis workloads at AWS scale for over a decade.

What KCL doesn't provide:

  • A Go-native implementation (KCL Go is a thin wrapper over a Java MultiLangDaemon process — not idiomatic)
  • General-purpose work coordination (its checkpoint is a stream sequence number; the entire model assumes Kinesis shard processing)
  • Portability outside AWS (DynamoDB backend, CloudWatch metrics, IAM — AWS-only)

worklease extracts the same semantics into a general-purpose Go primitive — no AWS dependency, no Kinesis assumption, pluggable backends.


What worklease does not solve

These are intentional scope boundaries, not gaps.

External side effects are not fenced. ErrFenced stops stale checkpoint writes to the lease store. It does not stop a zombie worker from calling Stripe, sending an email, or writing to S3 before it discovers it has been superseded. Callers must make every external mutation idempotent, use an outbox pattern, or enforce idempotency keys at downstream systems. This is an inherent property of any coordination primitive — the library fences what it owns.

The window between checkpoints is at-least-once. If Worker A executes a step and crashes before checkpointing, Worker B will re-execute that step from the previous checkpoint. worklease provides a resumable progress marker, not exactly-once step execution.

Recovery logic is yours. The library delivers checkpoint bytes and the cleanHandoff flag. What those bytes mean, how to validate partial state, and how to reconcile external effects that happened before the crash are application concerns.

Release expires the lease immediately. Calling Release sets clean_handoff = true and sets the lease expiration to the past, making the work item immediately available. A successor using WithWaitForLease will acquire on its next poll; a fail-fast successor can call Acquire immediately after. The TTL governs crash detection only — it does not add latency to clean handoffs.


Concepts

Lease — A time-limited claim on a named unit of work. Expires if not renewed. When it expires, another worker can acquire it.

Fencing token — A monotonically incrementing integer issued on every acquisition. A worker's writes to the lease store are rejected if a higher token has been issued — preventing zombie workers from corrupting the checkpoint after their lease expires. Tokens are monotonic but not contiguous — every Acquire attempt advances the underlying sequence, including attempts that return ErrLeaseHeld; gaps in the token sequence are expected and have no operational significance.

Checkpoint — Progress state written atomically with lease renewal. If the worker is making progress, it proves liveness and saves state in one operation. The last checkpoint survives to the next owner. Checkpoint and Renew are distinct: Checkpoint writes state and extends the TTL atomically; Renew extends the TTL without updating state.

Handoff vs crash recovery — Two distinct acquisition paths. If the previous owner called Release explicitly, the checkpoint contains final state and the successor knows the work was cleanly surrendered. If the lease expired without a release, the checkpoint contains the last known progress and the successor resumes from partial state. These are different situations and callers handle them differently. The library makes the distinction explicit.


Usage

Acquiring a lease and checkpointing progress
backend, err := postgres.New(db)
if err != nil {
    return err
}

lease, err := worklease.New(backend, worklease.Config{
    TTL:      30 * time.Second,
    HolderID: workerID,
})
if err != nil {
    return err
}

token, err := lease.Acquire(ctx, "onboarding:tenant-abc")
if err != nil {
    return err
}
defer lease.Release(ctx, token)

// Read what the previous owner left behind
state, cleanHandoff, err := lease.ReadCheckpoint(ctx, token)
if err != nil {
    return err
}

var progress OnboardingProgress
if state != nil {
    if err := json.Unmarshal(state, &progress); err != nil {
        return err
    }
    if !cleanHandoff {
        // Previous owner crashed — validate partial state before resuming.
        // External effects from incomplete steps may have already fired.
        progress = recoverFromPartial(progress)
    }
}

// Do work, checkpointing at each step boundary.
// Checkpoint after the external effect completes — not before.
for _, step := range remainingSteps(progress) {
    if err := executeStep(ctx, step); err != nil {
        return err
    }

    progress.CompletedSteps = append(progress.CompletedSteps, step.ID)
    snapshot, _ := json.Marshal(progress)

    // Atomically saves progress and renews the lease.
    // Returns ErrFenced if this worker has been superseded.
    if err := lease.Checkpoint(ctx, token, snapshot); err != nil {
        return err
    }
}
Renewing without checkpointing

If the worker is healthy but not at a checkpointable boundary yet:

if err := lease.Renew(ctx, token); err != nil {
    // ErrFenced: a higher token has been issued — stop working.
    return err
}

Checkpoint and Renew are separate because they mean different things. Renew says "I'm alive." Checkpoint says "I'm alive and here's where I am."

Fresh acquisition

ReadCheckpoint returns nil state (not an error) when this is the first worker to acquire this lease:

state, _, err := lease.ReadCheckpoint(ctx, token)
if err != nil {
    return err
}
if state == nil {
    // First acquisition — start from the beginning.
    progress = OnboardingProgress{}
}
Managing renewal directly

StartRenewal starts a managed renewal goroutine and returns a derived context and a stop function. worker.Runner and leader.Elect handle this lifecycle automatically; use StartRenewal directly only when you need fine-grained control.

Two requirements apply when calling StartRenewal directly:

  1. Call stopRenewal() before Release — the goroutine must exit before ownership is surrendered. Register defer stopRenewal() immediately as a panic-safety net, then call it explicitly before Release (the defer becomes a no-op on the clean path).
  2. Pass the original ctx to Release, not renewCtxrenewCtx may already be cancelled (by fencing or window exhaustion) when Release is called; using it causes Release to fail with a context error.
renewCtx, stopRenewal := lease.StartRenewal(ctx, token)
defer stopRenewal() // panic-safety net

if err := doWork(renewCtx, ...); err != nil {
    return err // stopRenewal fires via defer; do not Release if fenced
}

stopRenewal()             // explicit stop before Release
lease.Release(ctx, token) // use original ctx, not renewCtx

Error Reference

Error Meaning
ErrFenced A higher fencing token has been issued. This worker has been superseded. Stop writing to the lease store.
ErrLeaseHeld The lease is currently held by another worker.
ErrLeaseExpired The lease expired before this operation completed.

ErrFenced is the critical one. When Checkpoint or Renew returns ErrFenced, a successor has already acquired the lease and this worker is a zombie. Stop immediately — any further writes to the lease store will be rejected. Note that ErrFenced does not cancel in-flight external calls the worker may have already initiated; those must be made idempotent at the application layer.


Upgrading

v0.5.0 changes Acquire with WithWaitForLease: on context cancellation or deadline while waiting it now returns an error wrapping ctx.Err() (satisfying errors.Is(err, context.Canceled) / context.DeadlineExceeded) instead of bare ErrLeaseHeld. This is a runtime break for callers that treated ErrLeaseHeld as their sole wait-loop termination signal; the synchronous no-wait path is unchanged. The renewal goroutine also now retries transient errors with backoff bounded by the lease window rather than stopping on the first error (configure with WithRenewalBackoff).

v0.4.0 redesigns LeaseObserver from flat parameters to event structs (adds OnReadCheckpoint, fires OnFenced on the Release path, adds Duration), and pool.New now returns distinct config sentinels (ErrNilLease / ErrEmptyWorkIDs / ErrWithWaitForLeaseProhibited, all wrapping ErrConfigInvalid) while pool.Pool.Run returns ErrAllSlotsDead when every slot dies permanently.

v0.3.0 includes a breaking change in the checkpoint package: Codec.Encode and Codec.Decode are renamed to Marshal and Unmarshal. Callers using checkpoint.JSON() are unaffected.

See UPGRADING.md for full migration instructions.


Higher-level packages

worker — Lifecycle management

worker.Runner handles the acquire / ReadCheckpoint / StartRenewal / WorkFn / Release lifecycle so callers implement only the work function:

import "github.com/aetomala/worklease/worker"

r, err := worker.NewRunner(worker.RunnerConfig{
    Lease:  lease,
    WorkFn: func(ctx context.Context, token worklease.Token, prior []byte, cleanHandoff bool) ([]byte, error) {
        // prior is the last checkpoint from the previous holder; nil on first acquisition.
        // Return updated checkpoint bytes, or nil to leave the checkpoint unchanged.
        return processWork(ctx, prior, cleanHandoff)
    },
})
if err != nil { ... }
if err := r.Run(ctx, "onboarding:tenant-abc"); err != nil { ... }
leader — Simplified leadership

leader.Elect acquires a work ID, starts managed lease renewal, calls fn under the renewal context, stops renewal, and releases. Suitable for single-leader patterns where one process should be active at a time:

import "github.com/aetomala/worklease/leader"

err := leader.Elect(ctx, lease, "scheduler:primary", leader.Config{}, func(ctx context.Context) error {
    // ctx is cancelled if the lease is fenced or renewal fails.
    // Return when the work is done; leader.Elect will release.
    return runScheduler(ctx)
})

Pass worklease.WithWaitForLease() in leader.Config.AcquireOptions to block until leadership is available rather than returning ErrLeaseHeld immediately. leader.Config also accepts optional lifecycle callbacks — OnElected, OnLost, and OnRelinquished — for observing leadership transitions.

pool — Distributed work distribution

pool.Pool distributes a fixed set of work IDs across competing processes. Multiple Pool instances — one per process — collectively cover the full work ID set. Rebalancing is emergent from lease acquisition races:

import "github.com/aetomala/worklease/pool"

p, err := pool.New(lease, pool.Config{
    WorkIDs: []string{"shard-0", "shard-1", "shard-2", "shard-3"},
}, func(ctx context.Context, workID string, token worklease.Token, prior []byte, cleanHandoff bool) ([]byte, error) {
    return processShard(ctx, workID, prior)
})
if err != nil { ... }

// Blocks until ctx is cancelled; all active slots complete before Run returns.
if err := p.Run(ctx); err != nil { ... }

Return a PermanentError from the work function to drop a slot without reacquisition — implement the interface on a custom type, or wrap an error with pool.Permanent(err). When every slot exits permanently, Run returns pool.ErrAllSlotsDead (rather than nil, which signals clean context cancellation). Set pool.Config.Observer to a pool.Observer to receive slot lifecycle events (OnSlotAcquired / OnSlotLost / OnSlotBackoff / OnSlotDead), and call ActiveSlots() for a point-in-time view of slots currently executing their work function. Construction errors are distinct sentinels (ErrNilLease / ErrEmptyWorkIDs / ErrWithWaitForLeaseProhibited) that all satisfy errors.Is(err, pool.ErrConfigInvalid).

checkpoint — Typed serialization helpers

checkpoint.Codec and the generic Encode[T] / Decode[T] helpers add typed serialization on top of the raw []byte checkpoint layer. checkpoint.JSON() returns a JSONCodec backed by encoding/json.

Observability — LeaseObserver

Set worklease.Config.Observer to a LeaseObserver to receive a synchronous callback after every lease operation — OnAcquire, OnCheckpoint, OnRenew, OnRelease, OnReadCheckpoint, and OnFenced — each with a per-operation event struct carrying the Token, error, and a Duration for the final backend call. nil installs a no-op, so observability is fully opt-in. See examples/observability for a stdlib-only implementation.


Backends

PostgreSQL (v0.1)
import "github.com/aetomala/worklease/backend/postgres"

db, _ := sql.Open("postgres", dsn)
backend, _ := postgres.New(db)

lease, _ := worklease.New(backend, worklease.Config{
    TTL:      30 * time.Second,
    HolderID: os.Getenv("WORKER_ID"),
})

Run the migration before first use (canonical source: backend/postgres/schema.sql):

-- canonical: backend/postgres/schema.sql
CREATE SEQUENCE IF NOT EXISTS worklease_fencing_seq;

CREATE TABLE IF NOT EXISTS worklease_leases (
    work_id         TEXT PRIMARY KEY,
    holder_id       TEXT        NOT NULL,
    fencing_token   BIGINT      NOT NULL DEFAULT nextval('worklease_fencing_seq'),
    expires_at      TIMESTAMPTZ NOT NULL,
    checkpoint      BYTEA,
    clean_handoff   BOOLEAN     NOT NULL DEFAULT FALSE,
    acquired_at     TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

CREATE INDEX IF NOT EXISTS idx_worklease_leases_updated_at
    ON worklease_leases (updated_at);

The atomic checkpoint operation is a single UPDATE ... WHERE fencing_token = $n. Zero rows updated means the token is stale. There is no Lua script, no optimistic retry loop, no distributed clock dependency.

In-memory (testing)
import "github.com/aetomala/worklease/backend/memory"

backend := memory.New()
lease, _ := worklease.New(backend, worklease.Config{
    TTL:      5 * time.Second,
    HolderID: "test-worker",
})

Suitable for unit tests within a single process. Implements the same fencing token semantics as the PostgreSQL backend. Not safe for concurrent use across processes.


Comparison

Tool Layer Solves Doesn't solve
distlock / pglock Library Mutual exclusion with TTL No checkpoint, no handoff semantics
dynamodb-lock-go Library DynamoDB-backed locking No progress state, no write fencing
etcd leases Infrastructure Kubernetes-native presence No work coordination layer
client-go/leaderelection Library Single leader election Not per-work-item ownership
KCL LeaseRefresher Library Checkpointed lease handoff at AWS scale Java only, Kinesis-specific, AWS-only
Temporal Platform Durable workflow execution with replay Requires full application rewrite as Workflows + Activities; run a server
Inngest Platform Managed durable step execution Requires platform adoption — not a drop-in library
DBOS Framework DB-backed durable execution Requires full application restructure; limited Go support
Watermill Library Go message routing Checkpointing is broker offset tracking, not general work state
River / Asynq / job queues Library Work assignment with retry No stateful handoff — successor restarts from scratch
worklease Library Work handoff with continuity, idiomatic Go, pluggable backends Not a general-purpose lock; does not fence external side effects
Why not Temporal, Inngest, or DBOS?

These tools sit at the high-guarantee, high-adoption-cost end of the spectrum. They solve the failure mode — but the adoption unit is your entire application architecture.

Temporal requires rewriting business logic as Workflows and Activities, running a Temporal server, and accepting deterministic replay constraints. Inngest is a fully managed platform with its own execution model. DBOS restructures your entire application around a framework.

Teams already running Go workers with PostgreSQL don't need to restructure their application to get correct lease handoff semantics. They need a library primitive they can drop into the system they already have.

Distributed locks sit at the opposite end — low adoption cost, wrong abstraction. They tell you who holds the resource; they don't help the new holder continue what the old holder started.

The gap between these two ends is where worklease lives. The only prior art in that gap is KCL, which is Java-only and Kinesis-specific.


Installation

go get github.com/aetomala/worklease

Requires Go 1.25+. PostgreSQL backend requires PostgreSQL 12+.


Status

v0.5.0 is the latest release line. The core public API (Lease, Token, options, sentinels) is stable. v0.5 adds bounded renewal retry (WithRenewalBackoff, ErrLeaseWindowExhausted, RenewEvent.Attempt), a global fencing sequence on both backends, a single-statement Postgres Acquire with RETURNING, and ctx-aware Acquire cancellation under WithWaitForLease (a runtime break — see UPGRADING.md).


Roadmap

Released
  • v0.1.0 — Core lease primitives: Lease, Backend, PostgreSQL + in-memory backends, fencing, checkpoint
  • v0.2.0worker.Runner, checkpoint.Codec, LeaseObserver, memory.Clock, examples
  • v0.3.0leader.Elect, pool.Pool, HasWaitForLease, checkpoint.Codec method rename (breaking — see UPGRADING.md)
  • v0.4.0LeaseObserver event-struct redesign (breaking), backend/conformance suite, pool.Observer/Permanent/ErrAllSlotsDead, leader lifecycle callbacks, memory slice-ownership fix
  • v0.5.0 — bounded renewal retry (WithRenewalBackoff, ErrLeaseWindowExhausted, RenewEvent.Attempt); global fencing sequence on both backends; single-statement Acquire with RETURNING; ctx-aware Acquire cancellation under WithWaitForLease (breaking — see UPGRADING.md)
Future
  • v0.6 — caller-governed row lifecycle (Forget / Vacuum.Sweep)
  • Redis backend, etcd backend (unscheduled, post-1.0)
  • Token test constructor — unblocks table-driven tests that construct tokens directly

Examples

Runnable examples covering crash recovery, checkpoint resume, cluster leadership, partition processing, observability, and the renewal and acquire lifecycle. No infrastructure required — all examples run against the in-memory backend.

See examples/ for the full list with descriptions and run instructions.


License

Apache 2.0 — see LICENSE.

Documentation

Overview

Package worklease provides lease-based work coordination for distributed systems.

worklease solves a different problem than distributed locking. Distributed locks answer: who owns this resource right now? worklease answers: when ownership changes, what does the new owner need to continue the work the previous owner started?

The library is built around three primitives:

  • A Lease — a time-limited, named claim on a unit of work.
  • A fencing token — a monotonically incrementing integer that prevents zombie writes from a worker that was slow, not dead.
  • A checkpoint — progress state written atomically with lease renewal, so the last known state survives to the next owner.

Entry point

Construct a Lease with New, backed by a [Backend]:

backend, err := postgres.New(db)
lease, err := worklease.New(backend, worklease.Config{
    TTL:      30 * time.Second,
    HolderID: workerID,
})

See the Lease interface for the full API.

What worklease is not

worklease is not a distributed lock library, a workflow engine, or a general-purpose job queue. For mutual exclusion alone, use distlock or pglock. For durable workflow execution, use Temporal. For Kubernetes-native leader election, use client-go/leaderelection. The leader subpackage provides simplified single-work-item leadership, and the pool subpackage distributes a fixed set of work IDs across competing processes — both built on worklease's own Lease primitives.

Backends

Two backends are included:

Index

Constants

This section is empty.

Variables

View Source
var (
	ErrFenced       = errors.New(msgFenced)
	ErrLeaseHeld    = errors.New(msgLeaseHeld)
	ErrLeaseExpired = errors.New(msgLeaseExpired)

	// ErrLeaseWindowExhausted is set as the cancel cause of the renewal context
	// when the renewal goroutine exhausts the remaining lease window without a
	// successful renewal (goroutine lifecycle path 3).
	// Inspect via context.Cause(renewCtx) — not returned directly from any method.
	ErrLeaseWindowExhausted = errors.New(msgLeaseWindowExhausted)
)

Sentinel errors for Lease operations.

Functions

func HasWaitForLease added in v0.3.0

func HasWaitForLease(opts []AcquireOption) bool

HasWaitForLease reports whether opts includes WithWaitForLease. Pool.New uses this to enforce that WithWaitForLease is not passed in Config.AcquireOptions at construction time.

Types

type AcquireEvent added in v0.4.0

type AcquireEvent struct {
	WorkID   string
	Token    Token
	Duration time.Duration
	Err      error
}

AcquireEvent carries the result of an Acquire call. Duration is the duration of the final backend call only — not the wait loop. Token is the zero value of Token if Err is non-nil.

type AcquireOption

type AcquireOption func(*acquireConfig)

AcquireOption is a functional option for Acquire.

func WithPollInterval

func WithPollInterval(d time.Duration) AcquireOption

WithPollInterval sets the interval at which Acquire polls when WithWaitForLease is active. If d is zero or negative, the default (2 * time.Second) is preserved.

func WithWaitForLease

func WithWaitForLease() AcquireOption

WithWaitForLease configures Acquire to block and retry until a lease becomes available, rather than returning ErrLeaseHeld immediately.

type CheckpointEvent added in v0.4.0

type CheckpointEvent struct {
	Token    Token
	Size     int
	Duration time.Duration
	Err      error
}

CheckpointEvent carries the result of a Checkpoint call. Size is len(state) from the call site.

type Config

type Config struct {
	// TTL is the time-to-live for acquired leases. Required; zero returns an error.
	TTL time.Duration

	// HolderID is the identifier of the entity that will hold leases. Required; empty returns an error.
	HolderID string

	// Observer is the LeaseObserver that will be notified of lease events. Optional; nil defaults to noopObserver.
	Observer LeaseObserver
}

Config holds configuration for a Lease instance.

type FencedEvent added in v0.4.0

type FencedEvent struct {
	Token     Token
	Operation Operation
}

FencedEvent carries the context of a fencing event. Called in addition to the operation-specific event — not instead of.

type Lease

type Lease interface {
	// Acquire attempts to acquire a lease for the given workID. Returns ErrLeaseHeld
	// if a lease already exists for this workID. If WithWaitForLease is set, blocks
	// until the lease is available, polling at the configured interval.
	Acquire(ctx context.Context, workID string, opts ...AcquireOption) (Token, error)

	// Checkpoint persists state associated with the current lease. The caller must
	// pass a valid Token obtained from Acquire or Renew. Returns ErrFenced if the
	// token's fencing token no longer matches the stored lease.
	Checkpoint(ctx context.Context, token Token, state []byte) error

	// Renew extends the lease expiration time. Returns ErrFenced if the token's
	// fencing token no longer matches the stored lease, or ErrLeaseExpired if the
	// lease has already expired.
	Renew(ctx context.Context, token Token) error

	// Release surrenders the lease and expires it immediately, making the work item
	// available for acquisition by a successor without waiting for the TTL. Sets
	// clean_handoff so the successor knows the previous owner finished intentionally.
	// Returns ErrFenced if the fencing token no longer matches the stored lease.
	Release(ctx context.Context, token Token) error

	// ReadCheckpoint retrieves persisted state and the clean handoff flag for the
	// given lease. The caller must pass a valid Token. Returns ErrFenced if the
	// token's fencing token no longer matches the stored lease.
	ReadCheckpoint(ctx context.Context, token Token) (state []byte, cleanHandoff bool, err error)

	// StartRenewal begins automatic renewal of the lease at regular intervals. Returns
	// a derived context and a stop function. Calling stop cancels the renewal context
	// and terminates the renewal loop. The renewal context is cancelled if the underlying
	// context is cancelled or if the lease is lost.
	StartRenewal(ctx context.Context, token Token, opts ...RenewalOption) (renewCtx context.Context, stopRenewal func())
}

Lease defines the contract for acquiring, managing, and renewing leases on distributed work. Implementations are responsible for handling backend storage, fencing, and expiration logic. All methods are safe for concurrent use.

func New

func New(b backend.Backend, cfg Config) (Lease, error)

New returns a new Lease instance backed by the provided Backend. Returns an error if the backend is nil, TTL is zero, or HolderID is empty.

type LeaseObserver added in v0.2.0

type LeaseObserver interface {
	// OnAcquire is called after every Acquire attempt, successful or not.
	// e.Duration is the duration of the final backend call only — not the wait loop.
	// e.Token is the zero value of Token if e.Err is non-nil.
	OnAcquire(ctx context.Context, e AcquireEvent)

	// OnCheckpoint is called after every Checkpoint attempt.
	// If e.Err is ErrFenced, OnFenced is also called after this method returns.
	OnCheckpoint(ctx context.Context, e CheckpointEvent)

	// OnRenew is called after every Renew attempt.
	// If e.Err is ErrFenced, OnFenced is also called after this method returns.
	OnRenew(ctx context.Context, e RenewEvent)

	// OnRelease is called after every Release attempt.
	// If e.Err is ErrFenced, OnFenced is also called after this method returns.
	OnRelease(ctx context.Context, e ReleaseEvent)

	// OnReadCheckpoint is called after every ReadCheckpoint attempt.
	// OnFenced is NOT called on ReadCheckpoint — ErrFenced surfaces via e.Err only.
	OnReadCheckpoint(ctx context.Context, e ReadCheckpointEvent)

	// OnFenced is called when Checkpoint, Renew, or Release returns ErrFenced.
	// Called in addition to the operation-specific callback — not instead of.
	// e.Operation identifies which operation triggered the fencing event.
	OnFenced(ctx context.Context, e FencedEvent)
}

LeaseObserver receives callbacks after each Lease operation. All methods are called synchronously. Implementations must not block or panic. The zero value of Config.Observer is nil; the library substitutes a no-op observer.

type Operation added in v0.4.0

type Operation uint8

Operation identifies the Lease operation that triggered a fencing event.

const (
	OperationCheckpoint Operation = iota
	OperationRenew
	OperationRelease
)

Operation values for fencing events.

type ReadCheckpointEvent added in v0.4.0

type ReadCheckpointEvent struct {
	Token        Token
	Duration     time.Duration
	CleanHandoff bool
	Size         int
	Err          error
}

ReadCheckpointEvent carries the result of a ReadCheckpoint call. Size is len of the returned state slice; 0 if nil.

type ReleaseEvent added in v0.4.0

type ReleaseEvent struct {
	Token    Token
	Duration time.Duration
	Err      error
}

ReleaseEvent carries the result of a Release call.

type RenewEvent added in v0.4.0

type RenewEvent struct {
	Token    Token
	Duration time.Duration
	Attempt  int
	Err      error
}

RenewEvent carries the result of a Renew call. Attempt is 1 on the first attempt. The renewal goroutine increments Attempt on each retry attempt within the backoff loop. Direct calls to Renew from caller code always produce Attempt: 1.

type RenewalOption

type RenewalOption func(*renewalConfig)

RenewalOption is a functional option for StartRenewal.

func WithRenewalBackoff added in v0.5.0

func WithRenewalBackoff(initial, maxInterval time.Duration, jitter float64) RenewalOption

WithRenewalBackoff configures the exponential backoff policy used by the renewal goroutine when Renew returns a non-fencing, non-nil error. The initial parameter is the first retry interval; maxInterval caps the interval after growth. The jitter parameter is a fraction in [0, 1]; the actual wait is drawn from [base, base+jitter*base]. Defaults: initial=100ms, max=5s, jitter=0.20. Clamping (in order): initial floors to 1ms if <= 0; max floors to 1ms if <= 0; jitter clamps to [0, 1]; finally initial caps at max if initial > max.

func WithRenewalInterval

func WithRenewalInterval(d time.Duration) RenewalOption

WithRenewalInterval sets the time between renewal attempts. Default: TTL/2. Zero or negative values are ignored — the default is used silently.

type Token

type Token struct {
	// contains filtered or unexported fields
}

Token represents a currently held lease. It is returned by Acquire and Renew and must be passed back to Checkpoint, Renew, Release, ReadCheckpoint, and StartRenewal. All fields are unexported; use accessor methods to read them.

func (Token) ExpiresAt

func (t Token) ExpiresAt() time.Time

ExpiresAt returns the wall-clock time at which the lease expires.

func (Token) FencingToken

func (t Token) FencingToken() uint64

FencingToken returns the monotonically increasing token that prevents stale operations on the lease. If the lease is reassigned, the token changes.

func (Token) HolderID

func (t Token) HolderID() string

HolderID returns the identifier of the entity holding the lease.

func (Token) String

func (t Token) String() string

String returns a string representation of the Token.

func (Token) WorkID

func (t Token) WorkID() string

WorkID returns the identifier for the unit of work being leased.

Directories

Path Synopsis
conformance
Package conformance provides a backend-agnostic Ginkgo specification that any worklease backend must satisfy.
Package conformance provides a backend-agnostic Ginkgo specification that any worklease backend must satisfy.
Package testutil is a generated GoMock package.
Package testutil is a generated GoMock package.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL