|
|
|
@ -6,25 +6,27 @@ defmodule Indexer.Sequence do |
|
|
|
|
@enforce_keys ~w(current queue step)a |
|
|
|
|
defstruct current: nil, |
|
|
|
|
queue: nil, |
|
|
|
|
step: nil, |
|
|
|
|
mode: :infinite |
|
|
|
|
step: nil |
|
|
|
|
|
|
|
|
|
@typedoc """ |
|
|
|
|
The initial ranges to stream from the `t:Stream.t/` returned from `build_stream/1` |
|
|
|
|
The ranges to stream from the `t:Stream.t/` returned from `build_stream/1` |
|
|
|
|
""" |
|
|
|
|
@type prefix :: [Range.t()] |
|
|
|
|
@type ranges :: [Range.t()] |
|
|
|
|
|
|
|
|
|
@typep prefix_option :: {:prefix, prefix} |
|
|
|
|
@typep ranges_option :: {:ranges, ranges} |
|
|
|
|
|
|
|
|
|
@typedoc """ |
|
|
|
|
The first number in the sequence to start at once the `t:prefix/0` ranges and any `t:Range.t/0`s injected with |
|
|
|
|
`inject_range/2` are all consumed. |
|
|
|
|
The first number in the sequence to start for infinite sequences. |
|
|
|
|
""" |
|
|
|
|
@type first :: pos_integer() |
|
|
|
|
@type first :: integer() |
|
|
|
|
|
|
|
|
|
@typep first_named_argument :: {:first, pos_integer()} |
|
|
|
|
@typep first_option :: {:first, first} |
|
|
|
|
|
|
|
|
|
@type mode :: :infinite | :finite |
|
|
|
|
@typedoc """ |
|
|
|
|
* `:finite` - only popping ranges from `queue`. |
|
|
|
|
* `:infinite` - generating new ranges from `current` and `step` when `queue` is empty. |
|
|
|
|
""" |
|
|
|
|
@type mode :: :finite | :infinite |
|
|
|
|
|
|
|
|
|
@typedoc """ |
|
|
|
|
The size of `t:Range.t/0` to construct based on the `t:first_named_argument/0` or its current value when all |
|
|
|
@ -34,17 +36,25 @@ defmodule Indexer.Sequence do |
|
|
|
|
|
|
|
|
|
@typep step_named_argument :: {:step, step} |
|
|
|
|
|
|
|
|
|
@type options :: [prefix_option | first_named_argument | step_named_argument] |
|
|
|
|
@type options :: [ranges_option | first_option | step_named_argument] |
|
|
|
|
|
|
|
|
|
@typep t :: %__MODULE__{ |
|
|
|
|
current: pos_integer(), |
|
|
|
|
queue: :queue.queue(Range.t()), |
|
|
|
|
step: step(), |
|
|
|
|
mode: mode() |
|
|
|
|
current: nil | integer(), |
|
|
|
|
step: step() |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
@doc """ |
|
|
|
|
Starts a process for managing a block sequence. |
|
|
|
|
|
|
|
|
|
Infinite sequence |
|
|
|
|
|
|
|
|
|
Indexer.Sequence.start_link(first: 100, step: 10) |
|
|
|
|
|
|
|
|
|
Finite sequence |
|
|
|
|
|
|
|
|
|
Indexer.Sequence.start_link(ranges: [100..0]) |
|
|
|
|
|
|
|
|
|
""" |
|
|
|
|
@spec start_link(options) :: GenServer.on_start() |
|
|
|
|
def start_link(options) when is_list(options) do |
|
|
|
@ -69,9 +79,7 @@ defmodule Indexer.Sequence do |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@doc """ |
|
|
|
|
Changes the mode for the sequencer to signal continuous streaming mode. |
|
|
|
|
|
|
|
|
|
Returns the previous `t:mode/0`. |
|
|
|
|
Changes the mode for the sequence to finite. |
|
|
|
|
""" |
|
|
|
|
@spec cap(pid()) :: mode |
|
|
|
|
def cap(sequence) when is_pid(sequence) do |
|
|
|
@ -79,11 +87,11 @@ defmodule Indexer.Sequence do |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@doc """ |
|
|
|
|
Adds a range of block numbers to the sequence. |
|
|
|
|
Adds a range of block numbers to the end of sequence. |
|
|
|
|
""" |
|
|
|
|
@spec inject_range(pid(), Range.t()) :: :ok |
|
|
|
|
def inject_range(sequence, _first.._last = range) when is_pid(sequence) do |
|
|
|
|
GenServer.call(sequence, {:inject_range, range}) |
|
|
|
|
@spec queue(pid(), Range.t()) :: :ok |
|
|
|
|
def queue(sequence, _first.._last = range) when is_pid(sequence) do |
|
|
|
|
GenServer.call(sequence, {:queue, range}) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@doc """ |
|
|
|
@ -96,59 +104,164 @@ defmodule Indexer.Sequence do |
|
|
|
|
|
|
|
|
|
@impl GenServer |
|
|
|
|
@spec init(options) :: {:ok, t} |
|
|
|
|
def init(named_arguments) when is_list(named_arguments) do |
|
|
|
|
def init(options) when is_list(options) do |
|
|
|
|
Process.flag(:trap_exit, true) |
|
|
|
|
|
|
|
|
|
{:ok, |
|
|
|
|
%__MODULE__{ |
|
|
|
|
queue: |
|
|
|
|
named_arguments |
|
|
|
|
|> Keyword.get(:prefix, []) |
|
|
|
|
|> :queue.from_list(), |
|
|
|
|
current: Keyword.fetch!(named_arguments, :first), |
|
|
|
|
step: Keyword.fetch!(named_arguments, :step) |
|
|
|
|
}} |
|
|
|
|
with {:ok, %{ranges: ranges, first: first, step: step}} <- validate_options(options), |
|
|
|
|
initial_queue = :queue.new(), |
|
|
|
|
{:ok, queue} <- queue_chunked_ranges(initial_queue, step, ranges) do |
|
|
|
|
{:ok, %__MODULE__{queue: queue, current: first, step: step}} |
|
|
|
|
else |
|
|
|
|
{:error, reason} -> |
|
|
|
|
{:stop, reason} |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@impl GenServer |
|
|
|
|
|
|
|
|
|
@spec handle_call(:cap, GenServer.from(), t()) :: {:reply, mode(), %__MODULE__{mode: :infinite}} |
|
|
|
|
def handle_call(:cap, _from, %__MODULE__{mode: mode} = state) do |
|
|
|
|
{:reply, mode, %__MODULE__{state | mode: :finite}} |
|
|
|
|
@spec handle_call(:cap, GenServer.from(), %__MODULE__{current: nil}) :: {:reply, :finite, %__MODULE__{current: nil}} |
|
|
|
|
@spec handle_call(:cap, GenServer.from(), %__MODULE__{current: integer()}) :: |
|
|
|
|
{:reply, :infinite, %__MODULE__{current: nil}} |
|
|
|
|
def handle_call(:cap, _from, %__MODULE__{current: current} = state) do |
|
|
|
|
mode = |
|
|
|
|
case current do |
|
|
|
|
nil -> :finite |
|
|
|
|
_ -> :infinite |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
{:reply, mode, %__MODULE__{state | current: nil}} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@spec handle_call({:inject_range, Range.t()}, GenServer.from(), t()) :: {:reply, mode(), t()} |
|
|
|
|
def handle_call({:inject_range, _first.._last = range}, _from, %__MODULE__{queue: queue} = state) do |
|
|
|
|
{:reply, :ok, %__MODULE__{state | queue: :queue.in(range, queue)}} |
|
|
|
|
@spec handle_call({:queue, Range.t()}, GenServer.from(), t()) :: {:reply, :ok | {:error, String.t()}, t()} |
|
|
|
|
def handle_call({:queue, _first.._last = range}, _from, %__MODULE__{queue: queue, step: step} = state) do |
|
|
|
|
case queue_chunked_range(queue, step, range) do |
|
|
|
|
{:ok, updated_queue} -> |
|
|
|
|
{:reply, :ok, %__MODULE__{state | queue: updated_queue}} |
|
|
|
|
|
|
|
|
|
{:error, _} = error -> |
|
|
|
|
{:reply, error, state} |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@spec handle_call(:pop, GenServer.from(), t()) :: {:reply, Range.t() | :halt, t()} |
|
|
|
|
def handle_call(:pop, _from, %__MODULE__{mode: mode, queue: queue, current: current, step: step} = state) do |
|
|
|
|
def handle_call(:pop, _from, %__MODULE__{queue: queue, current: current, step: step} = state) do |
|
|
|
|
{reply, new_state} = |
|
|
|
|
case {mode, :queue.out(queue)} do |
|
|
|
|
case {current, :queue.out(queue)} do |
|
|
|
|
{_, {{:value, range}, new_queue}} -> |
|
|
|
|
{range, %__MODULE__{state | queue: new_queue}} |
|
|
|
|
|
|
|
|
|
{:infinite, {:empty, new_queue}} -> |
|
|
|
|
case current + step do |
|
|
|
|
negative when negative < 0 -> |
|
|
|
|
{current..0, %__MODULE__{state | current: 0, mode: :finite, queue: new_queue}} |
|
|
|
|
{nil, {:empty, new_queue}} -> |
|
|
|
|
{:halt, %__MODULE__{state | queue: new_queue}} |
|
|
|
|
|
|
|
|
|
{_, {:empty, new_queue}} -> |
|
|
|
|
case current + step do |
|
|
|
|
new_current -> |
|
|
|
|
last = new_current - sign(step) |
|
|
|
|
last = new_current - 1 |
|
|
|
|
{current..last, %__MODULE__{state | current: new_current, queue: new_queue}} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
{:finite, {:empty, new_queue}} -> |
|
|
|
|
{:halt, %__MODULE__{state | queue: new_queue}} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
{:reply, reply, new_state} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@spec sign(neg_integer()) :: -1 |
|
|
|
|
defp sign(integer) when integer < 0, do: -1 |
|
|
|
|
@spec queue_chunked_range(:queue.queue(Range.t()), step, Range.t()) :: |
|
|
|
|
{:ok, :queue.queue(Range.t())} | {:error, reason :: String.t()} |
|
|
|
|
defp queue_chunked_range(queue, step, _.._ = range) when is_integer(step) do |
|
|
|
|
with {:error, [reason]} <- queue_chunked_ranges(queue, step, [range]) do |
|
|
|
|
{:error, reason} |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
@spec queue_chunked_range(:queue.queue(Range.t()), step, [Range.t()]) :: |
|
|
|
|
{:ok, :queue.queue(Range.t())} | {:error, reasons :: [String.t()]} |
|
|
|
|
defp queue_chunked_ranges(queue, step, ranges) when is_integer(step) and is_list(ranges) do |
|
|
|
|
reduce_chunked_ranges(ranges, step, queue, &:queue.in/2) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp reduce_chunked_ranges(ranges, step, initial, reducer) |
|
|
|
|
when is_list(ranges) and is_integer(step) and step != 0 and is_function(reducer, 2) do |
|
|
|
|
Enum.reduce(ranges, {:ok, initial}, fn |
|
|
|
|
range, {:ok, acc} -> |
|
|
|
|
case reduce_chunked_range(range, step, acc, reducer) do |
|
|
|
|
{:ok, _} = ok -> |
|
|
|
|
ok |
|
|
|
|
|
|
|
|
|
{:error, reason} -> |
|
|
|
|
{:error, [reason]} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
range, {:error, acc_reasons} = acc -> |
|
|
|
|
case reduce_chunked_range(range, step, initial, reducer) do |
|
|
|
|
{:ok, _} -> acc |
|
|
|
|
{:error, reason} -> {:error, [reason | acc_reasons]} |
|
|
|
|
end |
|
|
|
|
end) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp reduce_chunked_range(_.._ = range, step, initial, reducer) do |
|
|
|
|
count = Enum.count(range) |
|
|
|
|
reduce_chunked_range(range, count, step, initial, reducer) |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp reduce_chunked_range(first..last = range, _count, step, _initial, _reducer) |
|
|
|
|
when (step < 0 and first < last) or (0 < step and last < first) do |
|
|
|
|
{:error, "Range (#{inspect(range)}) direction is opposite step (#{step}) direction"} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp reduce_chunked_range(_.._ = range, count, step, initial, reducer) when count <= abs(step) do |
|
|
|
|
{:ok, reducer.(range, initial)} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp reduce_chunked_range(first..last = range, _, step, initial, reducer) do |
|
|
|
|
{sign, comparator} = |
|
|
|
|
if step > 0 do |
|
|
|
|
{1, &Kernel.>=/2} |
|
|
|
|
else |
|
|
|
|
{-1, &Kernel.<=/2} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
final = |
|
|
|
|
first |
|
|
|
|
|> Stream.iterate(&(&1 + step)) |
|
|
|
|
|> Enum.reduce_while(initial, fn chunk_first, acc -> |
|
|
|
|
next_chunk_first = chunk_first + step |
|
|
|
|
full_chunk_last = next_chunk_first - sign |
|
|
|
|
|
|
|
|
|
@spec sign(non_neg_integer()) :: 1 |
|
|
|
|
defp sign(_), do: 1 |
|
|
|
|
{action, chunk_last} = |
|
|
|
|
if comparator.(full_chunk_last, last) do |
|
|
|
|
{:halt, last} |
|
|
|
|
else |
|
|
|
|
{:cont, full_chunk_last} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
{action, reducer.(chunk_first..chunk_last, acc)} |
|
|
|
|
end) |
|
|
|
|
|
|
|
|
|
{:ok, final} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
defp validate_options(options) do |
|
|
|
|
step = Keyword.fetch!(options, :step) |
|
|
|
|
|
|
|
|
|
case {Keyword.fetch(options, :ranges), Keyword.fetch(options, :first)} do |
|
|
|
|
{:error, {:ok, first}} -> |
|
|
|
|
case step do |
|
|
|
|
pos_integer when is_integer(pos_integer) and pos_integer > 0 -> |
|
|
|
|
{:ok, %{ranges: [], first: first, step: step}} |
|
|
|
|
|
|
|
|
|
_ -> |
|
|
|
|
{:error, ":step must be a positive integer for infinite sequences"} |
|
|
|
|
end |
|
|
|
|
|
|
|
|
|
{{:ok, ranges}, :error} -> |
|
|
|
|
{:ok, %{ranges: ranges, first: nil, step: step}} |
|
|
|
|
|
|
|
|
|
{{:ok, _}, {:ok, _}} -> |
|
|
|
|
{:error, |
|
|
|
|
":ranges and :first cannot be set at the same time as :ranges is for :finite mode while :first is for :infinite mode"} |
|
|
|
|
|
|
|
|
|
{:error, :error} -> |
|
|
|
|
{:error, "either :ranges or :first must be set"} |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|
end |
|
|
|
|