defmodule SuperCache.Bootstrap do @moduledoc """ Unified startup and shutdown for SuperCache. Handles both `:local` (single-node) and `:distributed` (cluster) modes. The mode is selected via the `:cluster` option: - `:local` (default) — single-node ETS cache, no replication. - `:distributed` — cluster-aware; delegates to `SuperCache.Cluster.Bootstrap`. ## Options | Option | Type | Default | Description | |---------------------|---------|-----------------|--------------------------------------| | `:key_pos` | integer | `0` | Tuple index used as the ETS key | | `:partition_pos` | integer | `0` | Tuple index used to select partition | | `:cluster` | atom | `:local` | `:local` or `:distributed` | | `:num_partition` | integer | scheduler count | Number of ETS partitions | | `:table_type` | atom | `:set` | ETS table type | | `:table_prefix` | string | `"SuperCache…"` | Prefix for ETS table atom names | Additional options for `:distributed` mode (see `SuperCache.Cluster.Bootstrap`): | Option | Type | Default | Description | |----------------------|---------|----------|------------------------------------| | `:replication_factor`| integer | `2` | Total copies (primary + replicas) | | `:replication_mode` | atom | `:async` | `:async`, `:sync`, or `:strong` | ## Examples Start with defaults (local mode): SuperCache.Bootstrap.start!() Start with custom configuration: opts = [ key_pos: 0, partition_pos: 1, num_partition: 8, table_type: :bag ] SuperCache.Bootstrap.start!(opts) Stop the cache and release all resources: SuperCache.Bootstrap.stop() """ require Logger require SuperCache.Log alias SuperCache.{Config, Partition, Storage, Buffer} @default_opts [key_pos: 0, partition_pos: 0, cluster: :local, table_type: :set] @valid_table_types [:set, :ordered_set, :bag, :duplicate_bag] @doc """ Start SuperCache with default options (local mode). Uses `key_pos: 0`, `partition_pos: 0`, `cluster: :local`, and `table_type: :set`. The number of partitions defaults to the number of online schedulers. ## Examples SuperCache.Bootstrap.start!() # => :ok """ @spec start!() :: :ok def start!(), do: start!(@default_opts) @doc """ Start SuperCache with the given keyword options. Validates all options before starting. Raises `ArgumentError` if any option is invalid or missing. ## Options See module documentation for the full list of supported options. ## Examples SuperCache.Bootstrap.start!(key_pos: 0, partition_pos: 1, num_partition: 4) # => :ok SuperCache.Bootstrap.start!(cluster: :invalid) # => ** (ArgumentError) unsupported cluster mode: :invalid """ @spec start!(keyword) :: :ok def start!(opts) when is_list(opts) do t0 = System.monotonic_time(:millisecond) Logger.info("super_cache, bootstrap, starting with options: #{inspect(opts)}") validate!(opts) result = case Keyword.get(opts, :cluster, :local) do :distributed -> Logger.info("super_cache, bootstrap, starting in distributed mode") SuperCache.Cluster.Bootstrap.start!(opts) :local -> Logger.info("super_cache, bootstrap, starting in local mode") local_start!(opts) other -> raise ArgumentError, "unsupported cluster mode: #{inspect(other)}, expected :local or :distributed" end elapsed = System.monotonic_time(:millisecond) - t0 Logger.info("super_cache, bootstrap, started successfully in #{elapsed}ms") result end @doc """ Stop SuperCache and release all ETS resources. Gracefully shuts down buffers, storage partitions, and the partition registry. Configuration is preserved so the cache can be restarted with the same settings. ## Examples SuperCache.Bootstrap.stop() # => :ok """ @spec stop() :: :ok def stop() do Logger.info("super_cache, bootstrap, stopping...") t0 = System.monotonic_time(:millisecond) result = case Config.get_config(:cluster, :local) do :distributed -> Logger.info("super_cache, bootstrap, stopping distributed mode") SuperCache.Cluster.Bootstrap.stop() :local -> local_stop() end elapsed = System.monotonic_time(:millisecond) - t0 Logger.info("super_cache, bootstrap, stopped successfully in #{elapsed}ms") result end # ── Private — local-mode lifecycle ─────────────────────────────────────────── defp local_start!(opts) do # Clear previous configuration to avoid stale state Config.clear_config() # Apply new configuration Enum.each(opts, fn {k, v} -> Config.set_config(k, v) end) # Resolve defaults for missing options num = resolve_num_partition() resolve_table_type() resolve_table_prefix() # Start components in dependency order with :ok <- start_partitions(num), :ok <- start_storage(num), :ok <- start_buffers() do Config.set_config(:started, true) Logger.info( "super_cache, bootstrap, local mode ready (#{num} partition(s), table_type: #{Config.get_config(:table_type)})" ) :ok else {:error, reason} -> Logger.error("super_cache, bootstrap, failed to start local mode: #{inspect(reason)}") # Rollback on failure local_stop() raise RuntimeError, "failed to start SuperCache: #{inspect(reason)}" end end defp local_stop() do Logger.info("super_cache, bootstrap, stopping local mode components...") # Stop in reverse dependency order Buffer.stop() SuperCache.Log.debug(fn -> "super_cache, bootstrap, buffers stopped" end) case Config.get_config(:num_partition) do nil -> SuperCache.Log.debug(fn -> "super_cache, bootstrap, no partitions to stop" end) :ok n when is_integer(n) and n > 0 -> Storage.stop(n) SuperCache.Log.debug(fn -> "super_cache, bootstrap, #{n} storage partition(s) stopped" end) other -> Logger.warning("super_cache, bootstrap, invalid num_partition config: #{inspect(other)}") :ok end Partition.stop() SuperCache.Log.debug(fn -> "super_cache, bootstrap, partition registry cleared" end) Config.set_config(:started, false) Logger.info("super_cache, bootstrap, local mode stopped") :ok end # ── Private — component startup ────────────────────────────────────────────── defp start_partitions(num) do SuperCache.Log.debug(fn -> "super_cache, bootstrap, creating #{num} partition(s)" end) try do Partition.start(num) :ok rescue err -> Logger.error("super_cache, bootstrap, failed to create partitions: #{inspect(err)}") {:error, {:partition_start_failed, err}} end end defp start_storage(num) do SuperCache.Log.debug(fn -> "super_cache, bootstrap, creating #{num} storage table(s)" end) try do Storage.start(num) :ok rescue err -> Logger.error("super_cache, bootstrap, failed to create storage tables: #{inspect(err)}") {:error, {:storage_start_failed, err}} end end defp start_buffers() do num_schedulers = Partition.get_schedulers() SuperCache.Log.debug(fn -> "super_cache, bootstrap, creating #{num_schedulers} buffer(s)" end) try do Buffer.start(num_schedulers) :ok rescue err -> Logger.error("super_cache, bootstrap, failed to create buffers: #{inspect(err)}") {:error, {:buffer_start_failed, err}} end end # ── Private — validation ───────────────────────────────────────────────────── defp validate!(opts) do unless Keyword.keyword?(opts), do: raise(ArgumentError, "options must be a keyword list, got: #{inspect(opts)}") # Validate required options for key <- [:key_pos, :partition_pos] do unless Keyword.has_key?(opts, key), do: raise(ArgumentError, "missing required option: #{inspect(key)}") value = Keyword.fetch!(opts, key) unless is_integer(value) and value >= 0, do: raise( ArgumentError, "#{inspect(key)} must be a non-negative integer, got: #{inspect(value)}" ) end # Validate cluster mode case Keyword.get(opts, :cluster, :local) do mode when mode in [:local, :distributed] -> :ok other -> raise ArgumentError, "unsupported cluster mode: #{inspect(other)}, expected :local or :distributed" end # Validate table type if provided case Keyword.get(opts, :table_type) do nil -> :ok type when type in @valid_table_types -> :ok other -> raise ArgumentError, "unsupported table_type: #{inspect(other)}, expected one of #{inspect(@valid_table_types)}" end # Validate num_partition if provided case Keyword.get(opts, :num_partition) do nil -> :ok n when is_integer(n) and n > 0 -> :ok other -> raise ArgumentError, "num_partition must be a positive integer, got: #{inspect(other)}" end # Validate replication_factor if provided case Keyword.get(opts, :replication_factor) do nil -> :ok n when is_integer(n) and n >= 1 -> :ok other -> raise ArgumentError, "replication_factor must be a positive integer, got: #{inspect(other)}" end # Validate replication_mode if provided case Keyword.get(opts, :replication_mode) do nil -> :ok mode when mode in [:async, :sync, :strong] -> :ok other -> raise ArgumentError, "unsupported replication_mode: #{inspect(other)}, expected :async, :sync, or :strong" end # Cross-validation: replication_factor should not exceed reasonable bounds case {Keyword.get(opts, :replication_factor), Keyword.get(opts, :num_partition)} do {rf, np} when is_integer(rf) and is_integer(np) and rf > np -> Logger.warning( "super_cache, bootstrap, replication_factor (#{rf}) exceeds num_partition (#{np}). " <> "This may reduce effective replication." ) :ok _ -> :ok end :ok end # ── Private — config resolution ────────────────────────────────────────────── defp resolve_num_partition() do case Config.get_config(:num_partition, :not_found) do :not_found -> n = Partition.get_schedulers() Config.set_config(:num_partition, n) Logger.info("super_cache, bootstrap, num_partition defaulted to #{n} (scheduler count)") n n when is_integer(n) and n > 0 -> Logger.info("super_cache, bootstrap, using configured num_partition: #{n}") n other -> raise ArgumentError, "invalid num_partition: #{inspect(other)}" end end defp resolve_table_type() do case Config.get_config(:table_type, :not_found) do :not_found -> Config.set_config(:table_type, :set) SuperCache.Log.debug(fn -> "super_cache, bootstrap, table_type defaulted to :set" end) t when t in @valid_table_types -> SuperCache.Log.debug(fn -> "super_cache, bootstrap, using table_type: #{inspect(t)}" end) :ok bad -> raise ArgumentError, "unsupported table_type: #{inspect(bad)}, must be one of #{inspect(@valid_table_types)}" end end defp resolve_table_prefix() do case Config.get_config(:table_prefix, :not_found) do :not_found -> Config.set_config(:table_prefix, "SuperCache.Storage.Ets") SuperCache.Log.debug(fn -> "super_cache, bootstrap, table_prefix defaulted" end) prefix when is_binary(prefix) -> SuperCache.Log.debug(fn -> "super_cache, bootstrap, using table_prefix: #{inspect(prefix)}" end) :ok other -> raise ArgumentError, "table_prefix must be a binary string, got: #{inspect(other)}" end end end