defmodule Exkad.Node.Buckets do use GenServer use Bitwise require Logger alias Exkad.Node alias Exkad.Node.Descriptor, as: ND @k 10 def start_link(opts) do GenServer.start_link(__MODULE__, opts) end def k do @k end def init(%{node: node, parent: parent} = options) do buckets = node.id |> Node.id_to_bin |> Enum.map(fn _ -> [] end) Exkad.Node.register(parent, :buckets, self) Logger.debug("Buckets have been started in #{inspect self}") {:ok, %{buckets: buckets, node: node}} end def dump(buckets) do GenServer.call(buckets, :dump) end def add(buckets, node) do Logger.debug("Adding #{node} to buckets of #{inspect self}") GenServer.call(buckets, {:add, node}) end def add_all(buckets, nodes) do # Enum.reduce(nodes, state, fn(desc, acc) -> # add_to_bucket(acc, desc) # end) Enum.map(nodes, fn node -> add(buckets, node) end) end def k_closest(buckets, to_id) do GenServer.call(buckets, {:k_closest, to_id}) end def prefix_length([a | this_rest], [a | other_rest], acc) do prefix_length(this_rest, other_rest, acc + 1) end def prefix_length(_, _, acc), do: acc def prefix_length(this, other) do prefix_length(Node.id_to_bin(this), Node.id_to_bin(other), 0) end # # xor distance between from_id and to_id # def distance_from(from_id, to_id) do Enum.zip(Node.id_to_bin(from_id), Node.id_to_bin(to_id)) |> Enum.map(fn {f, t} -> f ^^^ t end) end # # Get the k_closest_nodes to `to_id` in local buckets # def k_closest_nodes_to(nodes, to_id) do nodes |> Enum.uniq |> Enum.map(fn %ND{id: id, loc: loc} = nd -> {distance_from(id, to_id), nd} end) |> Enum.sort(fn {a, _}, {b, _} -> a <= b end) |> Enum.take(@k) |> Enum.map(fn {_distance, desc} -> desc end) end def handle_call({:add, %ND{id: id}}, _from, %{node: %ND{id: id}} = state) do {:reply, :ok, state} end def handle_call({:add, add_node}, _from, %{node: node, buckets: buckets} = state) do %ND{id: add_id} = add_node index = prefix_length(node.id, add_id) bucket = [add_node | Enum.at(buckets, index)] |> Enum.uniq |> Enum.take(@k) buckets = List.replace_at(buckets, index, bucket) {:reply, :ok, Enum.into(%{buckets: buckets}, state)} end def handle_call({:k_closest, to_id}, _from, %{buckets: buckets} = state) do k_closest = List.flatten(buckets) |> k_closest_nodes_to(to_id) {:reply, k_closest, state} end def handle_call(:dump, _from, %{buckets: buckets} = state) do {:reply, buckets, state} end end