defmodule SApp.Chat do @moduledoc """ Shard application for a replicated chat room with full history. Chat rooms are globally identified by their channel name. A chat room manifest is of the form: {:chat, channel_name} Future improvements: - message signing - storage of the chatroom messages to disk - storage of the known peers that have this channel to disk - use a DHT to find peers that are interested in this channel - epidemic broadcast (carefull not to be too costly, maybe by limiting the number of peers we talk to) - partial synchronization only == data distributed over peers """ use GenServer alias SData.MerkleList, as: ML @doc """ Start a process that connects to a given channel """ def start_link(channel) do GenServer.start_link(__MODULE__, channel) end @doc """ Initialize channel process. """ def init(channel) do store = ML.new(&msg_cmp/2) manifest = {:chat, channel} id = SData.term_hash manifest GenServer.cast(Shard.Manager, {:register, id, manifest, self()}) GenServer.cast(self(), :init_pull) {:ok, %{channel: channel, id: id, manifest: manifest, store: store, peers: MapSet.new}} end @doc """ Implementation of the :manifest call that returns the chat room's manifest """ def handle_call(:manifest, _from, state) do {:reply, state.manifest, state} end @doc """ Implementation of the :redundant handler: if another process is already synchronizing this channel then we exit. """ def handle_cast({:redundant, _}, _state) do exit :normal end @doc """ Implementation of the :init_pull handler, which is called when the process starts. It contacts all currently connected peers and asks them to send data for this channel if they have some. """ def handle_cast(:init_pull, state) do for {_, pid, _, _} <- :ets.tab2list(:peer_db) do GenServer.cast(pid, {:send_msg, {:interested, [state.id]}}) end {:noreply, state} end @doc """ Implementation of the :chat_send handler. This is the main handler that is used to send a message to the chat room. Puts the message in the store and syncs with all connected peers. """ def handle_cast({:chat_send, msg}, state) do msgitem = {(System.os_time :seconds), Shard.Identity.get_nickname(), msg} new_state = %{state | store: ML.insert(state.store, msgitem)} for peer <- state.peers do push_messages(new_state, peer, nil, 5) end {:noreply, new_state} end @doc """ Implementation of the :interested handler, this is called when a peer we are connected to asks to recieve data for this channel. """ def handle_cast({:interested, peer_id}, state) do push_messages(state, peer_id, nil, 10) new_peers = MapSet.put(state.peers, peer_id) {:noreply, %{ state | peers: new_peers }} end @doc """ Implementation of the :msg handler, which is the main handler for messages comming from other peers concerning this chat room. Messages are: - `{:get, start}`: get some messages starting at a given Merkle hash - `{:info, start, list, rest}`: put some messages and informs of the Merkle hash of the store of older messages. """ def handle_cast({:msg, peer_id, msg}, state) do case msg do {:get_manifest} -> Shard.Manager.send(peer_id, {state.id, {:manifest, state.manifest}}) {:get, start} -> push_messages(peer_id, state, start, 20) {:info, _start, list, rest} -> if rest != nil and not ML.has(state.store, rest) do Shard.Manager.send(peer_id, {state.id, {:get, rest}}) end who = self() spawn_link(fn -> Process.sleep 1000 GenServer.cast(who, {:deferred_insert, list}) end) _ -> nil end if MapSet.member?(state.peers, peer_id) do {:noreply, state} else handle_cast({:interested, peer_id}, state) end end def handle_cast({:deferred_insert, list}, state) do new_store = ML.insert_many(state.store, list, (fn msg -> msg_callback(state.channel, msg) end)) %{state | store: new_store} end defp push_messages(state, to, start, num) do case ML.read(state.store, start, num) do {:ok, list, rest} -> Shard.Manager.send(to, {state.id, {:info, start, list, rest}}) _ -> nil end end defp msg_callback(chan, {ts, nick, msg}) do IO.puts "#{ts |> DateTime.from_unix! |> DateTime.to_iso8601} ##{chan} <#{nick}> #{msg}" end defp msg_cmp({ts1, nick1, msg1}, {ts2, nick2, msg2}) do SData.MerkleList.cmp_ts_str({ts1, nick1<>"|"<>msg1}, {ts2, nick2<>"|"<>msg2}) end end