defmodule DaProductApp.GlobalSettings do @moduledoc """ Caches global settings from `global_table` in ShukriaMms database. - Loads settings at startup - Periodically refreshes (default 10 minutes) - Exposes `get_all/0`, `get/1` and `refresh/0` API Settings are stored in a public ETS table (`read_concurrency: true`) so that reader processes (`get/1`, `get_all/0`) fetch values concurrently and lock-free, without serializing through the GenServer. The GenServer is the sole writer: it loads/refreshes the data and writes it into ETS. The cache is a flat `%{string_key => value}` map so `get_all/0` stays backward compatible with existing consumers (e.g. `TransactionRules.merge_settings_with_global/2`) that expect scalar values. `global_table` rows are `{id, config_key, config_value, group, created_at, updated_at}` - there is no separate "extended value" column; any list-style config (e.g. `POS_ENTRY_BLOCKED`) stores its comma-separated value directly in `config_value`. """ use GenServer require Logger alias DaProductApp.Repos.ShukriaMmsRepo import Ecto.Query @name __MODULE__ @table __MODULE__ # Single key under which the full settings map is stored in ETS. @settings_key :settings # Key under which the "at least one load has succeeded" flag is stored. @loaded_key :loaded @refresh_ms Application.get_env( :da_product_app, :global_settings_refresh_ms, :timer.minutes(10) ) # Public API def start_link(_opts) do GenServer.start_link(__MODULE__, %{}, name: @name) end def get_all do case :ets.lookup(@table, @settings_key) do [{@settings_key, map}] -> map _ -> %{} end rescue ArgumentError -> # Table not yet created (e.g. process not started) - fail safe to empty. %{} end def get(key) do Map.get(get_all(), to_string(key)) end @doc """ Returns true once a `global_table` load has completed successfully at least once (even if the table itself is empty). Returns false if no load has succeeded yet (never started, still starting, or every attempt so far has errored). Read from ETS - no DB call. """ def loaded? do case :ets.lookup(@table, @loaded_key) do [{@loaded_key, true}] -> true _ -> false end rescue ArgumentError -> false end def refresh do GenServer.cast(@name, :refresh) end # Callbacks @impl true def init(_args) do :ets.new(@table, [:named_table, :set, :public, read_concurrency: true]) load_and_store(%{}) if is_integer(@refresh_ms) and @refresh_ms > 0 do :timer.send_interval(@refresh_ms, :refresh) end {:ok, %{}} end @impl true def handle_cast(:refresh, state) do load_and_store(get_all()) {:noreply, state} end @impl true def handle_info(:refresh, state) do load_and_store(get_all()) {:noreply, state} end defp load_and_store(current_state) do case load_once(current_state) do {:ok, map} -> :ets.insert(@table, {@settings_key, map}) :ets.insert(@table, {@loaded_key, true}) map {:error, ^current_state} -> current_state end end defp load_once(current_state) do query = from(g in "global_table", select: {g.config_key, g.config_value}) case ShukriaMmsRepo.all(query) do rows when is_list(rows) -> map = Enum.reduce(rows, %{}, fn {k, v}, acc -> Map.put(acc, to_string(k), v) end) {:ok, map} _ -> Logger.error( "GlobalSettings: unexpected response loading settings, keeping last-known state" ) {:error, current_state} end rescue e -> Logger.error("GlobalSettings load error: #{inspect(e)} - keeping last-known state") {:error, current_state} end end