Documentation
¶
Overview ¶
Package lock provides a single-instance Redis distributed mutex with owner-only release and TTL extension, for single-execution across replicas.
Index ¶
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrNotAcquired = errors.New("lock: not acquired")
ErrNotAcquired is returned when a lock could not be obtained: by Acquire when the context ends before the lock is free, and by Do when another owner holds the lock (the wrapped function is not run).
var ErrNotHeld = errors.New("lock: not held")
ErrNotHeld is returned by Release/Extend when the lock is no longer owned by this handle (it expired or was acquired by another owner).
Functions ¶
This section is empty.
Types ¶
type Lock ¶
type Lock struct {
// contains filtered or unexported fields
}
Lock is a held lock handle. It carries the owner token so that Release and Extend act only when this handle still owns the key.
func (*Lock) Extend ¶
Extend renews the lock's TTL if it is still owned by this handle. It returns ErrNotHeld when the lock has been lost.
type Locker ¶
type Locker struct {
// contains filtered or unexported fields
}
Locker acquires distributed locks backed by a single Redis instance.
func New ¶
func New(client redis.UniversalClient, opts ...Option) *Locker
New returns a Locker using the given Redis client.
func (*Locker) Acquire ¶
Acquire blocks until it acquires key for ttl, retrying every retryDelay, until the context is cancelled or its deadline passes. It returns ErrNotAcquired (wrapping ctx.Err()) if the context ends first.
func (*Locker) Do ¶
Do acquires key for ttl, runs fn, then releases the lock. It returns ErrNotAcquired without running fn if the lock is held by another owner. The lock is released even when ctx is cancelled while fn runs. Release errors are ignored; the error from fn is returned.
Example ¶
Do runs a unit of work on at most one replica. Here two Lockers (standing in for two replicas sharing one Redis) contend for the same periodic task: while replica A holds the lock and runs the work, replica B fires concurrently, fails to acquire, and skips with ErrNotAcquired.
package main
import (
"context"
"errors"
"fmt"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/rahmadafandi/fibr/v2/lock"
"github.com/redis/go-redis/v9"
)
func main() {
mr, _ := miniredis.Run()
defer mr.Close()
client := redis.NewClient(&redis.Options{Addr: mr.Addr()})
defer func() { _ = client.Close() }()
replicaA := lock.New(client)
replicaB := lock.New(client)
ctx := context.Background()
const key = "cron:nightly-cleanup"
tryB := func() {
err := replicaB.Do(ctx, key, 30*time.Second, func() error {
fmt.Println("running on B")
return nil
})
if errors.Is(err, lock.ErrNotAcquired) {
fmt.Println("skipped on B")
}
}
_ = replicaA.Do(ctx, key, 30*time.Second, func() error {
fmt.Println("running on A")
// B fires while A still holds the lock.
tryB()
return nil
})
}
Output: running on A skipped on B
func (*Locker) TryAcquire ¶
func (l *Locker) TryAcquire(ctx context.Context, key string, ttl time.Duration) (*Lock, bool, error)
TryAcquire makes one attempt to acquire key for ttl. It returns ok=false (and a nil error) when the lock is already held by another owner; a non-nil error indicates a Redis failure.