ExZarr.Storage.FileLock (ExZarr v1.1.0)

View Source

File locking utilities for cross-process coordination on filesystem storage.

Provides advisory file locking to prevent concurrent modifications to the same chunk file by different processes or even different BEAM instances.

Features

  • Advisory locking via :file.open with :exclusive mode
  • Automatic lock release on process termination
  • Timeout support for lock acquisition
  • Compatible with NFS and other network filesystems (best-effort)

Lock Types

  • Write locks: Use :exclusive mode, prevents all other access
  • Read locks: Advisory only, multiple readers can open simultaneously

Limitations

  • Advisory locks only (not enforced by OS)
  • Network filesystem compatibility varies
  • Lock files may persist if process crashes before cleanup

Usage

# Acquire write lock
{:ok, lock} = ExZarr.Storage.FileLock.acquire_write("/path/to/chunk", timeout: 5000)

# Write data...

# Release lock
:ok = ExZarr.Storage.FileLock.release(lock)

# Or use with_lock for automatic cleanup
ExZarr.Storage.FileLock.with_write_lock("/path/to/file", fn ->
  # Write operations here
  :ok
end)

Summary

Functions

Acquires a read lock on a file.

Acquires an exclusive write lock on a file.

Checks if a write lock exists for a file.

Releases a lock.

Removes a stale lock file.

Executes a function while holding a read lock.

Executes a function while holding a write lock.

Types

lock()

@type lock() :: %{
  path: String.t(),
  lock_file: String.t(),
  fd: :file.fd() | nil,
  type: :read | :write
}

lock_error()

@type lock_error() :: :timeout | :eexist | :eacces | term()

Functions

acquire_read(path, opts \\ [])

@spec acquire_read(
  String.t(),
  keyword()
) :: {:ok, lock()} | {:error, lock_error()}

Acquires a read lock on a file.

For read locks, we use a less restrictive approach since multiple readers are allowed. This is advisory only.

Options

  • :timeout - Maximum time to wait for lock in milliseconds (default: 5000)

Returns

  • {:ok, lock} if lock acquired
  • {:error, reason} for errors

acquire_write(path, opts \\ [])

@spec acquire_write(
  String.t(),
  keyword()
) :: {:ok, lock()} | {:error, lock_error()}

Acquires an exclusive write lock on a file.

Creates a lock file with .lock extension. The lock is automatically released when the process terminates or when explicitly released.

Options

  • :timeout - Maximum time to wait for lock in milliseconds (default: 5000)
  • :retries - Number of retry attempts (default: based on timeout)

Returns

  • {:ok, lock} if lock acquired
  • {:error, :timeout} if lock couldn't be acquired
  • {:error, reason} for other errors

locked?(path)

@spec locked?(String.t()) :: boolean()

Checks if a write lock exists for a file.

Returns true if a lock file exists, false otherwise. Note: This is a best-effort check and subject to race conditions.

release(lock)

@spec release(lock()) :: :ok

Releases a lock.

Closes the lock file handle and removes the lock file if it's a write lock.

remove_stale_lock(path)

@spec remove_stale_lock(String.t()) :: :ok | {:error, atom()}

Removes a stale lock file.

Use with caution - only call this if you're certain the lock is stale (e.g., from a crashed process).

with_read_lock(path, opts \\ [], fun)

@spec with_read_lock(String.t(), keyword(), (-> result)) ::
  {:ok, result} | {:error, lock_error()}
when result: term()

Executes a function while holding a read lock.

Similar to with_write_lock/3 but for read operations.

with_write_lock(path, opts \\ [], fun)

@spec with_write_lock(String.t(), keyword(), (-> result)) ::
  {:ok, result} | {:error, lock_error()}
when result: term()

Executes a function while holding a write lock.

Automatically acquires the lock before executing and releases it after. Ensures cleanup even if the function raises an error.

Example

ExZarr.Storage.FileLock.with_write_lock("/path/to/file", timeout: 10_000, fn ->
  # Write operations here
  File.write!("/path/to/file", data)
end)