Skip to content

Commit 064319d

Browse files
authored
fix(empty_blocks_sanitizer): handle nil block result, flag for refetch (#20)
1 parent 7948a37 commit 064319d

2 files changed

Lines changed: 90 additions & 9 deletions

File tree

apps/indexer/lib/indexer/fetcher/empty_blocks_sanitizer.ex

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizer do
9999
{non_empty_blocks, empty_blocks} = classify_blocks_from_result(result)
100100
process_non_empty_blocks(non_empty_blocks)
101101
process_empty_blocks(empty_blocks)
102+
process_missing_blocks(unprocessed_empty_blocks_list, non_empty_blocks, empty_blocks)
102103

103104
Logger.info("Batch of empty blocks is sanitized",
104105
fetcher: :empty_blocks_to_refetch
@@ -114,18 +115,51 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizer do
114115
end
115116

116117
defp classify_blocks_from_result(result) do
117-
result
118-
|> Enum.reduce({[], []}, fn %{id: _id, result: block}, {non_empty_blocks, empty_blocks} ->
119-
transactions = Map.get(block, "transactions") || []
120-
121-
if Enum.empty?(transactions) do
122-
{non_empty_blocks, [block_fields(block, transactions) | empty_blocks]}
123-
else
124-
{[block_fields(block, transactions) | non_empty_blocks], empty_blocks}
125-
end
118+
# A spec-compliant JSON-RPC server returns `result: null` for blocks it
119+
# cannot find (e.g. pruned or reorged). Skip those without crashing — the
120+
# caller reconciles which requested blocks are missing and flags them for
121+
# refetch.
122+
Enum.reduce(result, {[], []}, fn
123+
%{id: _id, result: nil}, acc ->
124+
acc
125+
126+
%{id: _id, result: block}, {non_empty_blocks, empty_blocks} ->
127+
transactions = Map.get(block, "transactions") || []
128+
129+
if Enum.empty?(transactions) do
130+
{non_empty_blocks, [block_fields(block, transactions) | empty_blocks]}
131+
else
132+
{[block_fields(block, transactions) | non_empty_blocks], empty_blocks}
133+
end
126134
end)
127135
end
128136

137+
# Blocks the RPC returned nil for stay in `is_empty: nil, refetch_needed: false`,
138+
# so without intervention the sanitizer's query would re-select them every cycle.
139+
# Flag them `refetch_needed: true` to remove them from the query set and let the
140+
# regular refetch path handle them.
141+
defp process_missing_blocks(requested, non_empty_blocks, empty_blocks) do
142+
returned = MapSet.new(non_empty_blocks ++ empty_blocks, & &1.number)
143+
144+
missing =
145+
requested
146+
|> Enum.map(& &1.number)
147+
|> Enum.reject(&MapSet.member?(returned, &1))
148+
149+
case missing do
150+
[] ->
151+
:ok
152+
153+
numbers ->
154+
Logger.warning(
155+
"JSON-RPC returned nil for block numbers #{inspect(numbers)}; marking as refetch_needed",
156+
fetcher: :empty_blocks_to_refetch
157+
)
158+
159+
Block.set_refetch_needed(numbers)
160+
end
161+
end
162+
129163
defp block_fields(block, transactions) do
130164
%{
131165
number: quantity_to_integer(block["number"]),

apps/indexer/test/indexer/fetcher/empty_blocks_sanitizer_test.exs

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,53 @@ defmodule Indexer.Fetcher.EmptyBlocksSanitizerTest do
140140
assert processed_block.refetch_needed == true, "invalid `refetch_needed` value set for processed block"
141141
end
142142

143+
test "marks block as refetch_needed when JSON-RPC returns nil result",
144+
%{json_rpc_named_arguments: json_rpc_named_arguments} do
145+
# Setup
146+
block_to_process = insert(:block, is_empty: nil)
147+
populate_database_with_dummy_blocks()
148+
assert Repo.get!(Block, block_to_process.hash).is_empty == nil, "precondition to check setup correctness"
149+
assert Repo.get!(Block, block_to_process.hash).refetch_needed == false, "precondition to check setup correctness"
150+
151+
encoded_expected_block_number = "0x" <> Integer.to_string(block_to_process.number, 16)
152+
153+
if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do
154+
EthereumJSONRPC.Mox
155+
|> stub(
156+
:json_rpc,
157+
fn [
158+
%{
159+
id: id,
160+
method: "eth_getBlockByNumber",
161+
params: [^encoded_expected_block_number, false]
162+
}
163+
],
164+
_options ->
165+
{:ok, [%{id: id, result: nil}]}
166+
end
167+
)
168+
end
169+
170+
EmptyBlocksSanitizer.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
171+
172+
# Wait for the sanitizer to flag the nil-result block as refetch_needed.
173+
# On the un-fixed code this never happens (the GenServer crashes on
174+
# BadMapError), so `wait_for_results` would time out and fail the test.
175+
processed_block =
176+
wait_for_results(fn ->
177+
Repo.one!(
178+
from(block in Block,
179+
where: block.hash == ^block_to_process.hash and block.refetch_needed == true
180+
)
181+
)
182+
end)
183+
184+
assert processed_block.is_empty == nil, "is_empty should remain untouched for unresolved blocks"
185+
186+
assert processed_block.refetch_needed == true,
187+
"refetch_needed should be set so the block exits the sanitizer's query set"
188+
end
189+
143190
test "only old enough blocks are sanitized", %{json_rpc_named_arguments: json_rpc_named_arguments} do
144191
# Setup
145192
block_to_process = insert(:block, is_empty: nil)

0 commit comments

Comments
 (0)