locker

package
v1.6.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 22, 2026 License: BSD-2-Clause Imports: 5 Imported by: 0

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

View Source
const DefaultTTL = 60 * time.Second

Variables

View Source
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

func Prefixed(prefix string, inner Factory) (Factory, error)

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

type FactoryFunc func(ctx context.Context, name string, opts ...Option) (Locker, error)

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.

func (FactoryFunc) NewLocker

func (f FactoryFunc) NewLocker(ctx context.Context, name string, opts ...Option) (Locker, error)

NewLocker satisfies Factory by invoking f.

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.

type Option

type Option func(*Options)

func WithTTL

func WithTTL(d time.Duration) Option

type Options

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
}

Options holds configuration for a Locker instance.

func ApplyOptions

func ApplyOptions(opts []Option) Options

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL