defmodule Buffer do
use GenServer
def start_link(capacity), do: GenServer.start_link(__MODULE__, capacity)
def push(buffer, item, timeout \\ :infinity), do: GenServer.call(buffer, {:push, item}, timeout)
def pop(buffer), do: GenServer.call(buffer, :pop)
def info(buffer), do: GenServer.call(buffer, :info)
@impl true
def init(capacity), do: {:ok, %{capacity: capacity, items: :queue.new(), waiting: :queue.new()}}
@impl true
def handle_call({:push, item}, from, state) do
if :queue.len(state.items) < state.capacity do
{:reply, :ok, %{state | items: :queue.in(item, state.items)}}
else
# Full: park the caller. No reply means it stays
# blocked.
{:noreply, %{state | waiting: :queue.in({from, item}, state.waiting)}}
end
end
def handle_call(:pop, _from, state) do
case :queue.out(state.items) do
{:empty, _} ->
{:reply, :empty, state}
{{:value, item}, items} ->
{:reply, {:ok, item}, admit_waiting(%{state | items: items})}
end
end
def handle_call(:info, _from, state) do
blocked = for {_from, item} <- :queue.to_list(state.waiting), do: item
{:reply, %{items: :queue.to_list(state.items), blocked: blocked}, state}
end
# A slot is free: move the oldest waiting item in and
# release its producer.
defp admit_waiting(state) do
case :queue.out(state.waiting) do
{:empty, _} ->
state
{{:value, {from, item}}, waiting} ->
GenServer.reply(from, :ok)
%{state | items: :queue.in(item, state.items), waiting: waiting}
end
end
end