Packages
ex_esdb
0.3.2
0.11.0
0.10.0
0.9.0
0.8.0
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.1
0.6.0
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
0.0.20
0.0.19
0.0.18
0.0.17
0.0.16
0.0.15
0.0.14-alpha
0.0.13-alpha
0.0.12-alpha
0.0.11-alpha
0.0.10-alpha
0.0.9-alpha
0.0.8-alpha
0.0.6-alpha
0.0.5-alpha
0.0.4-alpha
0.0.3-alpha
0.0.2-alfa
0.0.1-alfa
ExESDB is a reincarnation of rabbitmq/khepri, specialized for use as a BEAM-native event store.
Current section
Files
Jump to
Current section
Files
lib/ex_esdb/store_registry.ex
defmodule ExESDB.StoreRegistry do
@moduledoc false
use GenServer
alias ExESDB.Themes, as: Themes
require Logger
alias UUIDv7
alias ExESDB.StoreNaming
def registry_name, do: {:store_registry, :erlang.phash2(UUIDv7.generate())}
def all_registries do
Swarm.registered()
|> Enum.filter(fn {name, _pid} -> match?({:store_registry, _}, name) end)
|> Enum.map(fn {_, pid} -> pid end)
end
def other_registries do
current = self()
all_registries()
|> Enum.reject(fn pid -> pid == current end)
end
def random_registry, do: all_registries() |> Enum.random()
####################### API #######################
def list_stores,
do:
GenServer.call(
random_registry(),
{:list_stores}
)
@doc """
Queries stores by database type (e.g., :single, :cluster).
This demonstrates the value of having richer store information
beyond just store_id.
"""
def list_stores_by_db_type(db_type) do
case list_stores() do
{:ok, stores} ->
filtered_stores =
stores
|> Enum.filter(fn %{store: store_config} ->
store_config[:db_type] == db_type
end)
{:ok, filtered_stores}
error ->
error
end
end
@doc """
Queries stores by timeout configuration.
Useful for finding stores with specific performance characteristics.
"""
def list_stores_by_timeout(timeout) do
case list_stores() do
{:ok, stores} ->
filtered_stores =
stores
|> Enum.filter(fn %{store: store_config} ->
store_config[:timeout] == timeout
end)
{:ok, filtered_stores}
error ->
error
end
end
@doc """
Gets detailed store information for a specific store_id.
Returns the full store configuration including all operational parameters.
"""
def get_store_info(store_id) do
case list_stores() do
{:ok, stores} ->
case Enum.find(stores, fn %{store: %{store_id: id}} -> id == store_id end) do
nil -> {:error, :not_found}
store -> {:ok, store}
end
error ->
error
end
end
def sync_stores do
other_registries()
|> Enum.map(fn registry ->
try do
GenServer.call(registry, {:list_stores}, 5000)
rescue
_ -> {:ok, []}
catch
:exit, _ -> {:ok, []}
end
end)
|> Enum.reduce([], fn
{:ok, stores}, acc -> stores ++ acc
_, acc -> acc
end)
|> Enum.uniq_by(fn %{store: %{store_id: id}, node: node} -> {id, node} end)
end
def announce(store, node) do
other_registries()
|> Enum.map(fn pid ->
try do
# Send announce to all existing registries and collect their stores
GenServer.call(
pid,
{:announce_store_for_node, store, node},
5000
)
rescue
_ -> {:ok, []}
catch
:exit, _ -> {:ok, []}
end
end)
|> Enum.reduce([], fn
{:ok, stores}, acc -> stores ++ acc
_, acc -> acc
end)
|> Enum.uniq_by(fn %{store: %{store_id: id}, node: node} -> {id, node} end)
end
def unregister(store, node) do
other_registries()
|> Enum.each(fn pid ->
GenServer.cast(
pid,
{:unregister_store_for_node, store, node}
)
end)
end
defp remove_store_for_node(stores, gone_id, gone_node) do
stores
|> Enum.filter(fn %{store: %{store_id: id}, node: node} ->
id != gone_id || node != gone_node
end)
end
defp maybe_add_store_for_node(stores, %{store_id: maybe_store_id} = maybe_store, maybe_node) do
case stores
|> Enum.find(fn %{store: %{store_id: id}, node: node} ->
id == maybe_store_id && node == maybe_node
end) do
nil ->
IO.puts(
Themes.store_registry(
self(),
"✍️ Registering store [#{maybe_store_id}] on node [#{inspect(maybe_node)}]"
)
)
store_with_node = %{store: maybe_store, node: maybe_node}
[store_with_node | stores]
%{store: %{store_id: store_id}, node: node} ->
IO.puts(
Themes.store_registry(
self(),
"⚠️ Store [#{store_id}] on node [#{inspect(node)}] already registered"
)
)
stores
end
end
############ CALLBACKS ############
@impl true
def handle_call(
{:announce_store_for_node, store_config, node},
_from,
%{stores: stores} = state
) do
registered_stores =
stores
|> maybe_add_store_for_node(store_config, node)
state = %{state | stores: registered_stores}
# Respond with current list of stores
{:reply, {:ok, state.stores}, state}
end
@impl true
def handle_call({:list_stores}, _from, state) do
{:reply, {:ok, state.stores}, state}
end
@impl true
def handle_cast(
{:unregister_store_for_node, %{store_id: gone_id} = _store_config, gone_node},
%{stores: stores} = state
) do
IO.puts(
Themes.store_registry(
self(),
"👋 Unregistering store [#{gone_id}] on node [#{inspect(gone_node)}]"
)
)
filtered_stores =
stores
|> remove_store_for_node(gone_id, gone_node)
state = %{state | stores: filtered_stores}
{:noreply, state}
end
@impl true
def handle_info({:announce, %{store_id: store_id} = store_config}, state) do
# Announce to other registries and collect their stores
collected_stores = announce(store_config, node())
# Merge with collected stores (avoiding duplicates with existing stores)
current_stores = state.stores || []
all_stores =
(current_stores ++ collected_stores)
|> Enum.uniq_by(fn %{store: %{store_id: id}, node: n} -> {id, n} end)
IO.puts(
Themes.store_registry(
self(),
"📢 Announced store #{store_id} from node #{node()} 📢"
)
)
new_state = %{state | stores: all_stores}
{:noreply, new_state}
end
####################### PLUMBING #######################
@impl true
def init(opts) do
Process.flag(:trap_exit, true)
Swarm.register_name(registry_name(), self())
IO.puts(Themes.store_registry(self(), "is UP"))
# Initialize state as a map with stores
init_state = %{
config: opts,
stores: []
}
# Add our own store to initial state if store_id is provided
state =
if store_id = Keyword.get(opts, :store_id) do
store_config = build_store_config(opts)
local_store = %{store: store_config, node: node()}
IO.puts(
Themes.store_registry(
self(),
"🏪 Added own store [#{inspect(store_id)}] with full config to registry."
)
)
# Schedule announcement after a short delay to avoid blocking init
Process.send_after(self(), {:announce, store_config}, 500)
%{init_state | stores: [local_store]}
end
{:ok, state}
end
def start_link(opts) do
store_id = StoreNaming.extract_store_id(opts)
name = StoreNaming.genserver_name(__MODULE__, store_id)
GenServer.start_link(
__MODULE__,
opts,
name: name
)
end
@impl true
def terminate(reason, state) do
IO.puts(
Themes.store_registry(self(), "⚠️ Terminating on #{node()} with reason: #{inspect(reason)}")
)
# Unregister this store from all other registries before shutting down
if store_id = Keyword.get(state.config, :store_id) do
store_config = build_store_config(state.config)
unregister(store_config, node())
IO.puts(
Themes.store_registry(
self(),
"👋 Unregistered store [#{inspect(store_id)}] from #{length(other_registries())} other registries."
)
)
end
:ok
end
def child_spec(opts) do
store_id = StoreNaming.extract_store_id(opts)
%{
id: StoreNaming.child_spec_id(__MODULE__, store_id),
start: {__MODULE__, :start_link, [opts]},
restart: :permanent,
shutdown: 5_000,
type: :worker
}
end
####################### HELPERS #######################
@doc """
Transforms a keyword list configuration into a rich store configuration map.
This function extracts all relevant store configuration options and creates
a structured map that can be used for store registration and querying.
"""
def build_store_config(opts) when is_list(opts) do
%{
store_id: Keyword.get(opts, :store_id),
data_dir: Keyword.get(opts, :data_dir),
timeout: Keyword.get(opts, :timeout),
db_type: Keyword.get(opts, :db_type),
pub_sub: Keyword.get(opts, :pub_sub),
reader_idle_ms: Keyword.get(opts, :reader_idle_ms),
writer_idle_ms: Keyword.get(opts, :writer_idle_ms),
store_description: Keyword.get(opts, :store_description),
# Operational metadata
created_at: Keyword.get(opts, :created_at, DateTime.utc_now()),
version: Keyword.get(opts, :version, 1),
status: Keyword.get(opts, :status, :active),
# Resource management
priority: Keyword.get(opts, :priority, :normal),
auto_start: Keyword.get(opts, :auto_start, true),
# Administrative
tags: Keyword.get(opts, :store_tags, []),
environment: Keyword.get(opts, :environment)
}
end
def build_store_config(opts), do: opts
end