Packages

phoenix_kit

2.57.0
2.59.0 2.58.0 2.57.1 2.57.0 2.56.1 2.56.0 2.55.1 2.55.0 2.54.2 2.54.1 2.54.0 2.53.0 2.52.2 2.52.1 2.52.0 2.51.0 2.50.0 2.49.1 2.49.0 2.48.0 2.47.0 2.46.0 2.45.0 2.44.0 2.43.1 2.43.0 2.42.1 2.42.0 2.41.6 2.41.4 2.41.3 2.41.2 2.41.1 2.41.0 2.40.1 2.40.0 2.39.0 2.38.1 2.38.0 2.37.5 2.37.4 2.37.3 2.37.2 2.37.1 2.37.0 2.36.1 2.36.0 2.35.0 2.34.0 2.33.0 2.32.1 2.32.0 2.31.1 2.31.0 2.30.0 2.29.1 2.29.0 2.28.2 2.28.1 2.28.0 2.27.2 2.27.1 2.27.0 2.26.1 2.26.0 2.25.0 2.24.0 2.23.3 2.23.2 2.23.1 2.23.0 2.22.24 2.22.23 2.22.22 2.22.21 2.22.20 2.22.19 2.22.18 2.22.17 2.22.16 2.22.15 2.22.14 2.22.13 2.22.12 2.22.11 2.22.10 2.22.9 2.22.8 2.22.7 2.22.6 2.22.5 2.22.4 2.22.3 2.22.2 2.22.1 2.22.0 2.21.5 2.21.4 2.21.3 2.21.2 2.21.1 2.21.0 2.20.0 2.19.0 2.18.1 2.18.0 2.17.0 2.16.0 2.15.1 2.15.0 2.14.2 2.14.1 2.14.0 2.13.19 2.13.18 2.13.17 2.13.16 2.13.15 2.13.13 2.13.12 2.13.11 2.13.10 2.13.9 2.13.8 2.13.7 2.13.6 2.13.5 2.13.4 2.13.3 2.13.2 2.13.1 2.13.0 2.12.1 2.12.0 2.11.0 2.10.0 2.9.0 2.8.1 2.8.0 2.7.0 2.6.0 2.5.0 2.4.0 2.3.0 2.2.0 2.1.0 2.0.1 2.0.0 1.7.236 1.7.235 1.7.234 1.7.233 1.7.232 1.7.231 1.7.230 1.7.229 1.7.228 1.7.227 1.7.226 1.7.225 1.7.224 1.7.223 1.7.222 1.7.221 1.7.220 1.7.219 1.7.218 1.7.217 1.7.216 1.7.215 1.7.214 1.7.213 1.7.212 1.7.211 1.7.210 1.7.209 1.7.208 1.7.207 1.7.206 1.7.205 1.7.204 1.7.203 1.7.202 1.7.201 1.7.200 1.7.199 1.7.198 1.7.197 1.7.196 1.7.194 1.7.193 1.7.192 1.7.191 1.7.190 1.7.189 1.7.187 1.7.186 1.7.185 1.7.184 1.7.183 1.7.182 1.7.181 1.7.180 1.7.179 1.7.178 1.7.177 1.7.176 1.7.175 1.7.174 1.7.173 1.7.172 1.7.171 1.7.170 1.7.169 1.7.168 1.7.167 1.7.166 1.7.165 1.7.164 1.7.162 1.7.161 1.7.160 1.7.159 1.7.157 1.7.156 1.7.155 1.7.154 1.7.153 1.7.152 1.7.151 1.7.150 1.7.149 1.7.146 1.7.145 1.7.144 1.7.143 1.7.138 1.7.133 1.7.132 1.7.131 1.7.130 1.7.128 1.7.126 1.7.125 1.7.121 1.7.120 1.7.119 1.7.118 1.7.117 1.7.116 1.7.115 1.7.114 1.7.113 1.7.112 1.7.111 1.7.110 1.7.109 1.7.108 1.7.107 1.7.106 1.7.105 1.7.104 1.7.103 1.7.102 1.7.101 1.7.100 1.7.99 1.7.98 1.7.97 1.7.96 1.7.95 1.7.94 1.7.93 1.7.92 1.7.91 1.7.90 1.7.89 1.7.88 1.7.87 1.7.86 1.7.85 1.7.84 1.7.83 1.7.82 1.7.81 1.7.80 1.7.79 1.7.78 1.7.77 1.7.76 1.7.75 1.7.74 1.7.71 1.7.70 1.7.69 1.7.66 1.7.65 1.7.64 1.7.63 1.7.62 1.7.61 1.7.59 1.7.58 1.7.57 1.7.56 1.7.55 1.7.54 1.7.53 1.7.52 1.7.51 1.7.49 1.7.44 1.7.43 1.7.42 1.7.41 1.7.39 1.7.38 1.7.37 1.7.36 1.7.34 1.7.33 1.7.31 1.7.30 1.7.29 1.7.28 1.7.27 1.7.26 1.7.25 1.7.24 1.7.23 1.7.22 1.7.21 1.7.20 1.7.19 1.7.18 1.7.17 1.7.16 1.7.15 1.7.14 1.7.13 1.7.12 1.7.11 1.7.10 1.7.9 1.7.8 1.7.7 1.7.6 1.7.5 1.7.4 1.7.3 1.7.2 1.7.1 1.7.0 1.6.20 1.6.19 1.6.18 1.6.17 1.6.16 1.6.15 1.6.14 1.6.13 1.6.12 1.6.11 1.6.10 1.6.9 1.6.8 1.6.7 1.6.6 1.6.5 1.6.4 1.6.3 1.5.2 1.5.1 1.5.0 1.4.9 1.4.8 1.4.7 1.4.6 1.4.5 1.4.4 1.4.3 1.4.2 1.4.1 1.4.0 1.3.2 1.3.1 1.3.0 1.2.10 1.2.9 1.2.8 1.2.7 1.2.5 1.2.4 1.2.2 1.2.1 1.2.0 1.1.0 1.0.0

A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more

Current section

Files

Jump to
phoenix_kit lib modules storage library_state.ex
Raw

lib/modules/storage/library_state.ex

defmodule PhoenixKit.Modules.Storage.LibraryState do
  @moduledoc """
  What is going on with each library's storage, derived — never stored — from two
  things: how many of its files are out of date
  (`PhoenixKit.Modules.Storage.Reconciler.counts_by_library/0`, one grouped query)
  and its `storage.reconcile` run
  (`PhoenixKit.Modules.Storage.Jobs.Reconcile`, `dev_docs/plans/2026-10-03-job-runs.md`
  §6.3, R8).

  | state | when |
  |---|---|
  | `:paused` | its reconcile run is paused (or pausing) |
  | `:syncing` | a reconcile run is queued or running — "N files left" |
  | `:attention` | no run is active, and either its last run failed with files left, or the reconciler tried files and could not finish them (they wait out their retry window) |
  | `:waiting` | files are out of date and **no run** is active: the queue is down, or nothing has triggered yet — the case a global count hides |
  | `:up_to_date` | nothing is out of date |

  "Up to date" means the files carry the revisions of their library's profile and
  variant set. It does **not** prove every recorded object is still on its bucket;
  that is what a later `storage.verify` is for (§6.4).

  The map returned for a library:

      %{state: :syncing, out_of_date: 12, eligible: 12, failing: 0, run: %Run{}}

  `run` is the active run when there is one, else the last one, else `nil`.
  """

  import Ecto.Query

  alias PhoenixKit.Jobs.Run
  alias PhoenixKit.Modules.Storage.Reconciler

  @kind "storage.reconcile"

  @type state :: :up_to_date | :syncing | :paused | :attention | :waiting
  @type t :: %{
          state: state(),
          out_of_date: non_neg_integer(),
          eligible: non_neg_integer(),
          failing: non_neg_integer(),
          run: Run.t() | nil
        }

  @doc "The kind whose runs this state is about."
  @spec kind() :: String.t()
  def kind, do: @kind

  @doc """
  The state of each of `library_uuids`, as a map keyed by the uuid string: three
  queries however many libraries (the counts, the active runs, the last runs).
  """
  @spec for_libraries([String.t()]) :: %{String.t() => t()}
  def for_libraries(library_uuids) do
    uuids = Enum.map(library_uuids, &to_string/1)
    counts = Reconciler.counts_by_library(cast(uuids))
    active = active_runs(uuids)
    last = last_runs(uuids)

    Map.new(uuids, fn uuid ->
      {uuid, build(Map.get(counts, uuid), Map.get(active, uuid), Map.get(last, uuid))}
    end)
  end

  @doc "The state of one library."
  @spec for_library(String.t()) :: t()
  def for_library(library_uuid) do
    uuid = to_string(library_uuid)
    Map.fetch!(for_libraries([uuid]), uuid)
  end

  @doc false
  @spec build(map() | nil, Run.t() | nil, Run.t() | nil) :: t()
  def build(counts, active, last) do
    %{out_of_date: out, eligible: eligible, failing: failing} =
      counts || %{out_of_date: 0, eligible: 0, failing: 0}

    %{
      state: state(out, failing, active, last),
      out_of_date: out,
      eligible: eligible,
      failing: failing,
      run: active || last
    }
  end

  defp state(_out, _failing, %Run{state: state}, _last) when state in ~w(paused pausing),
    do: :paused

  defp state(_out, _failing, %Run{}, _last), do: :syncing

  defp state(out, failing, nil, last) do
    cond do
      failing > 0 -> :attention
      out > 0 and match?(%Run{state: "failed"}, last) -> :attention
      out > 0 -> :waiting
      true -> :up_to_date
    end
  end

  # The unfinished reconcile run of each library (there is at most one: the active
  # index allows no more).
  defp active_runs(uuids) do
    from(r in Run,
      where: r.kind == @kind and r.state in ^Run.active_states() and r.scope_type == "library",
      where: r.scope_uuid in ^cast(uuids)
    )
    |> repo().all()
    |> Map.new(&{to_string(&1.scope_uuid), &1})
  end

  # The most recent reconcile run of each library, finished or not.
  defp last_runs(uuids) do
    from(r in Run,
      where: r.kind == @kind and r.scope_type == "library" and r.scope_uuid in ^cast(uuids),
      distinct: r.scope_uuid,
      order_by: [asc: r.scope_uuid, desc: r.inserted_at, desc: r.uuid]
    )
    |> repo().all()
    |> Map.new(&{to_string(&1.scope_uuid), &1})
  end

  defp cast(uuids), do: for(uuid <- uuids, {:ok, cast} <- [Ecto.UUID.cast(uuid)], do: cast)

  defp repo, do: PhoenixKit.RepoHelper.repo()
end