Packages
fnord
0.8.50
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/settings/file_lock.ex
defmodule Settings.FileLock do
@moduledoc """
Cross-process filesystem lock helpers for settings.json. Uses `mkdir` for the
lock with atomic stale lock takeover.
Strategy:
1. Attempt to create a lock dir with a `.lock` suffix.
-> Success: we own the lock, GOTO 4
-> Failure: GOTO 2
2. If the lock dir already exists, check whether it's age is younger than the `:stale_ms` threshold.
-> Younger: sleep briefly and GOTO 1
-> Older: GOTO 3
3. Attempt to take over the stale lock dir:
a) Create a unique temp dir
b) Rename the stale lock to a .stale.* name
c) Rename temp dir to become the lock dir
-> Success at all steps: we own the lock, GOTO 4
-> Failure at any step: sleep briefly and GOTO 1
4. Write a file named `owner` in the lock dir with our PID and timestamp.
If at any time we exceed the `:timeout_ms` threshold, we raise an error.
"""
@timeout_ms 10_000
@stale_ms 120_000
@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 ->
# Safe to delete now; lock_dir path is no longer used by this release
File.rm_rf(stale_name)
:ok
{:error, :enoent} ->
# Already gone; nothing to do
:ok
{:error, _} ->
# Fallback; best-effort cleanup
File.rm_rf(lock_dir)
:ok
end
end
@doc """
Acquire a lock directory alongside the JSON file at `path`. Blocks up to
`opts[:timeout_ms]` (default 10_000ms), treats locks older than
`opts[:stale_ms]` (default 120_000ms) as stale and reclaims them.
"""
@spec acquire_lock!(path :: binary) :: :ok
def acquire_lock!(path) do
lock_dir = path <> ".lock"
do_acquire_lock(lock_dir, path, System.monotonic_time(:millisecond))
end
@spec do_acquire_lock(
lock_dir :: binary(),
path :: binary(),
start_time :: non_neg_integer()
) :: :ok
defp do_acquire_lock(lock_dir, path, start_time) do
# If we've been trying for too long, give up and raise an error.
elapsed = System.monotonic_time(:millisecond) - start_time
if elapsed >= @timeout_ms do
raise """
Timeout acquiring file lock for #{path}.
Waited #{elapsed}ms (timeout is #{@timeout_ms}ms).
"""
end
# Attempt to create the lock directory.
case File.mkdir(lock_dir) do
:ok ->
# The lock directory did not exist and we successfully created it.
# Ergo, we own the lock.
case write_owner_info(lock_dir) do
:ok ->
:ok
{:retry} ->
# Another process deleted the dir out from under us; start over.
# But don't count this against our timeout.
:timer.sleep(10)
do_acquire_lock(lock_dir, path, System.monotonic_time(:millisecond))
end
:ok
{:error, :eexist} ->
# The lock directory already exists. But is it stale? It may have been
# left behind by a crashed process. Check its age.
age_ms =
case File.stat(lock_dir, time: :posix) do
{:ok, %File.Stat{mtime: mtime}} when is_integer(mtime) ->
now = System.system_time(:second)
# Guard against clock skew going negative
max(0, (now - mtime) * 1_000)
_ ->
@stale_ms + 1
end
if age_ms > @stale_ms do
# The lock is stale, try to take it over.
case attempt_stale_takeover(lock_dir) do
:ok ->
# We successfully took over the stale lock
UI.debug("Took over stale lock", "#{path} -> #{age_ms} ms)")
:ok
:retry ->
# The take-over failed, possibly because another process took
# over the stale lock before we could. Retry until the timeout is
# reached.
:timer.sleep(50)
do_acquire_lock(lock_dir, path, start_time)
end
else
# The lock is not stale, wait and retry
:timer.sleep(50)
do_acquire_lock(lock_dir, path, start_time)
end
# Other error creating the lock directory
{:error, reason} ->
raise "Unable to create lock directory #{lock_dir}: #{inspect(reason)}"
end
end
# ----------------------------------------------------------------------------
# Atomically take over a stale lock directory. Returns :ok if takeover
# succeeded, :retry if it failed.
# ----------------------------------------------------------------------------
@spec attempt_stale_takeover(binary()) :: :ok | :retry
defp attempt_stale_takeover(lock_dir) do
temp_lock = "#{lock_dir}.tmp.#{:erlang.unique_integer([:positive])}"
case File.mkdir(temp_lock) do
:ok ->
# We have our temp lock, try to atomically replace the stale lock
stale_name = "#{lock_dir}.stale.#{:erlang.unique_integer([:positive])}"
case File.rename(lock_dir, stale_name) do
:ok ->
# Successfully moved stale lock out of the way
case File.rename(temp_lock, lock_dir) do
:ok ->
# We now own the lock!
write_owner_info(lock_dir)
# Clean up any other stale lock dirs we created in the
# background while attempting takeover.
Services.Globals.Spawn.spawn(fn ->
"#{lock_dir}.stale.*"
|> Path.wildcard()
|> Enum.each(&File.rm_rf/1)
end)
:ok
{:error, _} ->
# Someone else got there first. Clean up and retry.
File.rm_rf(temp_lock)
:retry
end
{:error, :enoent} ->
# Stale lock disappeared (most likely someone else took it)
File.rm_rf(temp_lock)
:retry
{:error, _} ->
# Can't rename (permissions?), clean up and retry
File.rm_rf(temp_lock)
:retry
end
{:error, _} ->
# Can't create temp lock for whatever reason, retry
: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
# dir was removed; caller should re-attempt acquire
{:error, :enoent} ->
{:retry}
{:error, reason} ->
raise File.Error, reason: reason, action: "write", path: owner_file
end
end
end