Documentation
¶
Overview ¶
Package lock excludes other processes from a directory with an flock(2) advisory lock.
The kernel drops the lock when its holder exits or crashes, so there is no stale-lock detection. Locker is the seam: FileLocker is the flock implementation, and tests substitute their own. With runs a function under a Locker and is re-entrant through its context.
Index ¶
Examples ¶
Constants ¶
const FileName = ".gotools.flock"
FileName is the lock file inside the locked directory. It is never deleted: removing it races with a waiting process.
Variables ¶
var ErrHeld = errors.New("lock held by another process")
ErrHeld matches the error Lock returns when another holder is still there at the deadline. The error is a Lock error from package errors.
ErrUnavailable matches the error Lock returns when the lock cannot be set up: the directory or file cannot be created, or locking fails. The error is an Environment error from package errors.
Functions ¶
func With ¶
With runs fn while holding l, releasing it on return or panic. dir names the locked directory; it keys re-entrancy.
It is re-entrant through ctx: fn's ctx records dir, and a With on that ctx for the same directory runs fn directly. Without that, a nested call would open a second file description, which flock treats as another holder, and wait on itself. Code that drops the ctx is another holder.
locker := lock.NewFileLocker(dir, lock.WithTimeout(10*time.Second))
err := lock.With(ctx, locker, dir, func(ctx context.Context) error {
// Work on dir. Pass ctx on: a nested lock.With for dir runs directly.
return sync(ctx)
})
if errors.Is(err, lock.ErrHeld) {
// another process is still running
}
Example ¶
With holds the lock for the duration of fn. The ctx it passes to fn records the held directory, so a function fn calls can take the same lock again without waiting on itself.
package main
import (
"context"
"fmt"
"os"
"time"
"github.com/piusalfred/gotools/internal/lock"
)
func main() {
dir, _ := os.MkdirTemp("", "lock-example")
defer os.RemoveAll(dir)
locker := lock.NewFileLocker(dir, lock.WithTimeout(time.Second))
err := lock.With(context.Background(), locker, dir, func(ctx context.Context) error {
fmt.Println("holding the lock")
// Pass ctx down: this nested call runs directly instead of waiting.
return lock.With(ctx, locker, dir, func(context.Context) error {
fmt.Println("nested call runs without waiting")
return nil
})
})
fmt.Println("error:", err)
held, _ := locker.Holder()
fmt.Println("held after return:", held.Held)
}
Output: holding the lock nested call runs without waiting error: <nil> held after return: false
Example (Busy) ¶
A caller that starts from a fresh context is another holder: while the lock is held, it gets an error matching ErrHeld once its timeout passes.
package main
import (
"context"
"errors"
"fmt"
"os"
"github.com/piusalfred/gotools/internal/lock"
)
func main() {
dir, _ := os.MkdirTemp("", "lock-example")
defer os.RemoveAll(dir)
locker := lock.NewFileLocker(dir)
_ = lock.With(context.Background(), locker, dir, func(context.Context) error {
err := lock.With(context.Background(), locker, dir, func(context.Context) error {
return nil
})
fmt.Println("busy:", errors.Is(err, lock.ErrHeld))
return nil
})
}
Output: busy: true
Types ¶
type FileLocker ¶
type FileLocker struct {
// contains filtered or unexported fields
}
FileLocker is the Locker that holds an flock(2) on a file in a directory. Build it with NewFileLocker; the fields are private so that settings can be added as options without breaking callers.
func NewFileLocker ¶
func NewFileLocker(dir string, opts ...Option) FileLocker
NewFileLocker returns the lock for the directory dir. It touches nothing on disk until Lock.
func (FileLocker) Holder ¶
func (l FileLocker) Holder() (Holder, error)
Holder implements Locker. A missing lock file means nobody holds the lock, and nothing is created. The check takes the lock for an instant and gives it back. It ignores the disabled setting: it describes the directory, not this process's settings.
type HeldError ¶
type HeldError struct {
Path string // the lock file
PID int // the recorded holder; 0 when unknown
}
HeldError is the error behind ErrHeld.
type Holder ¶
type Holder struct {
Held bool // another process holds the lock right now
PID int // the recorded holder; 0 when nobody holds it or it is unknown
}
Holder describes who holds a directory's lock.
type Lock ¶
type Lock struct {
// contains filtered or unexported fields
}
Lock is a held lock. The zero value and a nil *Lock unlock as no-ops.
type Locker ¶
type Locker interface {
// Lock takes the lock, waiting for another holder as the implementation
// is configured to. It returns an error matching ErrHeld when a holder
// was still there at the deadline, ErrUnavailable when the lock could not
// be set up, and ctx.Err() when ctx is done.
Lock(ctx context.Context) (*Lock, error)
// Holder reports whether another process holds the lock, without taking
// it or creating anything.
Holder() (Holder, error)
}
Locker is the lock on one directory. It excludes other processes, not other goroutines of this one.
type Option ¶
type Option func(*FileLocker)
Option configures a FileLocker.
func WithDisabled ¶
WithDisabled makes Lock succeed without locking.
func WithTimeout ¶
WithTimeout sets how long Lock waits for another holder. The default, 0, tries exactly once.
type UnavailableError ¶
type UnavailableError struct {
}
UnavailableError is the error behind ErrUnavailable.
func (*UnavailableError) Error ¶
func (e *UnavailableError) Error() string
func (*UnavailableError) Is ¶
func (e *UnavailableError) Is(target error) bool
func (*UnavailableError) Unwrap ¶
func (e *UnavailableError) Unwrap() error