defmodule ExSQL.Registry do @moduledoc """ An in-VM "commit version" per database path. File-backed connections share a database through its file: each persists the whole file after a committed write, and (historically) re-parsed the whole file before each read so it could observe another connection's commits. That reparse is pure waste in the common single-writer case, where a connection's in-memory `ExSQL.Database` is already authoritative. This registry lets a connection tell whether its snapshot is still current without touching the file: a monotonically increasing counter, keyed by database path, is bumped once per committed file write. A connection records the version it last synced to; when the registry version is unchanged, the reload can be skipped. The counter lives in a `:public` ETS table owned by this process (started under the application's supervision tree) so it outlives any individual connection. Reads and bumps go straight to ETS — no `GenServer` round-trip on the hot path. A file-stat fingerprint (`{mtime, size}`) is deliberately *not* used: a whole-file rewrite of a small change can keep the size identical, and POSIX `mtime` is second-resolution, so two writes plus a read within one second can look unchanged — skipping a reload that was actually required. An in-VM counter has neither problem (within one VM, the supported deployment). If the table is absent (the `:exsql` application was not started), the functions return `:no_registry` and callers fall back to always reloading — the original, correct-but-slower behavior. """ use GenServer @table __MODULE__ @doc "Starts the registry, creating its ETS table." @spec start_link(keyword()) :: GenServer.on_start() def start_link(opts \\ []) do GenServer.start_link(__MODULE__, :ok, Keyword.put_new(opts, :name, __MODULE__)) end @doc """ Returns the current commit version for `path` (0 if never written), or `:no_registry` if the registry is not running. """ @spec current_version(term()) :: non_neg_integer() | :no_registry def current_version(path) do case :ets.whereis(@table) do :undefined -> :no_registry _ref -> case :ets.lookup(@table, path) do [{^path, version}] -> version [] -> 0 end end end @doc """ Atomically bumps and returns the commit version for `path`, or `:no_registry` if the registry is not running. """ @spec bump(term()) :: non_neg_integer() | :no_registry def bump(path) do case :ets.whereis(@table) do :undefined -> :no_registry _ref -> :ets.update_counter(@table, path, {2, 1}, {path, 0}) end end @impl true def init(:ok) do _table = :ets.new(@table, [ :named_table, :public, :set, read_concurrency: true, write_concurrency: true ]) {:ok, %{}} end end