File size: 2,333 Bytes
8da2481 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 | defmodule Plausible.InternalStatsApiVersion do
@moduledoc """
Tracks the effective internal stats API version across the cluster.
Increment `@api_version` when deploying a change that breaks dashboards
already loaded from a previous deployment. The FE, upon detecting a
version mismatch, reloads the page to fetch the new dashboard code.
Each app node has `@api_version` compiled in. The effective version served
to clients is the minimum across all connected nodes, fetched via
`:rpc.multicall` and refreshed every 30 seconds. This means the version
only advances once every node in a rolling deploy is running the new code,
avoiding repeated dashboard reloads during the deployment window.
"""
use GenServer
@api_version 1
@refresh_interval :timer.seconds(30)
@spec api_version() :: non_neg_integer()
def api_version, do: @api_version
@spec effective_version() :: non_neg_integer()
def effective_version() do
# Use 0 as a placeholder version until the first multicall completes.
# The FE only reloads when the received version exceeds its compiled-in
# expectation, so 0 is always safe regardless of current @api_version.
case :ets.lookup(__MODULE__, :version) do
[{:version, v}] -> v
_ -> 0
end
end
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@impl GenServer
def init(_opts) do
__MODULE__ =
:ets.new(__MODULE__, [
:named_table,
:set,
:protected,
{:read_concurrency, true}
])
{:ok, nil, {:continue, :fetch}}
end
@impl GenServer
def handle_continue(:fetch, state) do
Process.send_after(self(), :refresh, @refresh_interval)
:ets.insert(__MODULE__, {:version, fetch_cluster_min()})
{:noreply, state}
end
@impl GenServer
def handle_info(:refresh, state) do
Process.send_after(self(), :refresh, @refresh_interval)
:ets.insert(__MODULE__, {:version, fetch_cluster_min()})
{:noreply, state}
end
defp fetch_cluster_min() do
{results, _bad_nodes} = :rpc.multicall(__MODULE__, :api_version, [], :timer.seconds(5))
cluster_min(results)
end
def cluster_min(results) do
case Enum.filter(results, &is_integer/1) do
[] -> @api_version
versions -> Enum.min(versions)
end
end
end
|