Packages

DurableServer provides durable, distributed GenServer processes backed by object storage.

Current section

Files

Jump to
durable_server lib durable_server group_conflict_resolver.ex
Raw

lib/durable_server/group_conflict_resolver.ex

defmodule DurableServer.GroupConflictResolver do
@moduledoc false
_ = """
DurableServer-specific conflict resolution for group registry conflicts.
When a network partition heals or race conditions occur, multiple processes may
have claimed the same key. Since DurableServers use object storage as a distributed
lock, registry conflicts are extremely rare in practice. When they do occur, we
kill both processes and let the system restart cleanly — the storage lock ensures
only one will successfully re-acquire the key.
This callback runs synchronously inside the Group shard GenServer, so it must
never block. We intentionally avoid any synchronous work (no GenServer.call to
the conflicting processes, no storage lookups) and just kill both immediately.
This module is registered as the `:resolve_registry_conflict` callback for
Group instances started by DurableServer.Supervisor.
"""
require Logger
alias DurableServer.GroupMeta
def resolve(name, key, {pid1, %GroupMeta{}, _time1}, {pid2, %GroupMeta{}, _time2}) do
Logger.error(fn ->
"#{inspect(__MODULE__)}: registry conflict detected: " <>
"name=#{inspect(name)}, key=#{inspect(key)}, " <>
"pid1=#{inspect(pid1)}, pid2=#{inspect(pid2)}, killing both for clean restart"
end)
DurableServer.fatal_exit!(pid1, :registry_conflict)
DurableServer.fatal_exit!(pid2, :registry_conflict)
# Return pid1 as nominal "winner" — both are killed, so Group will briefly
# keep pid1's entry until its DOWN handler fires and cleans up the key.
pid1
end
# Non-DurableServer keys (e.g. sprite cache entries) — fall back to Group's
# default behavior: most recent timestamp wins, pid ordering as tiebreaker.
def resolve(_name, key, {pid1, _meta1, time1}, {pid2, meta2, time2}) do
{winner_pid, loser_pid} =
if time2 > time1 or (time2 == time1 and pid2 > pid1), do: {pid2, pid1}, else: {pid1, pid2}
Logger.error(fn ->
"#{inspect(__MODULE__)}: registry conflict detected: key=#{inspect(key)}, " <>
"pid1=#{inspect(pid1)}, pid2=#{inspect(pid2)}, picking #{inspect(winner_pid)} as winner"
end)
Process.exit(loser_pid, {:group_registry_conflict, key, meta2})
winner_pid
end
end