aboutsummaryrefslogblamecommitdiff
path: root/shard/lib/app/blockstore.ex
blob: 8e4fddcad141a93921401f8ff86e9b7f01ef0180 (plain) (tree)

















































































































                                                                                        
                             











                                                                         




                                                                 
















                                                   
defmodule SApp.BlockStore do
  @moduledoc """
  A module that implements a content-adressable storage (blocks, or pages,
  identified by the hash of their contents).

  This is not a shard, it is a side process that a shard may use to store its data.

  TODO: WIP
  """

  use GenServer

  @enforce_keys [:pid]
  defstruct [:pid, :prefer_ask]


  defmodule State do
    defstruct [:shard_id, :path, :store, :reqs, :retries]
  end


  def start_link(shard_id, path) do
    GenServer.start_link(__MODULE__, [shard_id, path])
  end

  def init([shard_id, path]) do
    Shard.Manager.dispatch_to(shard_id, path, self())

    {:ok, %State{shard_id: shard_id, path: path, store: %{}, reqs: %{}, retries: %{}}}
  end

  def handle_call({:get, key, prefer_ask}, from, state) do
    case state.store[key] do
      nil ->
        case prefer_ask do
          [_ | _] ->
            for peer <- prefer_ask do
              Shard.Manager.send(peer, {state.shard_id, state.path, {:get, key}})
            end
          _ ->
            ask_random_peers(state, key)
        end
        reqs_key = case state.reqs[key] do
          nil ->
            MapSet.put(MapSet.new(), from)
          ms ->
            MapSet.put(ms, from)
        end
        state = put_in(state.reqs[key], reqs_key)
        {:noreply, state}
      v ->
        {:reply, v, state}
    end
  end

  def handle_call({:put, val}, _from, state) do
    hash = SData.term_hash val
    state = %{state | store: Map.put(state.store, hash, val)}
    {:reply, hash, state}
  end

  def handle_cast({:msg, peer_id, _shard_id, _path, msg}, state) do
    state = case msg do
      {:get, key} ->
        case state.store[key] do
          nil ->
            Shard.Manager.send(peer_id, {state.shard_id, state.path, {:not_found, key}})
          v ->
            Shard.Manager.send(peer_id, {state.shard_id, state.path, {:info, key, v}})
        end
        state
      {:info, hash, value} ->
        if SData.term_hash value == hash do
          reqs = case state.reqs[hash] do
            nil -> state.reqs
            pids ->
              for pid <- pids do
                GenServer.reply(pid, value)
              end
              Map.delete(state.reqs, hash)
          end
          state = %{state | retries: Map.delete(state.retries, hash)}
          %{state | store: Map.put(state.store, hash, value), reqs: reqs}
        else
          state
        end
      {:not_found, key} ->
        if state.reqs[key] != nil and state.store[key] == nil do
          nretry = case state.retries[key] do
            nil -> 1
            n -> n+1
          end
          if nretry < 3 do
            ask_random_peers(state, key)
            %{state | retries: Map.put(state.retries, key, nretry)}
          else
            for pid <- state.reqs[key] do
              GenServer.reply(pid, nil)
            end
            state = %{state | reqs: Map.delete(state.reqs, key)}
            state = %{state | retries: Map.delete(state.retries, key)}
            state
          end
        else
          state
        end
    end
    {:noreply, state}
  end

  def ask_random_peers(state, key) do
    peers = :ets.lookup(:shard_peer_db, state.shard_id)
      |> Enum.shuffle
      |> Enum.take(3)
    for {_, peer} <- peers do
      Shard.Manager.send(peer, {state.shard_id, state.path, {:get, key}})
    end
  end


  defimpl SData.PageStore do
    def put(store, page) do
      hash = GenServer.call(store.pid, {:put, page})
      { hash, store }
    end

    def get(store, hash) do
      try do
        GenServer.call(store.pid, {:get, hash, store.prefer_ask})
      catch
        :exit, {:timeout, _} -> nil
      end
    end

    def copy(store, other_store, hash) do
      page = SData.PageStore.get(other_store, hash)
      refs = SData.Page.refs(page)
      for ref <- refs do
        copy(store, other_store, ref)
      end
      GenServer.call(store.pid, {:put, page})
      store
    end

    def free(store, _hash) do
      store ## DO SOMETHING???
    end
  end
end