store

package module
v0.0.0-...-c587cea Latest Latest
Warning

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

Go to latest
Published: Aug 1, 2023 License: MIT Imports: 8 Imported by: 0

README

barney.ci/go-store

GoDoc GitHub

Pure-Go, cross-platform file locking and atomic swapping library.

go-store provides functions to streamline the following use-cases:

  • Creating PID lockfiles
  • Creating files that are safe to read and write from among many processes
  • Atomically updating files using common marshalling libraries

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

Examples

Constants

This section is empty.

Variables

View Source
var ErrRetry = errors.New("the operation needs to be retried")
View Source
var ErrWouldBlock = &likeError{Err: errWouldBlock, Like: unix.EWOULDBLOCK}

Functions

func Lock

func Lock(ctx context.Context, f OSFile) error

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

func RLock(ctx context.Context, f OSFile) error

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

func TryLock(f OSFile) error

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

func TryRLock(f OSFile) error

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

func Unlock(f OSFile) error

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 Decoder

type Decoder interface {
	Decode(v any) error
}

type Encoder

type Encoder interface {
	Encode(v any) error
}

type LoadAndStoreFunc

type LoadAndStoreFunc[T any] func(ctx context.Context, val *T, err error) error

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

type OSFile interface {
	Name() string
	Fd() uintptr
}

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 New

func New[T any, E Encoder, D Decoder](newEncoder func(io.Writer) E, newDecoder func(io.Reader) D) *Store[T]

func (*Store[T]) Load

func (store *Store[T]) Load(ctx context.Context, path string, v *T) (canary any, err error)

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.

Jump to

Keyboard shortcuts

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