mirror of
https://github.com/hanzokv/lock.git
synced 2026-10-30 15:07:01 +00:00
github.com/hanzokv/go/v9 v9.22.0 renamed its Go package from "redis" to "kv"; the import path is unchanged. A lock library is typed against the client it wraps -- the adapter's NewPool takes the client's UniversalClient and compares against the client's Nil sentinel -- so the qualifier change is load bearing, not cosmetic. Client-driven: - go.mod: github.com/hanzokv/go/v9 v9.21.1 -> v9.22.0 - the adapter for our client speaks kv.UniversalClient / kv.Nil, imported unaliased as in hanzokv/go's own sources Our own vocabulary follows: - redis/ -> kv/, package redis -> package kv. This is redsync's own Pool/Conn/Script abstraction over a KV backend, not anyone's product. - redis/goredis/v9 -> kv/kvgo (package kvgo). That adapter wraps hanzokv/go, not go-redis; the old name said the wrong thing. Its example moves with it: examples/goredis/v9 -> examples/kvgo. - import alias redsyncredis -> redsynckv - RedisError -> KVError (and its tests) - comments and README speak kv Deliberately NOT renamed, because they name foreign things: - the redigo, rueidis, valkeygo and go-redis v6/v7/v8 adapters, which target genuinely different clients, keep their names and their own "redis." qualifiers (redigo's redis.Conn, go-redis's redis.Nil) - the server-side Lua in scripts.go: redis.call is the store's scripting API. Rewriting it would change the script source and therefore the SHA1 hashes pinned in scripts.go, which TestScripts_Hash verifies against a live server. - upstream links (redis.io distlock post, redigo attribution) Import paths move from .../v4/redis/... to .../v4/kv/...; the module major stays v4 by policy. Verified with a local redis-server: go build ./..., go vet ./..., go test -count=1 ./... and go test -race -count=1 ./... all pass, TestScripts_Hash included. Co-authored-by: Hanzo Dev <[email protected]>
322 lines
7.9 KiB
Go
322 lines
7.9 KiB
Go
package redsync
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/hanzokv/lock/v4/kv"
|
|
)
|
|
|
|
// A DelayFunc is used to decide the amount of time to wait between retries.
|
|
type DelayFunc func(tries int) time.Duration
|
|
|
|
// A Mutex is a distributed mutual exclusion lock.
|
|
type Mutex struct {
|
|
name string
|
|
expiry time.Duration
|
|
|
|
tries int
|
|
delayFunc DelayFunc
|
|
|
|
driftFactor float64
|
|
timeoutFactor float64
|
|
|
|
quorum int
|
|
|
|
genValueFunc func() (string, error)
|
|
value string
|
|
until time.Time
|
|
shuffle bool
|
|
failFast bool
|
|
setNXOnExtend bool
|
|
|
|
pools []kv.Pool
|
|
}
|
|
|
|
// Name returns mutex name (i.e. the KV key).
|
|
func (m *Mutex) Name() string {
|
|
return m.name
|
|
}
|
|
|
|
// Value returns the current random value. The value will be empty until a lock is acquired (or WithValue option is used).
|
|
func (m *Mutex) Value() string {
|
|
return m.value
|
|
}
|
|
|
|
// Until returns the time of validity of acquired lock. The value will be zero value until a lock is acquired.
|
|
func (m *Mutex) Until() time.Time {
|
|
return m.until
|
|
}
|
|
|
|
// TryLock only attempts to lock m once and returns immediately regardless of success or failure without retrying.
|
|
func (m *Mutex) TryLock() error {
|
|
return m.TryLockContext(context.Background())
|
|
}
|
|
|
|
// TryLockContext only attempts to lock m once and returns immediately regardless of success or failure without retrying.
|
|
func (m *Mutex) TryLockContext(ctx context.Context) error {
|
|
return m.lockContext(ctx, 1)
|
|
}
|
|
|
|
// Lock locks m. In case it returns an error on failure, you may retry to acquire the lock by calling this method again.
|
|
func (m *Mutex) Lock() error {
|
|
return m.LockContext(context.Background())
|
|
}
|
|
|
|
// LockContext locks m. In case it returns an error on failure, you may retry to acquire the lock by calling this method again.
|
|
func (m *Mutex) LockContext(ctx context.Context) error {
|
|
return m.lockContext(ctx, m.tries)
|
|
}
|
|
|
|
// lockContext locks m. In case it returns an error on failure, you may retry to acquire the lock by calling this method again.
|
|
func (m *Mutex) lockContext(ctx context.Context, tries int) error {
|
|
if ctx == nil {
|
|
ctx = context.Background()
|
|
}
|
|
|
|
value, err := m.genValueFunc()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var timer *time.Timer
|
|
for i := range tries {
|
|
if i != 0 {
|
|
if timer == nil {
|
|
timer = time.NewTimer(m.delayFunc(i))
|
|
} else {
|
|
timer.Reset(m.delayFunc(i))
|
|
}
|
|
|
|
select {
|
|
case <-ctx.Done():
|
|
timer.Stop()
|
|
// Exit early if the context is done.
|
|
return ErrFailed
|
|
case <-timer.C:
|
|
// Fall-through when the delay timer completes.
|
|
}
|
|
}
|
|
|
|
start := time.Now()
|
|
|
|
n, err := func() (int, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, time.Duration(int64(float64(m.expiry)*m.timeoutFactor)))
|
|
defer cancel()
|
|
return m.actOnPoolsAsync(func(pool kv.Pool) (bool, error) {
|
|
return m.acquire(ctx, pool, value)
|
|
})
|
|
}()
|
|
|
|
now := time.Now()
|
|
until := now.Add(m.expiry - now.Sub(start) - time.Duration(int64(float64(m.expiry)*m.driftFactor)))
|
|
if n >= m.quorum && now.Before(until) {
|
|
m.value = value
|
|
m.until = until
|
|
return nil
|
|
}
|
|
_, _ = func() (int, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, time.Duration(int64(float64(m.expiry)*m.timeoutFactor)))
|
|
defer cancel()
|
|
return m.actOnPoolsAsync(func(pool kv.Pool) (bool, error) {
|
|
return m.release(ctx, pool, value)
|
|
})
|
|
}()
|
|
if i == tries-1 && err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return ErrFailed
|
|
}
|
|
|
|
// Unlock unlocks m and returns the status of unlock.
|
|
func (m *Mutex) Unlock() (bool, error) {
|
|
return m.UnlockContext(context.Background())
|
|
}
|
|
|
|
// UnlockContext unlocks m and returns the status of unlock.
|
|
func (m *Mutex) UnlockContext(ctx context.Context) (bool, error) {
|
|
n, err := m.actOnPoolsAsync(func(pool kv.Pool) (bool, error) {
|
|
return m.release(ctx, pool, m.value)
|
|
})
|
|
if n < m.quorum {
|
|
return false, err
|
|
}
|
|
return true, nil
|
|
}
|
|
|
|
// Extend resets the mutex's expiry and returns the status of expiry extension.
|
|
func (m *Mutex) Extend() (bool, error) {
|
|
return m.ExtendContext(context.Background())
|
|
}
|
|
|
|
// ExtendContext resets the mutex's expiry and returns the status of expiry extension.
|
|
func (m *Mutex) ExtendContext(ctx context.Context) (bool, error) {
|
|
start := time.Now()
|
|
n, err := m.actOnPoolsAsync(func(pool kv.Pool) (bool, error) {
|
|
return m.touch(ctx, pool, m.value, int(m.expiry/time.Millisecond))
|
|
})
|
|
if n < m.quorum {
|
|
return false, err
|
|
}
|
|
now := time.Now()
|
|
until := now.Add(m.expiry - now.Sub(start) - time.Duration(int64(float64(m.expiry)*m.driftFactor)))
|
|
if now.Before(until) {
|
|
m.until = until
|
|
return true, nil
|
|
}
|
|
return false, ErrExtendFailed
|
|
}
|
|
|
|
// Valid returns true if the lock acquired through m is still valid. It may
|
|
// also return true erroneously if quorum is achieved during the call and at
|
|
// least one node then takes long enough to respond for the lock to expire.
|
|
//
|
|
// Deprecated: Use Until instead. See https://github.com/go-redsync/redsync/issues/72.
|
|
func (m *Mutex) Valid() (bool, error) {
|
|
return m.ValidContext(context.Background())
|
|
}
|
|
|
|
// ValidContext returns true if the lock acquired through m is still valid. It may
|
|
// also return true erroneously if quorum is achieved during the call and at
|
|
// least one node then takes long enough to respond for the lock to expire.
|
|
//
|
|
// Deprecated: Use Until instead. See https://github.com/go-redsync/redsync/issues/72.
|
|
func (m *Mutex) ValidContext(ctx context.Context) (bool, error) {
|
|
n, err := m.actOnPoolsAsync(func(pool kv.Pool) (bool, error) {
|
|
return m.valid(ctx, pool)
|
|
})
|
|
return n >= m.quorum, err
|
|
}
|
|
|
|
func (m *Mutex) valid(ctx context.Context, pool kv.Pool) (bool, error) {
|
|
if m.value == "" {
|
|
return false, nil
|
|
}
|
|
conn, err := pool.Get(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer conn.Close()
|
|
reply, err := conn.Get(m.name)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return m.value == reply, nil
|
|
}
|
|
|
|
func genValue() (string, error) {
|
|
b := make([]byte, 16)
|
|
_, err := rand.Read(b)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return base64.StdEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
func (m *Mutex) acquire(ctx context.Context, pool kv.Pool, value string) (bool, error) {
|
|
conn, err := pool.Get(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer conn.Close()
|
|
reply, err := conn.SetNX(m.name, value, m.expiry)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return reply, nil
|
|
}
|
|
|
|
func (m *Mutex) release(ctx context.Context, pool kv.Pool, value string) (bool, error) {
|
|
conn, err := pool.Get(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer conn.Close()
|
|
status, err := conn.Eval(deleteScript, m.name, value)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if status == int64(-1) {
|
|
return false, ErrLockAlreadyExpired
|
|
}
|
|
return status != int64(0), nil
|
|
}
|
|
|
|
func (m *Mutex) touch(ctx context.Context, pool kv.Pool, value string, expiry int) (bool, error) {
|
|
conn, err := pool.Get(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer conn.Close()
|
|
|
|
touchScript := touchScript
|
|
if m.setNXOnExtend {
|
|
touchScript = touchWithSetNXScript
|
|
}
|
|
|
|
status, err := conn.Eval(touchScript, m.name, value, expiry)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
return status != int64(0), nil
|
|
}
|
|
|
|
func (m *Mutex) actOnPoolsAsync(actFn func(kv.Pool) (bool, error)) (int, error) {
|
|
type result struct {
|
|
node int
|
|
statusOK bool
|
|
err error
|
|
}
|
|
|
|
ch := make(chan result, len(m.pools))
|
|
for node, pool := range m.pools {
|
|
go func(node int, pool kv.Pool) {
|
|
r := result{node: node}
|
|
r.statusOK, r.err = actFn(pool)
|
|
ch <- r
|
|
}(node, pool)
|
|
}
|
|
|
|
var (
|
|
n = 0
|
|
taken []int
|
|
err error
|
|
)
|
|
|
|
for range m.pools {
|
|
r := <-ch
|
|
if r.statusOK {
|
|
n++
|
|
} else if r.err == ErrLockAlreadyExpired {
|
|
err = errors.Join(err, ErrLockAlreadyExpired)
|
|
} else if r.err != nil {
|
|
err = errors.Join(err, &KVError{Node: r.node, Err: r.err})
|
|
} else {
|
|
taken = append(taken, r.node)
|
|
err = errors.Join(err, &ErrNodeTaken{Node: r.node})
|
|
}
|
|
|
|
if m.failFast {
|
|
// fast return
|
|
if n >= m.quorum {
|
|
return n, err
|
|
}
|
|
|
|
// fail fast
|
|
if len(taken) >= m.quorum {
|
|
return n, &ErrTaken{Nodes: taken}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(taken) >= m.quorum {
|
|
return n, &ErrTaken{Nodes: taken}
|
|
}
|
|
return n, err
|
|
}
|