Packages

A comprehensive Elixir object system with AI integration, hierarchical coordination, and meta-schema evolution capabilities

Current section

Files

Jump to
object lib object_distributed_registry.ex
Raw

lib/object_distributed_registry.ex

defmodule Object.DistributedRegistry do
@moduledoc """
Distributed registry for Objects using Kademlia DHT algorithm.
Provides a decentralized, fault-tolerant registry for object discovery
across network boundaries. Implements Kademlia's XOR-based routing with
k-buckets for efficient O(log n) lookups.
## Features
- Kademlia DHT with 160-bit node IDs
- K-bucket routing table management
- Iterative lookup procedures
- Node liveness checking via PING/PONG
- Automatic republishing of values
- Byzantine fault tolerance mechanisms
- NAT-aware node addressing
"""
use GenServer
require Logger
alias Object.{NetworkTransport, NetworkProtocol}
@node_id_bits 160
@k_bucket_size 20 # Kademlia k parameter
@alpha 3 # Concurrency parameter
@republish_interval 3600_000 # 1 hour
@refresh_interval 900_000 # 15 minutes
@expiration_time 86400_000 # 24 hours
@type node_id :: <<_::160>>
@type node_info :: %{
id: node_id(),
address: String.t(),
port: non_neg_integer(),
last_seen: DateTime.t(),
rtt: non_neg_integer() | nil,
reputation: float()
}
@type routing_table :: %{
buckets: %{non_neg_integer() => [node_info()]},
self_id: node_id()
}
@type stored_value :: %{
key: binary(),
value: term(),
publisher: node_id(),
timestamp: DateTime.t(),
ttl: non_neg_integer()
}
@type state :: %{
node_id: node_id(),
routing_table: routing_table(),
storage: %{binary() => stored_value()},
pending_queries: %{reference() => query_state()},
transport: pid(),
config: map()
}
@type query_state :: %{
type: :find_node | :find_value | :store,
target: binary(),
visited: MapSet.t(),
active: MapSet.t(),
best_nodes: [node_info()],
from: GenServer.from() | nil,
start_time: DateTime.t()
}
# Client API
@doc """
Starts the distributed registry.
"""
def start_link(opts \\ []) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end
@doc """
Registers a local object in the distributed registry.
"""
@spec register_object(String.t(), Object.t()) :: :ok | {:error, term()}
def register_object(object_id, object) do
GenServer.call(__MODULE__, {:register_object, object_id, object})
end
@doc """
Looks up an object by ID in the distributed registry.
"""
@spec lookup_object(String.t()) :: {:ok, Object.t()} | {:error, :not_found}
def lookup_object(object_id) do
GenServer.call(__MODULE__, {:lookup_object, object_id}, 10_000)
end
@doc """
Finds nodes close to a given ID.
"""
@spec find_node(binary()) :: {:ok, [node_info()]} | {:error, term()}
def find_node(target_id) do
GenServer.call(__MODULE__, {:find_node, target_id})
end
@doc """
Bootstraps the node into the DHT network.
"""
@spec bootstrap([{String.t(), non_neg_integer()}]) :: :ok | {:error, term()}
def bootstrap(bootstrap_nodes) do
GenServer.call(__MODULE__, {:bootstrap, bootstrap_nodes})
end
@doc """
Gets the current node's ID.
"""
@spec get_node_id() :: node_id()
def get_node_id do
GenServer.call(__MODULE__, :get_node_id)
end
@doc """
Adds a peer to the routing table.
## Parameters
- `peer_id`: Node ID of the peer
- `address`: IP address of the peer
- `port`: Port number of the peer
"""
@spec add_peer(node_id(), String.t(), non_neg_integer()) :: :ok | {:error, term()}
def add_peer(peer_id, address, port) do
GenServer.call(__MODULE__, {:add_peer, peer_id, address, port})
end
# Server Callbacks
@impl true
def init(opts) do
# Generate or load node ID
node_id = generate_node_id(opts)
# Initialize routing table
routing_table = %{
buckets: initialize_buckets(),
self_id: node_id
}
# Start network transport - handle already started case
transport_result = case Object.NetworkTransport.start_link() do
{:ok, pid} -> {:ok, pid}
{:error, {:already_started, pid}} -> {:ok, pid}
other -> other
end
case transport_result do
{:ok, transport} ->
state = %{
node_id: node_id,
routing_table: routing_table,
storage: %{},
pending_queries: %{},
transport: transport,
config: %{
listen_port: Keyword.get(opts, :port, 4000),
bootstrap_nodes: Keyword.get(opts, :bootstrap_nodes, []),
storage_limit: Keyword.get(opts, :storage_limit, 1000)
}
}
# Schedule periodic tasks
schedule_refresh()
schedule_republish()
schedule_cleanup()
{:ok, state}
{:error, reason} ->
{:stop, reason}
end
end
@impl true
def handle_call({:register_object, object_id, object}, _from, state) do
key = hash_key(object_id)
# Store locally
stored_value = %{
key: key,
value: object,
publisher: state.node_id,
timestamp: DateTime.utc_now(),
ttl: @expiration_time
}
new_state = put_in(state.storage[key], stored_value)
# Initiate DHT store operation
initiate_store(new_state, key, object)
{:reply, :ok, new_state}
end
@impl true
def handle_call({:lookup_object, object_id}, from, state) do
key = hash_key(object_id)
# Check local storage first
case Map.get(state.storage, key) do
%{value: object} ->
{:reply, {:ok, object}, state}
nil ->
# Initiate DHT lookup
query_ref = make_ref()
query_state = %{
type: :find_value,
target: key,
visited: MapSet.new(),
active: MapSet.new(),
best_nodes: get_closest_nodes(state.routing_table, key, @alpha),
from: from,
start_time: DateTime.utc_now()
}
new_state = put_in(state.pending_queries[query_ref], query_state)
# Start iterative lookup
continue_lookup(new_state, query_ref)
{:noreply, new_state}
end
end
@impl true
def handle_call({:find_node, target_id}, _from, state) do
closest = get_closest_nodes(state.routing_table, target_id, @k_bucket_size)
{:reply, {:ok, closest}, state}
end
@impl true
def handle_call({:bootstrap, bootstrap_nodes}, _from, state) do
# Connect to bootstrap nodes
Enum.each(bootstrap_nodes, fn {host, port} ->
add_node(state, generate_temp_node_id(), host, port)
end)
# Find nodes close to ourselves
initiate_self_lookup(state)
{:reply, :ok, state}
end
@impl true
def handle_call({:add_peer, peer_id, address, port}, _from, state) do
# Add peer to routing table
updated_state = add_node(state, peer_id, address, port)
{:reply, :ok, updated_state}
end
@impl true
def handle_call(:get_node_id, _from, state) do
{:reply, state.node_id, state}
end
@impl true
def handle_info({:network_message, from_node, message}, state) do
case message do
{:ping, sender_id} ->
handle_ping(state, from_node, sender_id)
{:pong, sender_id} ->
handle_pong(state, from_node, sender_id)
{:find_node, sender_id, target} ->
handle_find_node(state, from_node, sender_id, target)
{:find_value, sender_id, key} ->
handle_find_value(state, from_node, sender_id, key)
{:store, sender_id, key, value} ->
handle_store(state, from_node, sender_id, key, value)
{:found_nodes, sender_id, nodes} ->
handle_found_nodes(state, from_node, sender_id, nodes)
{:found_value, sender_id, value} ->
handle_found_value(state, from_node, sender_id, value)
_ ->
Logger.warning("Unknown message type: #{inspect(message)}")
{:noreply, state}
end
end
@impl true
def handle_info(:refresh_buckets, state) do
# Refresh buckets that haven't been accessed recently
refreshed_state = refresh_stale_buckets(state)
schedule_refresh()
{:noreply, refreshed_state}
end
@impl true
def handle_info(:republish_values, state) do
# Republish stored values
republished_state = republish_stored_values(state)
schedule_republish()
{:noreply, republished_state}
end
@impl true
def handle_info(:cleanup_storage, state) do
# Remove expired values
cleaned_state = cleanup_expired_values(state)
schedule_cleanup()
{:noreply, cleaned_state}
end
# DHT Operations
defp initiate_store(state, key, value) do
# Find k closest nodes to the key
closest_nodes = get_closest_nodes(state.routing_table, key, @k_bucket_size)
# Send store requests to those nodes
Enum.each(closest_nodes, fn node ->
send_store_request(state, node, key, value)
end)
end
defp initiate_self_lookup(state) do
query_ref = make_ref()
query_state = %{
type: :find_node,
target: state.node_id,
visited: MapSet.new(),
active: MapSet.new(),
best_nodes: get_random_nodes(state.routing_table, @alpha),
from: nil,
start_time: DateTime.utc_now()
}
new_state = put_in(state.pending_queries[query_ref], query_state)
continue_lookup(new_state, query_ref)
end
defp continue_lookup(state, query_ref) do
case Map.get(state.pending_queries, query_ref) do
nil ->
state
query ->
# Select alpha nodes from best_nodes that we haven't queried
to_query = query.best_nodes
|> Enum.reject(fn node ->
MapSet.member?(query.visited, node.id) or
MapSet.member?(query.active, node.id)
end)
|> Enum.take(@alpha)
if Enum.empty?(to_query) and MapSet.size(query.active) == 0 do
# Lookup complete
finish_lookup(state, query_ref)
else
# Send queries to selected nodes
Enum.each(to_query, fn node ->
case query.type do
:find_node ->
send_find_node_request(state, node, query.target)
:find_value ->
send_find_value_request(state, node, query.target)
end
end)
# Update query state
new_active = Enum.reduce(to_query, query.active, fn node, acc ->
MapSet.put(acc, node.id)
end)
put_in(state.pending_queries[query_ref].active, new_active)
end
end
end
defp finish_lookup(state, query_ref) do
case Map.get(state.pending_queries, query_ref) do
%{type: :find_value, from: from} when not is_nil(from) ->
GenServer.reply(from, {:error, :not_found})
_ ->
:ok
end
Map.delete(state.pending_queries, query_ref)
end
# Message Handlers
defp handle_ping(state, from_node, sender_id) do
# Update routing table
new_state = update_routing_table(state, sender_id, from_node)
# Send pong response
send_pong(new_state, from_node, new_state.node_id)
{:noreply, new_state}
end
defp handle_pong(state, from_node, sender_id) do
# Update routing table with responsive node
new_state = update_routing_table(state, sender_id, from_node)
{:noreply, new_state}
end
defp handle_find_node(state, from_node, sender_id, target) do
# Update routing table
new_state = update_routing_table(state, sender_id, from_node)
# Find closest nodes
closest = get_closest_nodes(new_state.routing_table, target, @k_bucket_size)
# Send response
send_found_nodes(new_state, from_node, new_state.node_id, closest)
{:noreply, new_state}
end
defp handle_find_value(state, from_node, sender_id, key) do
new_state = update_routing_table(state, sender_id, from_node)
case Map.get(new_state.storage, key) do
%{value: value} ->
# We have the value
send_found_value(new_state, from_node, new_state.node_id, value)
nil ->
# Return closest nodes instead
closest = get_closest_nodes(new_state.routing_table, key, @k_bucket_size)
send_found_nodes(new_state, from_node, new_state.node_id, closest)
end
{:noreply, new_state}
end
defp handle_store(state, from_node, sender_id, key, value) do
new_state = update_routing_table(state, sender_id, from_node)
# Store the value if we have space
if map_size(new_state.storage) < new_state.config.storage_limit do
stored_value = %{
key: key,
value: value,
publisher: sender_id,
timestamp: DateTime.utc_now(),
ttl: @expiration_time
}
newer_state = put_in(new_state.storage[key], stored_value)
{:noreply, newer_state}
else
{:noreply, new_state}
end
end
defp handle_found_nodes(state, _from_node, sender_id, nodes) do
# Process response to find_node query
new_state = Enum.reduce(state.pending_queries, state, fn {ref, query}, acc ->
if MapSet.member?(query.active, sender_id) do
# Update query state
new_visited = MapSet.put(query.visited, sender_id)
new_active = MapSet.delete(query.active, sender_id)
# Add discovered nodes to routing table and best_nodes
{updated_state, new_best} = Enum.reduce(nodes, {acc, query.best_nodes},
fn node_info, {st, best} ->
st2 = add_node_info(st, node_info)
best2 = update_best_nodes(best, node_info, query.target)
{st2, best2}
end)
updated_query = %{query |
visited: new_visited,
active: new_active,
best_nodes: new_best
}
put_in(updated_state.pending_queries[ref], updated_query)
else
acc
end
end)
# Continue lookups
final_state = Enum.reduce(Map.keys(new_state.pending_queries), new_state, fn ref, acc ->
continue_lookup(acc, ref)
end)
{:noreply, final_state}
end
defp handle_found_value(state, _from_node, sender_id, value) do
# Process response to find_value query
new_state = Enum.reduce(state.pending_queries, state, fn {ref, query}, acc ->
if query.type == :find_value and MapSet.member?(query.active, sender_id) do
# Found the value!
if query.from do
GenServer.reply(query.from, {:ok, value})
end
Map.delete(acc.pending_queries, ref)
else
acc
end
end)
{:noreply, new_state}
end
# Routing Table Management
defp initialize_buckets do
Map.new(0..(@node_id_bits - 1), fn i -> {i, []} end)
end
defp update_routing_table(state, node_id, node_info) do
bucket_index = get_bucket_index(state.node_id, node_id)
bucket = Map.get(state.routing_table.buckets, bucket_index, [])
node = %{
id: node_id,
address: node_info.address,
port: node_info.port,
last_seen: DateTime.utc_now(),
rtt: nil,
reputation: 1.0
}
# Check if node already exists in bucket
case Enum.find_index(bucket, fn n -> n.id == node_id end) do
nil ->
# Add new node if bucket not full
if length(bucket) < @k_bucket_size do
new_bucket = [node | bucket]
put_in(state.routing_table.buckets[bucket_index], new_bucket)
else
# Bucket full - ping least recently seen node
[oldest | _rest] = Enum.reverse(bucket)
ping_node(state, oldest)
state
end
index ->
# Move to front (most recently seen)
{_old, rest} = List.pop_at(bucket, index)
new_bucket = [node | rest]
put_in(state.routing_table.buckets[bucket_index], new_bucket)
end
end
defp get_bucket_index(self_id, node_id) do
# XOR distance between IDs
distance = :crypto.exor(self_id, node_id)
# Find highest bit position
distance
|> :binary.bin_to_list()
|> Enum.find_index(&(&1 != 0))
|> case do
nil -> @node_id_bits - 1
byte_index ->
byte = :binary.at(distance, byte_index)
bit_index = highest_bit_position(byte)
byte_index * 8 + (7 - bit_index)
end
end
defp highest_bit_position(byte) do
cond do
byte >= 128 -> 7
byte >= 64 -> 6
byte >= 32 -> 5
byte >= 16 -> 4
byte >= 8 -> 3
byte >= 4 -> 2
byte >= 2 -> 1
true -> 0
end
end
defp get_closest_nodes(routing_table, target, count) do
routing_table.buckets
|> Map.values()
|> List.flatten()
|> Enum.sort_by(fn node -> xor_distance(node.id, target) end)
|> Enum.take(count)
end
defp xor_distance(id1, id2) do
:crypto.exor(id1, id2)
|> :binary.bin_to_list()
|> Integer.undigits(256)
end
defp add_node_info(state, node_info) do
update_routing_table(state, node_info.id, node_info)
end
defp update_best_nodes(best_nodes, new_node, target) do
_all_nodes = [new_node | best_nodes]
|> Enum.uniq_by(& &1.id)
|> Enum.sort_by(fn node -> xor_distance(node.id, target) end)
|> Enum.take(@k_bucket_size)
end
# Network Communication
defp send_ping(_state, node, sender_id) do
message = NetworkProtocol.create_cast(
"dht_node",
"ping",
[sender_id]
)
case NetworkProtocol.encode(message) do
{:ok, encoded} ->
NetworkTransport.send_to(node.address, node.port, encoded)
{:error, reason} ->
Logger.error("Failed to encode ping: #{inspect(reason)}")
end
end
defp send_pong(_state, node, sender_id) do
message = NetworkProtocol.create_cast(
"dht_node",
"pong",
[sender_id]
)
case NetworkProtocol.encode(message) do
{:ok, encoded} ->
NetworkTransport.send_to(node.address, node.port, encoded)
{:error, reason} ->
Logger.error("Failed to encode pong: #{inspect(reason)}")
end
end
defp send_find_node_request(state, node, target) do
message = NetworkProtocol.create_request(
"dht_node",
"find_node",
[state.node_id, target]
)
case NetworkProtocol.encode(message) do
{:ok, encoded} ->
NetworkTransport.send_to(node.address, node.port, encoded)
{:error, reason} ->
Logger.error("Failed to encode find_node: #{inspect(reason)}")
end
end
defp send_find_value_request(state, node, key) do
message = NetworkProtocol.create_request(
"dht_node",
"find_value",
[state.node_id, key]
)
case NetworkProtocol.encode(message) do
{:ok, encoded} ->
NetworkTransport.send_to(node.address, node.port, encoded)
{:error, reason} ->
Logger.error("Failed to encode find_value: #{inspect(reason)}")
end
end
defp send_store_request(state, node, key, value) do
message = NetworkProtocol.create_cast(
"dht_node",
"store",
[state.node_id, key, value]
)
case NetworkProtocol.encode(message) do
{:ok, encoded} ->
NetworkTransport.send_to(node.address, node.port, encoded)
{:error, reason} ->
Logger.error("Failed to encode store: #{inspect(reason)}")
end
end
defp send_found_nodes(_state, node, _sender_id, nodes) do
# Convert node_info to serializable format
nodes_data = Enum.map(nodes, fn n ->
%{
id: Base.encode16(n.id),
address: n.address,
port: n.port
}
end)
message = NetworkProtocol.create_response(
nil, # correlation_id would be tracked properly in full impl
nodes_data
)
case NetworkProtocol.encode(message) do
{:ok, encoded} ->
NetworkTransport.send_to(node.address, node.port, encoded)
{:error, reason} ->
Logger.error("Failed to encode found_nodes: #{inspect(reason)}")
end
end
defp send_found_value(_state, node, _sender_id, value) do
message = NetworkProtocol.create_response(
nil, # correlation_id would be tracked properly in full impl
value
)
case NetworkProtocol.encode(message) do
{:ok, encoded} ->
NetworkTransport.send_to(node.address, node.port, encoded)
{:error, reason} ->
Logger.error("Failed to encode found_value: #{inspect(reason)}")
end
end
# Utilities
defp generate_node_id(opts) do
case Keyword.get(opts, :node_id) do
nil ->
# Generate random node ID
:crypto.strong_rand_bytes(20)
id when is_binary(id) and byte_size(id) == 20 ->
id
id_string when is_binary(id_string) ->
# Hash string to get node ID
:crypto.hash(:sha, id_string)
end
end
defp generate_temp_node_id do
:crypto.strong_rand_bytes(20)
end
defp hash_key(key) when is_binary(key) do
:crypto.hash(:sha, key)
end
defp hash_key(key) do
key |> :erlang.term_to_binary() |> hash_key()
end
defp add_node(state, node_id, address, port) do
node_info = %{
id: node_id,
address: address,
port: port,
last_seen: DateTime.utc_now(),
rtt: nil,
reputation: 1.0
}
update_routing_table(state, node_id, node_info)
end
defp ping_node(state, node) do
send_ping(state, node, state.node_id)
end
defp get_random_nodes(routing_table, count) do
routing_table.buckets
|> Map.values()
|> List.flatten()
|> Enum.take_random(count)
end
# Periodic Tasks
defp schedule_refresh do
Process.send_after(self(), :refresh_buckets, @refresh_interval)
end
defp schedule_republish do
Process.send_after(self(), :republish_values, @republish_interval)
end
defp schedule_cleanup do
Process.send_after(self(), :cleanup_storage, 60_000) # Every minute
end
defp refresh_stale_buckets(state) do
# Refresh buckets that haven't been updated in a while
now = DateTime.utc_now()
Enum.reduce(state.routing_table.buckets, state, fn {index, bucket}, acc ->
if should_refresh_bucket?(bucket, now) do
# Generate random ID in this bucket's range
target = generate_id_in_bucket(state.node_id, index)
initiate_refresh_lookup(acc, target)
else
acc
end
end)
end
defp should_refresh_bucket?([], _now), do: true
defp should_refresh_bucket?(bucket, now) do
oldest = Enum.min_by(bucket, & &1.last_seen)
DateTime.diff(now, oldest.last_seen, :second) > 3600 # 1 hour
end
defp generate_id_in_bucket(self_id, bucket_index) do
# Generate ID with specific XOR distance
distance = :crypto.strong_rand_bytes(20)
# Set appropriate bit for bucket
distance_list = :binary.bin_to_list(distance)
byte_index = div(bucket_index, 8)
bit_index = rem(bucket_index, 8)
updated_list = List.update_at(distance_list, byte_index, fn byte ->
Bitwise.bor(byte, Bitwise.bsl(1, 7 - bit_index))
end)
distance_binary = :binary.list_to_bin(updated_list)
:crypto.exor(self_id, distance_binary)
end
defp initiate_refresh_lookup(state, target) do
query_ref = make_ref()
query_state = %{
type: :find_node,
target: target,
visited: MapSet.new(),
active: MapSet.new(),
best_nodes: get_closest_nodes(state.routing_table, target, @alpha),
from: nil,
start_time: DateTime.utc_now()
}
new_state = put_in(state.pending_queries[query_ref], query_state)
continue_lookup(new_state, query_ref)
end
defp republish_stored_values(state) do
# Republish values we're storing
Enum.each(state.storage, fn {key, stored_value} ->
if should_republish?(stored_value) do
initiate_store(state, key, stored_value.value)
end
end)
state
end
defp should_republish?(%{timestamp: timestamp}) do
DateTime.diff(DateTime.utc_now(), timestamp, :second) > 3600 # 1 hour
end
defp cleanup_expired_values(state) do
now = DateTime.utc_now()
new_storage = state.storage
|> Enum.reject(fn {_key, stored_value} ->
DateTime.diff(now, stored_value.timestamp, :millisecond) > stored_value.ttl
end)
|> Map.new()
%{state | storage: new_storage}
end
end