Documentation
¶
Overview ¶
Package locker provides the Locker interface and shared types for distributed lock drivers.
Example ¶
Example shows the basic Lock/Unlock cycle of a locker obtained through Storage. The dummy driver ships an in-memory locker that is convenient for tests and demos; production code wires the same interface through the etcd or TCS driver.
package main
import (
"context"
"fmt"
"log"
storage "github.com/tarantool/go-storage"
"github.com/tarantool/go-storage/driver/dummy"
)
func main() {
ctx := context.Background()
stor := storage.NewStorage(dummy.New())
lock, err := stor.NewLocker(ctx, "/locks/example")
if err != nil {
log.Fatalf("new-locker: %v", err)
}
err = lock.Lock(ctx)
if err != nil {
log.Fatalf("lock: %v", err)
}
fmt.Println("held:", lock.Key())
err = lock.Unlock(ctx)
if err != nil {
log.Fatalf("unlock: %v", err)
}
}
Output: held: /locks/example
Index ¶
Examples ¶
Constants ¶
const DefaultTTL = 60 * time.Second
Variables ¶
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") // ErrPrefixTrailingSlash is returned by Prefixed when prefix ends with "/". ErrPrefixTrailingSlash = errors.New("locker: prefix must not end with '/'") // ErrPrefixNoLeadingSlash is returned by Prefixed when a non-empty prefix // does not start with "/". ErrPrefixNoLeadingSlash = errors.New("locker: prefix must start with '/'") )
Functions ¶
func Do ¶
func Do(ctx context.Context, f Factory, name string, fn func(context.Context) error, opts ...Option) error
Do creates a Locker via f, acquires it with Lock, runs fn while the lock is held, then releases the lock. Returns whatever fn returns, joined with any error from creation, Lock, or Unlock via errors.Join — fn's result is the leading error so callers can errors.Is against domain errors without having to peel off lock-machinery errors.
The same ctx is used for create, Lock, fn, and Unlock. Callers that need a separate lifetime context (where session keepalive outlives the work context) should build the lock manually.
Example ¶
ExampleDo shows the typical "acquire, work, release" pattern. Do creates a Locker via the supplied Factory, acquires it, runs fn while held, then releases — even if fn returns an error.
package main
import (
"context"
"fmt"
"log"
storage "github.com/tarantool/go-storage"
"github.com/tarantool/go-storage/driver/dummy"
"github.com/tarantool/go-storage/locker"
)
func main() {
ctx := context.Background()
stor := storage.NewStorage(dummy.New())
err := locker.Do(ctx, stor.LockerFactory(), "/locks/do-example", func(_ context.Context) error {
fmt.Println("work under lock")
return nil
})
if err != nil {
log.Fatalf("do: %v", err)
}
}
Output: work under lock
Types ¶
type Factory ¶
type Factory interface {
NewLocker(ctx context.Context, name string, opts ...Option) (Locker, error)
}
Factory creates new Lockers bound to a storage instance.
It is the lightest "create a lock" surface — bind once via Storage.LockerFactory and pass the resulting Factory around to components that only need to acquire locks, instead of handing out the full Storage.
Any type with a NewLocker method matching this signature satisfies Factory, so Storage and Prefixed are already Factory values — no adapter needed for production wiring. FactoryFunc adapts a bare function for ad-hoc cases.
func Prefixed ¶
Prefixed returns a locker.Factory that scopes every lock name under prefix before delegating to inner. The prefix is concatenated to the caller's name with no separator, matching the convention used by storage.Prefixed: Prefixed("/ns", inner).NewLocker(ctx, "/lock", …) calls inner.NewLocker(ctx, "/ns/lock", …).
An empty prefix yields a transparent passthrough. A non-empty prefix must start with "/" (else ErrPrefixNoLeadingSlash) and must not end with "/" (else ErrPrefixTrailingSlash). Interior "/" separators are allowed (e.g. "/foo/bar").
Composition: Prefixed("/a", Prefixed("/b", inner)) ≡ Prefixed("/a/b", inner). Nested wrappers are flattened at construction so the outer prefix is the leftmost segment, mirroring storage.Prefixed.
Example ¶
ExamplePrefixed shows how locker.Prefixed lets two subcomponents share a backend while keeping their lock names in separate namespaces — both can acquire "/shared" concurrently because the effective inner names differ.
package main
import (
"context"
"fmt"
"log"
storage "github.com/tarantool/go-storage"
"github.com/tarantool/go-storage/driver/dummy"
"github.com/tarantool/go-storage/locker"
)
func main() {
ctx := context.Background()
inner := storage.NewStorage(dummy.New()).LockerFactory()
vaultA, err := locker.Prefixed("/vault-a", inner)
if err != nil {
log.Fatalf("prefixed vault-a: %v", err)
}
vaultB, err := locker.Prefixed("/vault-b", inner)
if err != nil {
log.Fatalf("prefixed vault-b: %v", err)
}
lockA, err := vaultA.NewLocker(ctx, "/shared")
if err != nil {
log.Fatalf("vault-a /shared: %v", err)
}
lockB, err := vaultB.NewLocker(ctx, "/shared")
if err != nil {
log.Fatalf("vault-b /shared: %v", err)
}
err = lockA.TryLock(ctx)
if err != nil {
log.Fatalf("lockA: %v", err)
}
err = lockB.TryLock(ctx)
if err != nil {
log.Fatalf("lockB: %v", err)
}
fmt.Println(lockA.Key())
fmt.Println(lockB.Key())
_ = lockA.Unlock(ctx)
_ = lockB.Unlock(ctx)
}
Output: /vault-a/shared /vault-b/shared
type FactoryFunc ¶
FactoryFunc adapts a bare function to the Factory interface, mirroring the http.HandlerFunc pattern. Use it when you don't have a type with a NewLocker method handy — e.g. in tests.
type Locker ¶
type Locker interface {
Lock(ctx context.Context) error
TryLock(ctx context.Context) error
Unlock(ctx context.Context) error
Key() string
// Done returns a channel that is closed when this Locker is no longer
// holding the lock — either because Unlock was called or because the
// backend's session was lost (TTL elapsed without renewal, connection
// dropped, etc.). The channel returned by Done corresponds to the most
// recent successful Lock/TryLock call; calling Done before any successful
// acquire returns an already-closed channel. The signal does not
// distinguish voluntary release from involuntary loss; callers that need
// to tell them apart must track that themselves.
Done() <-chan struct{}
}
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.