Documentation ¶
Overview ¶
The store package provides filesystem locking and concurrency primitives to streamline common use-cases when writing programs that access shared state through files or directories.
It provides a pure-go, interruptible file lock implementation with the Lock type.
Index ¶
- Variables
- func Lock(ctx context.Context, f OSFile) error
- func RLock(ctx context.Context, f OSFile) error
- func TryLock(f OSFile) error
- func TryRLock(f OSFile) error
- func Unlock(f OSFile) error
- type Decoder
- type Encoder
- type LoadAndStoreFunc
- type OSFile
- type Store
- func (store *Store[T]) Load(ctx context.Context, path string, v *T) (canary any, err error)
- func (store *Store[T]) LoadAndStore(ctx context.Context, path string, mode os.FileMode, fn LoadAndStoreFunc[T]) error
- func (store *Store[T]) Store(ctx context.Context, path string, mode os.FileMode, v *T, canary any) (err error)
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ErrRetry = errors.New("the operation needs to be retried")
var ErrWouldBlock = &likeError{Err: errWouldBlock, Like: unix.EWOULDBLOCK}
Functions ¶
func Lock ¶
Lock acquires (or promotes an already acquired lock to) an exclusive lock, i.e. a lock used for writing, on the specified file.
Lock is not re-entrant. Calling Lock on an exclusive lock is a no-op.
NOTE: On Windows, Lock always releases any lock that was previously held when called. This means that callers must not assume that the lock is still held if Lock returns with an error.
Example ¶
f, err := os.Open("/tmp") if err != nil { log.Fatal(err) } defer f.Close() // Acquire an exclusive lock if err := store.Lock(context.Background(), f); err != nil { log.Fatal(err) } if err := store.Unlock(f); err != nil { log.Fatal(err) } // Closing the underlying file also releases the lock. if err := f.Close(); err != nil { log.Fatal(err) }
Output:
Example (Context) ¶
// Open and exclusive-lock /tmp f, err := os.Open("/tmp") if err != nil { log.Fatal(err) } defer f.Close() if err := store.Lock(context.Background(), f); err != nil { log.Fatal(err) } ctx, cancel := context.WithCancel(context.Background()) // Try to do the same in a goroutine done := make(chan struct{}) go func() { defer close(done) f, err := os.Open("/tmp") if err != nil { log.Fatal(err) } defer f.Close() // This hangs until the context cancels err = store.Lock(ctx, f) if err != nil { log.Print(err) } else { log.Fatal("should not have succeeded locking") } }() // Cancel the context to abort the Lock operation cancel() <-done
Output:
func RLock ¶
RLock acquires (or demotes an already acquired lock to) a shared lock, i.e. a lock used for reading, on the specified file.
RLock is not re-entrant. Calling RLock on a shared lock is a no-op.
NOTE: On Windows, RLock always releases any lock that was previously held when called. This means that callers must not assume that the lock is still held if RLock returns with an error.
func TryLock ¶
TryLock attempts to acquire (or promote an already acquired lock to) an exclusive lock, i.e. a lock used for writing, on the specified file.
If the attempt would block, TryLock returns an error wrapping ErrWouldBlock.
NOTE: On Windows, TryLock always releases any lock that was previously held when called. This means that callers must not assume that the lock is still held if TryLock returns with an error.
func TryRLock ¶
TryRLock attempts to acquire (or demote an already acquired lock to) a shared lock, i.e. a lock used for reading.
If the attempt would block, TryRLock returns an error wrapping ErrWouldBlock.
NOTE: On Windows, TryRLock always releases any lock that was previously held when called. This means that callers must not assume that the lock is still held if TryRLock returns with an error.
func Unlock ¶
Unlock releases the lock on the specified file.
Note that in almost all scenarios, closing the file is better. This is because if the underlying file handle has been duplicated (say, via dup(2) on Unix-like systems), then calling Unlock will release the underlying lock for _all_ of these file descriptors, whereas closing the file ensures that the lock gets released automatically once all file descriptors are closed.
Types ¶
type LoadAndStoreFunc ¶
LoadAndStoreFunc is the signature of the user callback called by LoadAndStore.
LoadAndStore calls the function with val set to a non-nil pointer to the value that was unmarshaled from the content of the specified file.
If the value fails to load (commonly, because the file does not exist, or less commonly, because the file fails to unmarshal), the function is still called with val set to a pointer to the zero value of T, and err is set to the error that occured during loading.
type OSFile ¶
OSFile is an interface representing a file from which a file handle may be obtained. *os.File implements it.
type Store ¶
type Store[T any] struct { // contains filtered or unexported fields }
A Store represents a way to marshal and unmarshal values of type T atomically from and to the file system.
Basic usage is:
st := store.New[Type](json.NewEncoder, json.NewDecoder) err := st.LoadAndStore(context.Background(), "/path/to/state.json", 0666, func(val *Type) error { // Use and/or modify val; it will get re-marshaled to the file return nil }) if err != nil { log.Fatal(err) }
func (*Store[T]) Load ¶
Load reads the contents of the file at path and unmarshals it into v.
Load may block if another store is in the process of writing to the file.
func (*Store[T]) LoadAndStore ¶
func (store *Store[T]) LoadAndStore(ctx context.Context, path string, mode os.FileMode, fn LoadAndStoreFunc[T]) error
LoadAndStore loads the file at path and calls the specified function with the result of that load, as if store.Load(ctx, path, &v) was called.
The user function is then free to modify that value. If it returns without an error, LoadAndStore attempts to store the value back into the file.
If the underlying file did not change since it first loaded, the store succeeds. Otherwise, it is aborted, and the process is retried, reloading the file and calling the user function for re-modification.
In effect, LoadAndStore has Compare-and-Swap semantics; the function is preferred over Load and Store when the caller needs to update partially the contents of the file.
func (*Store[T]) Store ¶
func (store *Store[T]) Store(ctx context.Context, path string, mode os.FileMode, v *T, canary any) (err error)
Store marshals v and writes the result into the specified path, overwriting its contents. This write is atomic: either all of the data has been written, or none of it, in which case the destination remains untouched. This prevents all situations where a crashing process leaves the file half-written and corrupt.
Store may block if another store is in the process of reading the file.