281 lines
7.5 KiB
Elixir
281 lines
7.5 KiB
Elixir
defmodule Excache.Blocking do
|
|
@moduledoc false
|
|
|
|
use GenServer
|
|
|
|
alias Excache.Store
|
|
alias Excache.Command.Reply
|
|
|
|
@name __MODULE__
|
|
|
|
def start_link(_arg), do: GenServer.start_link(__MODULE__, nil, name: @name)
|
|
|
|
def acquire(request), do: GenServer.call(@name, {:acquire, request, :block})
|
|
|
|
def attempt(request) do
|
|
{:served, term} = GenServer.call(@name, {:acquire, request, :immediate})
|
|
term
|
|
end
|
|
|
|
def signal([]), do: []
|
|
def signal(keys), do: GenServer.call(@name, {:ready, keys})
|
|
|
|
def takeWrites, do: GenServer.call(@name, :takeWrites)
|
|
|
|
def waitingCount(key), do: GenServer.call(@name, {:waitingCount, key})
|
|
|
|
@impl true
|
|
def init(_arg), do: {:ok, %{queues: %{}, waiters: %{}, monitors: %{}, writes: []}}
|
|
|
|
@impl true
|
|
def handle_call({:acquire, request, mode}, {pid, _tag}, state) do
|
|
waiter = %{
|
|
ref: make_ref(),
|
|
pid: pid,
|
|
keys: request.keys,
|
|
op: request.op,
|
|
monitor: nil,
|
|
timer: nil
|
|
}
|
|
|
|
state = enqueue(state, waiter)
|
|
{state, served} = drain(state, request.keys, [])
|
|
|
|
case {deliver(served, waiter.ref), mode} do
|
|
{nil, :immediate} -> {:reply, {:served, missReply(request.op)}, dropWaiter(state, waiter)}
|
|
{nil, :block} -> {:reply, {:blocked, waiter.ref}, arm(state, waiter, request.timeout)}
|
|
{term, _mode} -> {:reply, {:served, term}, state}
|
|
end
|
|
end
|
|
|
|
def handle_call({:ready, keys}, _from, state) do
|
|
{state, served} = drain(state, keys, [])
|
|
deliver(served, nil)
|
|
takeWrites(state)
|
|
end
|
|
|
|
def handle_call(:takeWrites, _from, state), do: takeWrites(state)
|
|
|
|
def handle_call({:waitingCount, key}, _from, state) do
|
|
count =
|
|
case Map.fetch(state.queues, key) do
|
|
{:ok, queue} -> :queue.len(queue)
|
|
:error -> 0
|
|
end
|
|
|
|
{:reply, count, state}
|
|
end
|
|
|
|
@impl true
|
|
def handle_info({:blockingTimeout, ref}, state) do
|
|
case Map.fetch(state.waiters, ref) do
|
|
{:ok, waiter} ->
|
|
send(waiter.pid, {:excacheUnblocked, ref, {:array, nil}})
|
|
{:noreply, dropWaiter(state, waiter)}
|
|
|
|
:error ->
|
|
{:noreply, state}
|
|
end
|
|
end
|
|
|
|
def handle_info({:DOWN, monitor, :process, _pid, _reason}, state) do
|
|
ref = Map.fetch!(state.monitors, monitor)
|
|
{:noreply, dropWaiter(state, Map.fetch!(state.waiters, ref))}
|
|
end
|
|
|
|
defp drain(state, [], served), do: {state, served}
|
|
|
|
defp drain(state, [key | rest], served) do
|
|
{state, served, chained} = drainKey(state, key, served, [])
|
|
drain(state, chained ++ rest, served)
|
|
end
|
|
|
|
defp drainKey(state, key, served, chained) do
|
|
case headWaiter(state, key) do
|
|
nil ->
|
|
{state, served, chained}
|
|
|
|
waiter ->
|
|
case serveWaiter(waiter) do
|
|
:empty ->
|
|
{state, served, chained}
|
|
|
|
{:ok, term, ready, writes} ->
|
|
state = recordWrites(dropWaiter(state, waiter), writes)
|
|
drainKey(state, key, [{waiter, term} | served], ready ++ chained)
|
|
|
|
{:error, term} ->
|
|
drainKey(dropWaiter(state, waiter), key, [{waiter, term} | served], chained)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp recordWrites(state, writes), do: %{state | writes: state.writes ++ writes}
|
|
|
|
defp takeWrites(state), do: {:reply, state.writes, %{state | writes: []}}
|
|
|
|
defp serveWaiter(waiter) do
|
|
Enum.reduce_while(waiter.keys, :empty, fn key, _pending ->
|
|
case serve(waiter.op, key) do
|
|
:empty -> {:cont, :empty}
|
|
outcome -> {:halt, outcome}
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp serve({:pop, side}, key) do
|
|
case popOne(key, side) do
|
|
{:ok, value} ->
|
|
{:ok, {:array, [{:bulk, key}, {:bulk, value}]}, [], [{popName(side), [key]}]}
|
|
|
|
nil ->
|
|
:empty
|
|
|
|
{:error, reason} ->
|
|
{:error, errorTerm(reason)}
|
|
end
|
|
end
|
|
|
|
defp serve({:mpop, side, count}, key) do
|
|
case popMany(key, side, count) do
|
|
{:ok, [_ | _] = values} ->
|
|
taken = Integer.to_string(length(values))
|
|
|
|
{:ok, {:array, [{:bulk, key}, {:array, Reply.bulks(values)}]}, [],
|
|
[{popName(side), [key, taken]}]}
|
|
|
|
{:ok, []} ->
|
|
:empty
|
|
|
|
nil ->
|
|
:empty
|
|
|
|
{:error, reason} ->
|
|
{:error, errorTerm(reason)}
|
|
end
|
|
end
|
|
|
|
defp serve({:move, fromSide, toSide, destination}, key) do
|
|
case popOne(key, fromSide) do
|
|
{:ok, value} ->
|
|
moveInto(key, destination, fromSide, toSide, value)
|
|
|
|
nil ->
|
|
:empty
|
|
|
|
{:error, reason} ->
|
|
{:error, errorTerm(reason)}
|
|
end
|
|
end
|
|
|
|
defp moveInto(source, destination, fromSide, toSide, value) do
|
|
case pushOne(destination, toSide, value) do
|
|
{:ok, _length} ->
|
|
{:ok, {:bulk, value}, [destination],
|
|
moveWrites(source, destination, fromSide, toSide, value)}
|
|
|
|
{:error, reason} ->
|
|
pushOne(source, fromSide, value)
|
|
{:error, errorTerm(reason)}
|
|
end
|
|
end
|
|
|
|
defp popOne(key, :left), do: Store.lpop(key, nil)
|
|
defp popOne(key, :right), do: Store.rpop(key, nil)
|
|
|
|
defp popMany(key, :left, count), do: Store.lpop(key, count)
|
|
defp popMany(key, :right, count), do: Store.rpop(key, count)
|
|
|
|
defp pushOne(key, :left, value), do: Store.lpush(key, [value])
|
|
defp pushOne(key, :right, value), do: Store.rpush(key, [value])
|
|
|
|
defp popName(:left), do: "LPOP"
|
|
defp popName(:right), do: "RPOP"
|
|
|
|
defp pushName(:left), do: "LPUSH"
|
|
defp pushName(:right), do: "RPUSH"
|
|
|
|
defp moveWrites(source, destination, :right, :left, _value),
|
|
do: [{"RPOPLPUSH", [source, destination]}]
|
|
|
|
defp moveWrites(source, destination, fromSide, toSide, value),
|
|
do: [{popName(fromSide), [source]}, {pushName(toSide), [destination, value]}]
|
|
|
|
defp missReply({:move, _fromSide, _toSide, _destination}), do: {:bulk, nil}
|
|
defp missReply(_op), do: {:array, nil}
|
|
|
|
defp errorTerm(:wrongtype), do: Reply.wrongtype()
|
|
|
|
defp deliver(served, inlineRef) do
|
|
Enum.reduce(served, nil, fn {waiter, term}, inline ->
|
|
if waiter.ref == inlineRef do
|
|
term
|
|
else
|
|
send(waiter.pid, {:excacheUnblocked, waiter.ref, term})
|
|
inline
|
|
end
|
|
end)
|
|
end
|
|
|
|
defp headWaiter(state, key) do
|
|
with {:ok, queue} <- Map.fetch(state.queues, key),
|
|
{:value, ref} <- :queue.peek(queue) do
|
|
Map.fetch!(state.waiters, ref)
|
|
else
|
|
_missing -> nil
|
|
end
|
|
end
|
|
|
|
defp enqueue(state, waiter) do
|
|
queues =
|
|
Enum.reduce(Enum.uniq(waiter.keys), state.queues, fn key, acc ->
|
|
Map.put(acc, key, :queue.in(waiter.ref, Map.get(acc, key, :queue.new())))
|
|
end)
|
|
|
|
%{state | queues: queues, waiters: Map.put(state.waiters, waiter.ref, waiter)}
|
|
end
|
|
|
|
defp dropWaiter(state, waiter) do
|
|
armed = Map.fetch!(state.waiters, waiter.ref)
|
|
if armed.monitor, do: Process.demonitor(armed.monitor, [:flush])
|
|
if armed.timer, do: Process.cancel_timer(armed.timer)
|
|
|
|
queues =
|
|
Enum.reduce(Enum.uniq(armed.keys), state.queues, fn key, acc ->
|
|
dropFromQueue(acc, key, armed.ref)
|
|
end)
|
|
|
|
%{
|
|
state
|
|
| queues: queues,
|
|
waiters: Map.delete(state.waiters, armed.ref),
|
|
monitors: Map.delete(state.monitors, armed.monitor)
|
|
}
|
|
end
|
|
|
|
defp dropFromQueue(queues, key, ref) do
|
|
pruned = :queue.filter(&(&1 != ref), Map.fetch!(queues, key))
|
|
|
|
if :queue.is_empty(pruned) do
|
|
Map.delete(queues, key)
|
|
else
|
|
Map.put(queues, key, pruned)
|
|
end
|
|
end
|
|
|
|
defp arm(state, waiter, timeout) do
|
|
monitor = Process.monitor(waiter.pid)
|
|
|
|
timer =
|
|
if timeout > 0, do: Process.send_after(self(), {:blockingTimeout, waiter.ref}, timeout)
|
|
|
|
armed = %{waiter | monitor: monitor, timer: timer}
|
|
|
|
%{
|
|
state
|
|
| waiters: Map.put(state.waiters, waiter.ref, armed),
|
|
monitors: Map.put(state.monitors, monitor, waiter.ref)
|
|
}
|
|
end
|
|
end
|