|
|
|
@ -10,8 +10,16 @@ defmodule Explorer.BufferedTask do |
|
|
|
|
|
|
|
|
|
@callback run(entries :: list, retries :: pos_integer) :: :ok | {:retry, reason :: term} | {:halt, reason :: term} |
|
|
|
|
|
|
|
|
|
def buffer(server, entries) when is_list(entries) do |
|
|
|
|
GenServer.call(server, {:buffer, entries}) |
|
|
|
|
@doc """ |
|
|
|
|
Buffers list of entries for future async execution. |
|
|
|
|
""" |
|
|
|
|
def buffer(server, entries, timeout \\ 5000) when is_list(entries) do |
|
|
|
|
GenServer.call(server, {:buffer, entries}, timeout) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@doc false |
|
|
|
|
def debug_count(server) do |
|
|
|
|
GenServer.call(server, :debug_count) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def start_link({module, base_opts}) do |
|
|
|
@ -25,11 +33,14 @@ defmodule Explorer.BufferedTask do |
|
|
|
|
send(self(), :initial_stream) |
|
|
|
|
|
|
|
|
|
state = %{ |
|
|
|
|
callback_module: callback_module, |
|
|
|
|
pid: self(), |
|
|
|
|
init_task: nil, |
|
|
|
|
flush_timer: nil, |
|
|
|
|
callback_module: callback_module, |
|
|
|
|
flush_interval: Keyword.fetch!(opts, :flush_interval), |
|
|
|
|
max_batch_size: Keyword.fetch!(opts, :max_batch_size), |
|
|
|
|
max_concurrency: Keyword.fetch!(opts, :max_concurrency), |
|
|
|
|
stream_chunk_size: Keyword.fetch!(opts, :stream_chunk_size), |
|
|
|
|
current_buffer: [], |
|
|
|
|
buffer: :queue.new(), |
|
|
|
|
tasks: %{} |
|
|
|
@ -46,10 +57,6 @@ defmodule Explorer.BufferedTask do |
|
|
|
|
{:noreply, flush(state)} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def handle_info({:async_perform, entries}, state) do |
|
|
|
|
{:noreply, spawn_next_batch(state, entries)} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def handle_info({ref, {:performed, :ok}}, state) do |
|
|
|
|
{:noreply, drop_task(state, ref)} |
|
|
|
|
end |
|
|
|
@ -62,6 +69,14 @@ defmodule Explorer.BufferedTask do |
|
|
|
|
{:noreply, drop_task(state, ref)} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def handle_info({ref, :ok}, %{init_task: ref} = state) do |
|
|
|
|
{:noreply, state} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def handle_info({:DOWN, ref, :process, _pid, :normal}, %{init_task: ref} = state) do |
|
|
|
|
{:noreply, %{state | init_task: :complete}} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def handle_info({:DOWN, _ref, :process, _pid, :normal}, state) do |
|
|
|
|
{:noreply, state} |
|
|
|
|
end |
|
|
|
@ -70,13 +85,23 @@ defmodule Explorer.BufferedTask do |
|
|
|
|
{:noreply, drop_task_and_retry(state, ref)} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def handle_call({:async_perform, stream_que}, _from, state) do |
|
|
|
|
new_buffer = :queue.join(state.buffer, stream_que) |
|
|
|
|
{:reply, :ok, spawn_next_batch(%{state | buffer: new_buffer})} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def handle_call({:buffer, entries}, _from, state) do |
|
|
|
|
{:reply, :ok, buffer_entries(state, entries)} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
def handle_call(:debug_count, _from, state) do |
|
|
|
|
count = length(state.current_buffer) + :queue.len(state.buffer) * state.max_batch_size |
|
|
|
|
|
|
|
|
|
{:reply, count, state} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp drop_task(state, ref) do |
|
|
|
|
schedule_async_perform([]) |
|
|
|
|
%{state | tasks: Map.delete(state.tasks, ref)} |
|
|
|
|
spawn_next_batch(%{state | tasks: Map.delete(state.tasks, ref)}) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp drop_task_and_retry(state, ref) do |
|
|
|
@ -90,44 +115,55 @@ defmodule Explorer.BufferedTask do |
|
|
|
|
defp buffer_entries(state, []), do: state |
|
|
|
|
|
|
|
|
|
defp buffer_entries(state, entries) do |
|
|
|
|
current_buffer = entries ++ state.current_buffer |
|
|
|
|
{batch, overflow} = Enum.split(current_buffer, state.max_batch_size) |
|
|
|
|
|
|
|
|
|
if length(batch) == state.max_batch_size do |
|
|
|
|
queue(%{state | current_buffer: overflow}, batch, 0) |
|
|
|
|
else |
|
|
|
|
%{state | current_buffer: current_buffer} |
|
|
|
|
end |
|
|
|
|
%{state | current_buffer: [entries | state.current_buffer]} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp queue(state, batch, retries) do |
|
|
|
|
%{state | buffer: :queue.in({batch, retries}, state.buffer)} |
|
|
|
|
defp queue(%{} = state, batch, retries) do |
|
|
|
|
%{state | buffer: queue(state.buffer, batch, retries)} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp do_initial_stream(state) do |
|
|
|
|
{0, []} |
|
|
|
|
|> state.callback_module.init(fn entry, {len, acc} -> |
|
|
|
|
batch = [entry | acc] |
|
|
|
|
defp queue({_, _} = que, batch, retries) do |
|
|
|
|
:queue.in({batch, retries}, que) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
if len + 1 >= state.max_batch_size do |
|
|
|
|
schedule_async_perform(Enum.reverse(batch)) |
|
|
|
|
defp do_initial_stream(%{stream_chunk_size: stream_chunk_size} = state) do |
|
|
|
|
task = |
|
|
|
|
Task.Supervisor.async(Explorer.TaskSupervisor, fn -> |
|
|
|
|
{0, []} |
|
|
|
|
else |
|
|
|
|
{len + 1, batch} |
|
|
|
|
end |
|
|
|
|
end) |
|
|
|
|
|> catchup_remaining() |
|
|
|
|
|> state.callback_module.init(fn |
|
|
|
|
entry, {len, acc} when len + 1 >= stream_chunk_size -> |
|
|
|
|
[entry | acc] |
|
|
|
|
|> chunk_into_queue(state) |
|
|
|
|
|> async_perform(state.pid) |
|
|
|
|
|
|
|
|
|
schedule_next_buffer_flush(state) |
|
|
|
|
{0, []} |
|
|
|
|
|
|
|
|
|
entry, {len, acc} -> |
|
|
|
|
{len + 1, [entry | acc]} |
|
|
|
|
end) |
|
|
|
|
|> catchup_remaining(state) |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
schedule_next_buffer_flush(%{state | init_task: task.ref}) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp catchup_remaining({:ok, {0, []}}), do: :ok |
|
|
|
|
defp catchup_remaining({:ok, {0, []}}, _state), do: :ok |
|
|
|
|
|
|
|
|
|
defp catchup_remaining({:ok, {_len, batch}}, state) do |
|
|
|
|
batch |
|
|
|
|
|> chunk_into_queue(state) |
|
|
|
|
|> async_perform(state.pid) |
|
|
|
|
|
|
|
|
|
defp catchup_remaining({:ok, {_len, batch}}) do |
|
|
|
|
schedule_async_perform(Enum.reverse(batch)) |
|
|
|
|
:ok |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp chunk_into_queue(entries, state) do |
|
|
|
|
entries |
|
|
|
|
|> Enum.reverse() |
|
|
|
|
|> Enum.chunk_every(state.max_batch_size) |
|
|
|
|
|> Enum.reduce(:queue.new(), fn batch, acc -> queue(acc, batch, 0) end) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp take_batch(state) do |
|
|
|
|
case :queue.out(state.buffer) do |
|
|
|
|
{{:value, batch}, new_queue} -> {batch, new_queue} |
|
|
|
@ -135,8 +171,8 @@ defmodule Explorer.BufferedTask do |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp schedule_async_perform(entries, after_ms \\ 0) do |
|
|
|
|
Process.send_after(self(), {:async_perform, entries}, after_ms) |
|
|
|
|
defp async_perform(entries, dest) do |
|
|
|
|
GenServer.call(dest, {:async_perform, entries}) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp schedule_next_buffer_flush(state) do |
|
|
|
@ -144,9 +180,7 @@ defmodule Explorer.BufferedTask do |
|
|
|
|
%{state | flush_timer: timer} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp spawn_next_batch(state, entries) do |
|
|
|
|
state = buffer_entries(state, entries) |
|
|
|
|
|
|
|
|
|
defp spawn_next_batch(state) do |
|
|
|
|
if Enum.count(state.tasks) < state.max_concurrency and :queue.len(state.buffer) > 0 do |
|
|
|
|
{{batch, retries}, new_queue} = take_batch(state) |
|
|
|
|
|
|
|
|
@ -162,14 +196,16 @@ defmodule Explorer.BufferedTask do |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp flush(%{current_buffer: []} = state) do |
|
|
|
|
state |> spawn_next_batch([]) |> schedule_next_buffer_flush() |
|
|
|
|
state |> spawn_next_batch() |> schedule_next_buffer_flush() |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp flush(%{current_buffer: current} = state) do |
|
|
|
|
{batch, overflow} = Enum.split(current, state.max_batch_size) |
|
|
|
|
|
|
|
|
|
%{state | current_buffer: overflow} |
|
|
|
|
|> queue(batch, 0) |
|
|
|
|
current |
|
|
|
|
|> List.flatten() |
|
|
|
|
|> Enum.chunk_every(state.max_batch_size) |
|
|
|
|
|> Enum.reduce(%{state | current_buffer: []}, fn batch, state_acc -> |
|
|
|
|
queue(state_acc, batch, 0) |
|
|
|
|
end) |
|
|
|
|
|> flush() |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|