Packages
fnord
0.8.88
0.9.40
0.9.39
0.9.38
0.9.37
0.9.36
0.9.35
0.9.34
0.9.33
0.9.32
0.9.31
0.9.30
0.9.29
0.9.28
0.9.27
0.9.26
0.9.25
0.9.24
0.9.23
0.9.22
0.9.21
0.9.20
0.9.19
0.9.18
0.9.17
0.9.16
0.9.15
0.9.14
0.9.13
0.9.12
0.9.11
0.9.10
0.9.9
0.9.8
0.9.7
0.9.6
0.9.5
0.9.4
0.9.3
0.9.2
0.9.1
0.9.0
0.8.99
0.8.98
0.8.97
0.8.96
0.8.95
0.8.94
0.8.93
0.8.92
0.8.91
0.8.90
0.8.89
0.8.88
0.8.87
0.8.86
0.8.85
0.8.84
0.8.83
0.8.82
0.8.81
0.8.80
0.8.79
0.8.78
0.8.77
0.8.76
0.8.75
0.8.74
0.8.73
0.8.72
0.8.71
0.8.70
0.8.69
0.8.68
0.8.67
0.8.66
0.8.65
0.8.64
0.8.63
0.8.62
0.8.61
0.8.60
0.8.59
0.8.58
0.8.57
0.8.56
0.8.55
0.8.54
0.8.53
0.8.52
0.8.51
0.8.50
0.8.49
0.8.48
0.8.47
0.8.46
0.8.45
0.8.44
0.8.43
0.8.42
0.8.41
0.8.40
0.8.39
0.8.38
0.8.37
0.8.36
0.8.35
0.8.34
0.8.33
0.8.32
0.8.31
0.8.30
0.8.29
0.8.27
0.8.26
0.8.25
0.8.24
0.8.23
0.8.22
0.8.21
0.8.20
0.8.19
0.8.18
0.8.17
0.8.16
0.8.15
0.8.14
0.8.13
0.8.12
0.8.11
0.8.1
0.8.0
0.7.24
0.7.23
0.7.22
0.7.21
0.7.20
0.7.19
0.7.18
0.7.17
0.7.16
0.7.15
0.7.14
0.7.13
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.3
0.7.2
0.7.1
0.7.0
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.1
0.6.0
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.44
0.4.43
0.4.42
0.4.41
0.4.40
0.4.39
0.4.38
0.4.37
0.4.36
0.4.35
0.4.34
0.4.33
0.4.32
0.4.30
0.4.29
0.4.28
0.4.27
0.4.26
0.4.25
0.4.24
0.4.23
0.4.22
0.4.21
0.4.20
0.4.19
0.4.18
0.4.17
0.4.16
0.4.15
0.4.14
0.4.13
0.4.12
0.4.11
0.4.10
0.4.9
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.0
0.2.0
0.1.0
AI code archaeology
Current section
Files
Jump to
Current section
Files
lib/file_lock.ex
defmodule FileLock do
@moduledoc """
Cross-process filesystem lock helpers for arbitrary files. Uses a lock dir
with atomic stale lock takeover.
API is intentionally small:
- acquire_lock(path)
- release_lock(path)
- with_lock(path, fun, opts \\\\ [])
"""
@timeout_ms 10_000
@stale_ms 120_000
@doc """
Execute `fun` while holding a lock for `path`.
Returns:
- `{:ok, result}` when the callback returns normally
- `{:error, :lock_failed}` when the lock cannot be acquired
- `{:callback_error, exception}` when the callback raises
- any `{:error, reason}` tuple returned by the callback itself
"""
@spec with_lock(binary, (-> any), keyword) ::
{:ok, any}
| {:error, :lock_failed}
| {:error, term}
| {:callback_error, Exception.t()}
def with_lock(path, fun, _opts \\ []) do
case acquire_lock(path) do
:ok ->
try do
{:ok, fun.()}
rescue
e -> {:callback_error, e}
after
release_lock(path)
end
{:error, _reason} ->
{:error, :lock_failed}
end
end
@doc """
Release the lock directory created for `path`.
"""
@spec release_lock(path :: binary) :: :ok
def release_lock(path) do
lock_dir = path <> ".lock"
stale_name = "#{lock_dir}.released.#{:erlang.unique_integer([:positive])}"
case File.rename(lock_dir, stale_name) do
:ok ->
File.rm_rf(stale_name)
:ok
{:error, :enoent} ->
:ok
{:error, _} ->
File.rm_rf(lock_dir)
:ok
end
end
@doc """
Acquire a lock directory alongside the file at `path`.
Returns `:ok` on success, `{:error, reason}` on failure.
"""
@spec acquire_lock(path :: binary) :: :ok | {:error, term}
def acquire_lock(path) do
lock_dir = path <> ".lock"
do_acquire_lock(lock_dir, path, System.monotonic_time(:millisecond))
end
defp do_acquire_lock(lock_dir, path, start_time) do
elapsed = System.monotonic_time(:millisecond) - start_time
if elapsed >= @timeout_ms do
{:error, {:timeout, path, elapsed}}
else
case File.mkdir(lock_dir) do
:ok ->
case write_owner_info(lock_dir) do
:ok ->
:ok
{:retry} ->
:timer.sleep(10)
do_acquire_lock(lock_dir, path, System.monotonic_time(:millisecond))
end
{:error, :eexist} ->
age_ms =
case File.stat(lock_dir, time: :posix) do
{:ok, %File.Stat{mtime: mtime}} when is_integer(mtime) ->
now = System.system_time(:second)
max(0, (now - mtime) * 1_000)
_ ->
@stale_ms + 1
end
if age_ms > @stale_ms do
case attempt_stale_takeover(lock_dir) do
:ok ->
:ok
:retry ->
:timer.sleep(50)
do_acquire_lock(lock_dir, path, start_time)
{:error, reason} ->
{:error, reason}
end
else
:timer.sleep(50)
do_acquire_lock(lock_dir, path, start_time)
end
{:error, reason} ->
{:error, {:mkdir_failed, lock_dir, reason}}
end
end
end
@spec attempt_stale_takeover(binary()) :: :ok | :retry | {:error, term}
defp attempt_stale_takeover(lock_dir) do
temp_lock = "#{lock_dir}.tmp.#{:erlang.unique_integer([:positive])}"
case File.mkdir(temp_lock) do
:ok ->
stale_name = "#{lock_dir}.stale.#{:erlang.unique_integer([:positive])}"
case File.rename(lock_dir, stale_name) do
:ok ->
case File.rename(temp_lock, lock_dir) do
:ok ->
write_owner_info(lock_dir)
Services.Globals.Spawn.spawn(fn ->
"#{lock_dir}.stale.*" |> Path.wildcard() |> Enum.each(&File.rm_rf/1)
end)
:ok
{:error, _} ->
File.rm_rf(temp_lock)
:retry
end
{:error, :enoent} ->
File.rm_rf(temp_lock)
:retry
{:error, reason} ->
File.rm_rf(temp_lock)
{:error, {:rename_failed, stale_name, reason}}
end
{:error, _} ->
:retry
end
end
defp write_owner_info(lock_dir) do
owner_file = Path.join(lock_dir, "owner")
owner_info =
[
"pid: #{inspect(self())}",
"at: #{DateTime.utc_now() |> DateTime.to_iso8601()}"
]
|> Enum.join("\n")
case File.write(owner_file, owner_info) do
:ok -> :ok
{:error, :enoent} -> {:retry}
{:error, reason} -> {:error, {:write_failed, owner_file, reason}}
end
end
end