Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions locker/locker.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
// Package locker provides the Locker interface and shared types for distributed lock drivers.
package locker

import (
"context"
"errors"
"time"
)

const DefaultTTL = 60 * time.Second

var (
ErrLocked = errors.New("locker: held by another session")
ErrSessionExpired = errors.New("locker: session expired")
ErrLockReleased = errors.New("locker: lock already released")
ErrUnsupported = errors.New("locker: not supported by this driver instance")
)

// Locker acquires and releases a named distributed lock. A single Locker
// instance holds the lock at most once at a time: re-Lock on an already-held
// Locker is a no-op that returns nil. Unlock on a never-locked or
// already-released Locker returns ErrLockReleased; implementations never panic
// and never delete a foreign key.
type Locker interface {
Lock(ctx context.Context) error
TryLock(ctx context.Context) error
Unlock(ctx context.Context) error
Key() string
}

// Options holds configuration for a Locker instance.
type Options struct {
// TTL bounds how long the backend will hold the lock without a renewal.
// A zero value means no TTL.
TTL time.Duration
}

type Option func(*Options)

func WithTTL(d time.Duration) Option {
return func(o *Options) {
o.TTL = d
}
}

func ApplyOptions(opts []Option) Options {
out := Options{
TTL: DefaultTTL,
}
for _, fn := range opts {
fn(&out)
}

return out
}
76 changes: 76 additions & 0 deletions locker/locker_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Package locker provides the Locker interface and shared types for distributed lock drivers.
package locker_test

import (
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/tarantool/go-storage/locker"
)

func TestApplyOptions_DefaultTTL(t *testing.T) {
t.Parallel()

opts := locker.ApplyOptions(nil)
assert.Equal(t, locker.DefaultTTL, opts.TTL)
}

func TestApplyOptions_Override(t *testing.T) {
t.Parallel()

custom := 30 * time.Second
opts := locker.ApplyOptions([]locker.Option{locker.WithTTL(custom)})
assert.Equal(t, custom, opts.TTL)
}

func TestApplyOptions_EmptySlice(t *testing.T) {
t.Parallel()

opts := locker.ApplyOptions([]locker.Option{})
assert.Equal(t, locker.DefaultTTL, opts.TTL)
}

func TestApplyOptions_MultipleOverrides_LastWins(t *testing.T) {
t.Parallel()

first := 10 * time.Second
second := 45 * time.Second
opts := locker.ApplyOptions([]locker.Option{locker.WithTTL(first), locker.WithTTL(second)})
assert.Equal(t, second, opts.TTL)
}

func TestSentinelErrors_Identity(t *testing.T) {
t.Parallel()

errs := []error{
locker.ErrLocked,
locker.ErrSessionExpired,
locker.ErrLockReleased,
locker.ErrUnsupported,
}

for i := range errs {
for j := i + 1; j < len(errs); j++ {
require.NotEqual(t, errs[i], errs[j],
"sentinel errors at index %d and %d must differ", i, j)
}
}
}

func TestSentinelErrors_Messages(t *testing.T) {
t.Parallel()

assert.Equal(t, "locker: held by another session", locker.ErrLocked.Error())
assert.Equal(t, "locker: session expired", locker.ErrSessionExpired.Error())
assert.Equal(t, "locker: lock already released", locker.ErrLockReleased.Error())
assert.Equal(t, "locker: not supported by this driver instance", locker.ErrUnsupported.Error())
}

func TestDefaultTTL_Value(t *testing.T) {
t.Parallel()

assert.Equal(t, 60*time.Second, locker.DefaultTTL)
}
Loading