Current section

Files

Jump to
eddb lib database Worker.ex
Raw

lib/database/Worker.ex

defmodule Database.Worker do
alias Utilities.Serialize
alias Database.Topic
alias Database.Entry
require Logger
use GenServer, restart: :permanent
def init(init_arg) do
{:ok, init_arg}
end
def start_link({index}) do
GenServer.start_link(__MODULE__, [], name: :"db_worker_#{index}")
end
def handle_call({:get_state}, _caller_pid, state) do
{:reply, state, state}
end
def handle_call({:get, topic, key}, _caller_pid, state) do
aggregated_topics = get_topic(state, topic)
sorted_topics =
aggregated_topics
# Pre-Select only topics that contain the key
|> Stream.filter(fn topic ->
topic.entries
|> Enum.any?(fn entry -> String.equivalent?(entry.key, key) end)
end)
# Get the topic that contains the entry with the highest timestamp
|> Enum.sort_by(
fn topic ->
history =
topic.entries
|> Stream.filter(fn entry -> String.equivalent?(entry.key, key) end)
|> Enum.map(& &1.history)
# Get the first matching history array
|> List.first()
# Get the first element from history array
|> List.first()
history.timestamp
end,
:desc
)
entry =
sorted_topics
|> List.first()
|> Topic.get_entry(key)
{:reply, entry, state}
end
def handle_call({:get_local, topic, key}, _caller_pid, state) do
entry =
get_topic_local(state, topic)
|> Topic.get_entry(key)
{:reply, entry, state}
end
def handle_call({:put, topic_name, key, data}, _caller_pid, state) do
topic =
case get_topic_local(state, topic_name) do
nil ->
Topic.new(topic_name)
topic ->
topic
end
entry =
cond do
# Entry is present in topic, we need to append our data there
Topic.contains_entry_with_key?(topic, key) ->
topic.entries
|> Enum.find(&String.equivalent?(key, &1.key))
|> Entry.update(data)
# Default: Topic does not yet contain the entry
true ->
Entry.new(key, data)
end
new_topic = Topic.replace_entry(topic, entry)
new_state =
state
|> Enum.filter(&(!String.equivalent?(&1.topic, topic_name)))
# Store data on disc
Utilities.Serialize.store_object("topic_" <> topic <> ".bin", new_topic)
{:reply, entry, [new_topic | new_state]}
end
def handle_call({:put, topic_name, key, old_data, data}, caller_pid, state) do
{_, result, _} = handle_call({:get, topic_name, key}, caller_pid, state)
cond do
List.first(result.history).data == old_data ->
handle_call({:put, topic_name, key, data}, caller_pid, state)
{:reply, :ok, state}
true ->
# The data has changed
{:reply, :fail, state}
end
end
def handle_call({:sync, topic_name}, _caller_pid, state) do
topics = get_topic(state, topic_name)
entries =
topics
|> Enum.map(fn topic ->
Entry.get_keys(topic.entries)
|> Enum.map(fn key ->
Entry.combine(topic.entries, key)
end)
end)
filtered_state =
state
|> Enum.filter(&(!String.equivalent?(&1.topic, topic_name)))
new_state = [
%Topic{
topic: topic_name,
entries: entries
},
filtered_state
]
{:reply, new_state, new_state}
end
@moduledoc """
Find a topic not just locally, but on all nodes.
This works as follows:
1. Get all connected nodes
2. Get the pid of the equivalent workers on the other nodes
3. Call the get_topic_local method on the distante nodes
4. Aggregate the data to a list
5. Merge the list with local data and drop all *nil* values
"""
def get_topic(state, topic) do
db_worker_index = Database.Database.get_worker_index(self())
topics =
Node.list()
|> Enum.map(fn node ->
remote_worker_pid = :rpc.call(node, Process, :whereis, [:"db_worker_#{db_worker_index}"])
remote_state = :rpc.call(node, GenServer, :call, [remote_worker_pid, {:get_state}])
:rpc.call(node, Database.Worker, :get_topic_local, [remote_state, topic])
end)
|> Enum.to_list()
[get_topic_local(state, topic) | topics]
|> Enum.filter(& &1)
|> Enum.to_list()
end
@moduledoc """
Find a topic by name in current state.
If there is no match in state, try to retrive data from disc.
If there is no data on disc, return *nil*
"""
def get_topic_local(state, topic) do
result =
state
|> Enum.find(&String.equivalent?(topic, &1.topic))
cond do
# We found a topic in the state - Data has already been loaded from disc
result ->
result
# We did not find a topic in the state - Load data from disc
File.exists?("topic_" <> topic <> ".bin") ->
Utilities.Serialize.retrieve_object("topic_" <> topic <> ".bin")
# We did not find a topic in the state - No data on disc
true ->
nil
end
end
end