parent
b1e7258cf0
commit
cc67f502da
@ -0,0 +1,39 @@ |
||||
defmodule Indexer.Block.Catchup.Helper do |
||||
@moduledoc """ |
||||
Catchup helper functions |
||||
""" |
||||
|
||||
def sanitize_ranges(ranges) do |
||||
ranges |
||||
|> Enum.filter(&(not is_nil(&1))) |
||||
|> Enum.sort_by( |
||||
fn |
||||
from.._to -> from |
||||
el -> el |
||||
end, |
||||
:asc |
||||
) |
||||
|> Enum.chunk_while( |
||||
nil, |
||||
fn |
||||
_from.._to = chunk, nil -> |
||||
{:cont, chunk} |
||||
|
||||
_ch_from..ch_to = chunk, acc_from..acc_to = acc -> |
||||
if Range.disjoint?(chunk, acc), |
||||
do: {:cont, acc, chunk}, |
||||
else: {:cont, acc_from..max(ch_to, acc_to)} |
||||
|
||||
num, nil -> |
||||
{:halt, num} |
||||
|
||||
num, acc_from.._ = acc -> |
||||
if Range.disjoint?(num..num, acc), do: {:cont, acc, num}, else: {:halt, acc_from} |
||||
|
||||
_, num -> |
||||
{:halt, num} |
||||
end, |
||||
fn reminder -> {:cont, reminder, nil} end |
||||
) |
||||
end |
||||
end |
@ -0,0 +1,198 @@ |
||||
defmodule Indexer.Block.Catchup.MissingRangesCollector do |
||||
@moduledoc """ |
||||
Collects missing block ranges. |
||||
""" |
||||
|
||||
use GenServer |
||||
|
||||
alias Explorer.Chain |
||||
alias Explorer.Chain.Cache.BlockNumber |
||||
alias Indexer.Block.Catchup.Helper |
||||
|
||||
@default_missing_ranges_batch_size 100_000 |
||||
@future_check_interval Application.compile_env(:indexer, __MODULE__)[:future_check_interval] |
||||
@past_check_interval 10 |
||||
|
||||
@spec start_link(term()) :: GenServer.on_start() |
||||
def start_link(_) do |
||||
GenServer.start_link(__MODULE__, :ok, name: __MODULE__) |
||||
end |
||||
|
||||
@impl true |
||||
def init(_) do |
||||
{:ok, define_init()} |
||||
end |
||||
|
||||
defp define_init do |
||||
case Application.get_env(:indexer, :block_ranges) do |
||||
nil -> |
||||
default_init() |
||||
|
||||
string_ranges -> |
||||
case parse_block_ranges(string_ranges) do |
||||
:no_ranges -> default_init() |
||||
{:finite_ranges, ranges} -> ranges_init(ranges) |
||||
{:infinite_ranges, ranges, max_fetched_block_number} -> ranges_init(ranges, max_fetched_block_number) |
||||
end |
||||
end |
||||
end |
||||
|
||||
defp default_init do |
||||
max_number = BlockNumber.get_max() |
||||
{min_number, first_batch} = fetch_missing_ranges_batch(max_number, false) |
||||
initial_queue = push_batch_to_queue(first_batch, :queue.new()) |
||||
|
||||
Process.send_after(self(), :update_future, @future_check_interval) |
||||
Process.send_after(self(), :update_past, @past_check_interval) |
||||
|
||||
%{queue: initial_queue, min_fetched_block_number: min_number, max_fetched_block_number: max_number} |
||||
end |
||||
|
||||
defp ranges_init(ranges, max_fetched_block_number \\ nil) do |
||||
missing_ranges = |
||||
ranges |
||||
|> Enum.reverse() |
||||
|> Enum.flat_map(fn f..l -> Chain.missing_block_number_ranges(l..f) end) |
||||
|
||||
initial_queue = push_batch_to_queue(missing_ranges, :queue.new()) |
||||
|
||||
if not is_nil(max_fetched_block_number) do |
||||
Process.send_after(self(), :update_future, @future_check_interval) |
||||
end |
||||
|
||||
%{queue: initial_queue, max_fetched_block_number: max_fetched_block_number} |
||||
end |
||||
|
||||
def get_latest_batch do |
||||
GenServer.call(__MODULE__, :get_latest_batch) |
||||
end |
||||
|
||||
@impl true |
||||
def handle_call(:get_latest_batch, _from, %{queue: queue} = state) do |
||||
{latest_batch, new_queue} = |
||||
case :queue.out(queue) do |
||||
{{:value, batch}, rest} -> {batch, rest} |
||||
{:empty, rest} -> {[], rest} |
||||
end |
||||
|
||||
{:reply, latest_batch, %{state | queue: new_queue}} |
||||
end |
||||
|
||||
@impl true |
||||
def handle_info(:update_future, %{queue: queue, max_fetched_block_number: max_number} = state) do |
||||
if continue_future_updating?(max_number) do |
||||
{new_max_number, batch} = fetch_missing_ranges_batch(max_number, true) |
||||
Process.send_after(self(), :update_future, @future_check_interval) |
||||
{:noreply, %{state | queue: push_batch_to_queue(batch, queue, true), max_fetched_block_number: new_max_number}} |
||||
else |
||||
{:noreply, state} |
||||
end |
||||
end |
||||
|
||||
def handle_info(:update_past, %{queue: queue, min_fetched_block_number: min_number} = state) do |
||||
if min_number > first_block() do |
||||
{new_min_number, batch} = fetch_missing_ranges_batch(min_number, false) |
||||
Process.send_after(self(), :update_past, @past_check_interval) |
||||
{:noreply, %{state | queue: push_batch_to_queue(batch, queue), min_fetched_block_number: new_min_number}} |
||||
else |
||||
{:noreply, state} |
||||
end |
||||
end |
||||
|
||||
defp fetch_missing_ranges_batch(min_fetched_block_number, false = _to_future?) do |
||||
from = min_fetched_block_number - 1 |
||||
to = max(min_fetched_block_number - missing_ranges_batch_size(), first_block()) |
||||
|
||||
if from >= to do |
||||
{to, Chain.missing_block_number_ranges(from..to)} |
||||
else |
||||
{min_fetched_block_number, []} |
||||
end |
||||
end |
||||
|
||||
defp fetch_missing_ranges_batch(max_fetched_block_number, true) do |
||||
to = max_fetched_block_number + 1 |
||||
from = min(max_fetched_block_number + missing_ranges_batch_size(), BlockNumber.get_max()) |
||||
|
||||
if from >= to do |
||||
{from, Chain.missing_block_number_ranges(from..to)} |
||||
else |
||||
{max_fetched_block_number, []} |
||||
end |
||||
end |
||||
|
||||
defp push_batch_to_queue(batch, queue, r? \\ false) |
||||
defp push_batch_to_queue([], queue, _r?), do: queue |
||||
defp push_batch_to_queue(batch, queue, false), do: :queue.in(batch, queue) |
||||
defp push_batch_to_queue(batch, queue, true), do: :queue.in_r(batch, queue) |
||||
|
||||
defp first_block do |
||||
string_value = Application.get_env(:indexer, :first_block) |
||||
|
||||
case Integer.parse(string_value) do |
||||
{integer, ""} -> |
||||
integer |
||||
|
||||
_ -> |
||||
min_missing_block_number = |
||||
"min_missing_block_number" |
||||
|> Chain.get_last_fetched_counter() |
||||
|> Decimal.to_integer() |
||||
|
||||
min_missing_block_number |
||||
end |
||||
end |
||||
|
||||
defp continue_future_updating?(max_fetched_block_number) do |
||||
case Integer.parse(Application.get_env(:indexer, :last_block)) do |
||||
{last_block, ""} -> max_fetched_block_number < last_block |
||||
_ -> true |
||||
end |
||||
end |
||||
|
||||
defp missing_ranges_batch_size do |
||||
Application.get_env(:indexer, :missing_ranges_batch_size) || @default_missing_ranges_batch_size |
||||
end |
||||
|
||||
def parse_block_ranges(block_ranges_string) do |
||||
ranges = |
||||
block_ranges_string |
||||
|> String.split(",") |
||||
|> Enum.map(fn string_range -> |
||||
case String.split(string_range, "..") do |
||||
[from_string, "latest"] -> |
||||
parse_integer(from_string) |
||||
|
||||
[from_string, to_string] -> |
||||
with {from, ""} <- Integer.parse(from_string), |
||||
{to, ""} <- Integer.parse(to_string) do |
||||
if from <= to, do: from..to, else: nil |
||||
else |
||||
_ -> nil |
||||
end |
||||
|
||||
_ -> |
||||
nil |
||||
end |
||||
end) |
||||
|> Helper.sanitize_ranges() |
||||
|
||||
case List.last(ranges) do |
||||
_from.._to -> |
||||
{:finite_ranges, ranges} |
||||
|
||||
nil -> |
||||
:no_ranges |
||||
|
||||
num -> |
||||
{:infinite_ranges, List.delete_at(ranges, -1), num - 1} |
||||
end |
||||
end |
||||
|
||||
defp parse_integer(integer_string) do |
||||
case Integer.parse(integer_string) do |
||||
{integer, ""} -> integer |
||||
_ -> nil |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,83 @@ |
||||
defmodule Indexer.Block.Catchup.MissingRangesCollectorTest do |
||||
use Explorer.DataCase |
||||
|
||||
alias Indexer.Block.Catchup.MissingRangesCollector |
||||
|
||||
test "default_init" do |
||||
insert(:block, number: 1_000_000) |
||||
insert(:block, number: 500_123) |
||||
MissingRangesCollector.start_link([]) |
||||
Process.sleep(500) |
||||
|
||||
assert [999_999..900_000//-1] = MissingRangesCollector.get_latest_batch() |
||||
assert [899_999..800_000//-1] = MissingRangesCollector.get_latest_batch() |
||||
assert [799_999..700_000//-1] = MissingRangesCollector.get_latest_batch() |
||||
|
||||
insert(:block, number: 1_200_000) |
||||
Process.sleep(500) |
||||
|
||||
assert [1_199_999..1_100_001//-1] = MissingRangesCollector.get_latest_batch() |
||||
assert [1_100_000..1_000_001//-1] = MissingRangesCollector.get_latest_batch() |
||||
assert [699_999..600_000//-1] = MissingRangesCollector.get_latest_batch() |
||||
assert [599_999..500_124//-1, 500_122..500_000//-1] = MissingRangesCollector.get_latest_batch() |
||||
end |
||||
|
||||
describe "ranges_init" do |
||||
setup do |
||||
initial_env = Application.get_all_env(:indexer) |
||||
on_exit(fn -> Application.put_all_env([{:indexer, initial_env}]) end) |
||||
end |
||||
|
||||
test "infinite range" do |
||||
Application.put_env(:indexer, :block_ranges, "1..5,3..5,2qw1..12,10..11a,,asd..qwe,10..latest") |
||||
|
||||
insert(:block, number: 200_000) |
||||
|
||||
MissingRangesCollector.start_link([]) |
||||
Process.sleep(500) |
||||
|
||||
assert [199_999..100_010//-1] = MissingRangesCollector.get_latest_batch() |
||||
assert [100_009..10//-1] = MissingRangesCollector.get_latest_batch() |
||||
assert [5..1//-1] = MissingRangesCollector.get_latest_batch() |
||||
end |
||||
|
||||
test "finite range" do |
||||
Application.put_env(:indexer, :block_ranges, "10..20,5..15,18..25,35..40,30..50,150..200") |
||||
|
||||
insert(:block, number: 200_000) |
||||
|
||||
MissingRangesCollector.start_link([]) |
||||
Process.sleep(500) |
||||
|
||||
assert [200..150//-1, 50..30//-1, 25..5//-1] = MissingRangesCollector.get_latest_batch() |
||||
assert [] = MissingRangesCollector.get_latest_batch() |
||||
end |
||||
|
||||
test "finite range with existing blocks" do |
||||
Application.put_env(:indexer, :block_ranges, "10..20,5..15,18..25,35..40,30..50,150..200") |
||||
|
||||
insert(:block, number: 200_000) |
||||
insert(:block, number: 175) |
||||
insert(:block, number: 33) |
||||
|
||||
MissingRangesCollector.start_link([]) |
||||
Process.sleep(500) |
||||
|
||||
assert [200..176//-1, 174..150//-1, 50..34//-1, 32..30//-1, 25..5//-1] = MissingRangesCollector.get_latest_batch() |
||||
assert [] = MissingRangesCollector.get_latest_batch() |
||||
end |
||||
end |
||||
|
||||
test "parse_block_ranges/1" do |
||||
assert MissingRangesCollector.parse_block_ranges("1..5,3..5,2qw1..12,10..11a,,asd..qwe,10..latest") == |
||||
{:infinite_ranges, [1..5], 9} |
||||
|
||||
assert MissingRangesCollector.parse_block_ranges("latest..123,,fvdskvjglav!@#$%^&,2..1") == :no_ranges |
||||
|
||||
assert MissingRangesCollector.parse_block_ranges("10..20,5..15,18..25,35..40,30..50,100..latest,150..200") == |
||||
{:infinite_ranges, [5..25, 30..50], 99} |
||||
|
||||
assert MissingRangesCollector.parse_block_ranges("10..20,5..15,18..25,35..40,30..50,150..200") == |
||||
{:finite_ranges, [5..25, 30..50, 150..200]} |
||||
end |
||||
end |
Loading…
Reference in new issue