Current section

Files

Jump to
super_cache lib cluster cluster_bootstrap.ex
Raw

lib/cluster/cluster_bootstrap.ex

defmodule SuperCache.Cluster.Bootstrap do
@moduledoc """
Cluster-aware startup and shutdown for SuperCache.
## Start options
All options accepted by `SuperCache.Bootstrap.start!/1` are valid here,
plus the following cluster-specific keys:
| Option | Type | Default | Description |
|----------------------|-----------|----------------|------------------------------------------|
| `:cluster` | atom | `:distributed` | `:local` or `:distributed` |
| `:replication_factor`| integer | `2` | Total copies (primary + replicas) |
| `:replication_mode` | atom | `:async` | `:async`, `:sync`, or `:strong` (3PC) |
| `:num_partition` | integer | scheduler count| Number of ETS partitions |
| `:table_type` | atom | `:set` | ETS table type |
## Replication modes
- **`:async`** — fire-and-forget. Lowest latency; eventual consistency.
- **`:sync`** — synchronous delivery to all replicas before returning.
One extra RTT per write.
- **`:strong`** — three-phase commit via
`SuperCache.Cluster.ThreePhaseCommit`. Guarantees that either all
replicas apply a write or none do. Three extra RTTs per write.
## Start sequence
1. Validate options.
2. Write all options to `SuperCache.Config`.
3. Start `Partition` and `Storage` subsystems.
4. Start the `Buffer` write-buffer pool.
5. If `:replication_mode` is `:strong`, run crash-recovery via
`ThreePhaseCommit.recover/0` to resolve in-doubt transactions left
over from a previous crash.
6. If other nodes are already live, request a full sync so this node
receives a consistent snapshot of each partition.
7. Mark `:started` in config.
## Stop sequence
1. Stop the `Buffer` (flushes pending lazy writes).
2. Stop `Storage` (deletes ETS tables).
3. Stop `Partition` (clears partition registry).
4. Mark `:started` as `false`.
"""
require Logger
alias SuperCache.{Config, Partition, Storage, Buffer}
alias SuperCache.Cluster.{Manager, ThreePhaseCommit}
@default_opts [
key_pos: 0,
partition_pos: 0,
cluster: :distributed,
replication_factor: 2,
replication_mode: :async,
table_type: :set
]
@valid_table_types [:set, :ordered_set, :bag, :duplicate_bag]
@valid_rep_modes [:async, :sync, :strong]
# ── Public API ───────────────────────────────────────────────────────────────
@doc """
Start SuperCache in cluster mode with the given options.
Raises `ArgumentError` for invalid options.
"""
@spec start!(keyword) :: :ok
def start!(opts \\ @default_opts) do
Logger.info("super_cache, cluster.bootstrap, starting with opts: #{inspect(opts)}")
validate!(opts)
Config.clear_config()
Enum.each(opts, fn {k, v} -> Config.set_config(k, v) end)
num_partition = resolve_num_partition()
resolve_table_type()
resolve_table_prefix()
resolve_replication_mode()
Partition.start(num_partition)
Storage.start(num_partition)
Buffer.start(Partition.get_schedulers())
# Resolve any in-doubt 3PC transactions from a previous crash.
if Config.get_config(:replication_mode) == :strong do
Logger.info("super_cache, cluster.bootstrap, running 3PC crash recovery")
ThreePhaseCommit.recover()
end
# Pull a consistent snapshot from existing cluster members.
if Config.get_config(:cluster) == :distributed do
live = Manager.live_nodes() -- [node()]
if live != [] do
Logger.info(
"super_cache, cluster.bootstrap, requesting full sync from: #{inspect(live)}"
)
Manager.full_sync()
end
end
Config.set_config(:started, true)
Logger.info("super_cache, cluster.bootstrap, ready (#{num_partition} partitions)")
:ok
end
@doc """
Stop SuperCache and release all ETS resources.
"""
@spec stop() :: :ok
def stop() do
Buffer.stop()
case Config.get_config(:num_partition) do
nil -> :ok
n -> Storage.stop(n)
end
Partition.stop()
Config.set_config(:started, false)
:ok
end
@doc """
Returns `true` when this node is running in distributed mode and has
completed start-up.
Called remotely by `Manager.node_running?/1` via `:erpc`.
"""
@spec running?() :: boolean
def running?() do
Config.get_config(:cluster) == :distributed and
Config.get_config(:started, false) == true
end
# ── Private — validation ─────────────────────────────────────────────────────
defp validate!(opts) do
unless Keyword.keyword?(opts) do
raise ArgumentError, "options must be a keyword list"
end
for key <- [:key_pos, :partition_pos] do
unless Keyword.has_key?(opts, key) do
raise ArgumentError, "missing required option: #{inspect(key)}"
end
end
if mode = Keyword.get(opts, :replication_mode) do
unless mode in @valid_rep_modes do
raise ArgumentError,
"invalid :replication_mode #{inspect(mode)}, must be one of #{inspect(@valid_rep_modes)}"
end
end
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)
n
n ->
n
end
end
defp resolve_table_type() do
case Config.get_config(:table_type, :not_found) do
:not_found -> Config.set_config(:table_type, :set)
t when t in @valid_table_types -> :ok
bad -> raise ArgumentError, "unsupported table type: #{inspect(bad)}"
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")
_ -> :ok
end
end
defp resolve_replication_mode() do
case Config.get_config(:replication_mode, :not_found) do
:not_found -> Config.set_config(:replication_mode, :async)
m when m in @valid_rep_modes -> :ok
bad -> raise ArgumentError, "unsupported replication_mode: #{inspect(bad)}"
end
end
end