Packages

Aerospike driver for Elixir with an OTP-native cluster runtime

Current section

Files

Jump to
aerospike_driver lib aerospike cluster partition_map.ex
Raw

lib/aerospike/cluster/partition_map.ex

defmodule Aerospike.Cluster.PartitionMap do
@moduledoc false
# ETS-backed partition ownership map with regime guard and per-node
# partition-generation tracking.
#
# Two ETS tables back this module:
#
# * owners table — `{namespace, partition_id}` ⇒ `%PartitionOwners{}`.
# Holds the current best known replica list for each partition. An
# update with a *strictly lower* regime than the stored value is
# rejected (newer regime wins; equal regime overwrites idempotently).
#
# * node-generation table — `node_name` ⇒ `non_neg_integer()`.
# The last `partition-generation` value seen for each node. The
# tender consults this to decide whether a node's partition map
# needs refetching.
#
# All functions take table names and are pure with respect to the
# BEAM process model: any caller may *read*, but the Tender is the sole
# writer by convention.
@partitions 4096
defmodule PartitionOwners do
@moduledoc false
@enforce_keys [:regime, :replicas]
defstruct [:regime, :replicas]
@type t :: %__MODULE__{
regime: non_neg_integer(),
replicas: [String.t() | nil, ...]
}
end
@type table :: :ets.tab()
@type namespace :: String.t()
@type partition_id :: 0..4095
@type regime :: non_neg_integer()
@type node_name :: String.t()
@doc """
Creates the two ETS tables and returns their names.
`name` is used as a prefix so multiple clusters can coexist in the same VM.
Caller (the Tender) owns these tables — they inherit its lifetime.
"""
@spec create_tables(atom()) :: {owners :: atom(), node_gens :: atom()}
def create_tables(name) when is_atom(name) do
owners = table_name(name, :owners)
node_gens = table_name(name, :node_gens)
:ets.new(owners, [:set, :public, :named_table, read_concurrency: true])
:ets.new(node_gens, [:set, :public, :named_table, read_concurrency: true])
{owners, node_gens}
end
@doc """
Returns the owners of `{namespace, partition_id}` or `:unknown_partition`.
"""
@spec owners(table(), namespace(), partition_id()) ::
{:ok, PartitionOwners.t()} | {:error, :unknown_partition}
def owners(tab, namespace, partition_id)
when is_binary(namespace) and partition_id in 0..(@partitions - 1)//1 do
case :ets.lookup(tab, {namespace, partition_id}) do
[{_, %PartitionOwners{} = po}] -> {:ok, po}
[] -> {:error, :unknown_partition}
end
end
@doc """
Stores `replicas` for `{namespace, partition_id}` at `regime`.
Returns `{:error, :stale_regime}` if the currently stored entry has a
strictly higher regime. Equal regime overwrites idempotently.
"""
@spec update(table(), namespace(), partition_id(), regime(), [node_name() | nil, ...]) ::
:ok | {:error, :stale_regime}
def update(tab, namespace, partition_id, regime, [_ | _] = replicas)
when is_binary(namespace) and partition_id in 0..(@partitions - 1)//1 and
is_integer(regime) and regime >= 0 do
key = {namespace, partition_id}
case :ets.lookup(tab, key) do
[{_, %PartitionOwners{regime: current}}] when current > regime ->
{:error, :stale_regime}
_ ->
:ets.insert(tab, {key, %PartitionOwners{regime: regime, replicas: replicas}})
:ok
end
end
@doc """
Returns `true` iff every partition id 0..4095 has an entry with at least
one replica for `namespace`.
"""
@spec complete?(table(), namespace()) :: boolean()
def complete?(tab, namespace) when is_binary(namespace) do
complete?(tab, namespace, 0)
end
defp complete?(_tab, _namespace, @partitions), do: true
defp complete?(tab, namespace, partition_id) do
case :ets.lookup(tab, {namespace, partition_id}) do
[{_, %PartitionOwners{replicas: [_ | _]}}] ->
complete?(tab, namespace, partition_id + 1)
_ ->
false
end
end
@doc """
Removes every owners entry for every partition across every namespace.
"""
@spec clear(table()) :: :ok
def clear(tab) do
:ets.delete_all_objects(tab)
:ok
end
@doc """
Removes every owners entry where `node_name` appears as a replica.
Used by the Tender when a node is dropped after the failure threshold.
An entry whose last remaining replica is removed is deleted outright.
"""
@spec drop_node(table(), node_name()) :: :ok
def drop_node(tab, node_name) when is_binary(node_name) do
:ets.foldl(
fn {key, %PartitionOwners{regime: regime, replicas: replicas}}, :ok ->
case Enum.reject(replicas, &(&1 == node_name)) do
^replicas ->
:ok
[] ->
:ets.delete(tab, key)
:ok
remaining ->
:ets.insert(tab, {key, %PartitionOwners{regime: regime, replicas: remaining}})
:ok
end
end,
:ok,
tab
)
end
@doc """
Reads the last seen `partition-generation` value for `node_name`.
"""
@spec get_node_gen(table(), node_name()) :: {:ok, non_neg_integer()} | {:error, :unknown_node}
def get_node_gen(tab, node_name) when is_binary(node_name) do
case :ets.lookup(tab, node_name) do
[{_, gen}] -> {:ok, gen}
[] -> {:error, :unknown_node}
end
end
@doc """
Stores the latest `partition-generation` for `node_name`.
"""
@spec put_node_gen(table(), node_name(), non_neg_integer()) :: :ok
def put_node_gen(tab, node_name, gen)
when is_binary(node_name) and is_integer(gen) and gen >= 0 do
:ets.insert(tab, {node_name, gen})
:ok
end
@doc """
Removes the per-node partition-generation entry for `node_name`.
"""
@spec delete_node_gen(table(), node_name()) :: :ok
def delete_node_gen(tab, node_name) when is_binary(node_name) do
:ets.delete(tab, node_name)
:ok
end
@doc """
Returns the fixed partition count (4096).
"""
@spec partition_count() :: pos_integer()
def partition_count, do: @partitions
defp table_name(prefix, suffix) do
:"#{prefix}_partition_map_#{suffix}"
end
end