From 4ba1dab439b3987b4b73c55e2cdca3b148080c71 Mon Sep 17 00:00:00 2001 From: Eugene Blikh Date: Tue, 5 May 2026 08:13:30 +0300 Subject: [PATCH] locker: add Locker interface package Add Locker (Lock/TryLock/Unlock/Key), Options + WithTTL + ApplyOptions, sentinel errors (ErrLocked, ErrSessionExpired, ErrLockReleased, ErrUnsupported), and DefaultTTL = 60s. Part of #29 --- locker/locker.go | 55 +++++++++++++++++++++++++++++++ locker/locker_test.go | 76 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 131 insertions(+) create mode 100644 locker/locker.go create mode 100644 locker/locker_test.go diff --git a/locker/locker.go b/locker/locker.go new file mode 100644 index 0000000..5937f30 --- /dev/null +++ b/locker/locker.go @@ -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 +} diff --git a/locker/locker_test.go b/locker/locker_test.go new file mode 100644 index 0000000..63e7b3a --- /dev/null +++ b/locker/locker_test.go @@ -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) +}