From 95eef6e0c083d0b7406e00379125346bf6c0364c Mon Sep 17 00:00:00 2001 From: romanornr <6548898+romanornr@users.noreply.github.com> Date: Wed, 21 Oct 2026 15:58:39 +0200 Subject: [PATCH] fix(indexer): keep one flush timer per BufferedTask, recheck missing hot-contract days daily BufferedTask: every idle re-check started new flush timer and overwrote stored reference without stopping old timer, so timers piled up with uptime (idle explorer at ~1.1 cores after 42 days, 0.04 after restart). Cancel stored timer before starting new one. Same cancel-then-reschedule order as Broadway RateLimiter. Hot contracts: day without indexed blocks no longer retried every 10 s with error log. Info log, recheck next UTC day within existing 30-day window, daily sequence continues. Other errors keep 10 s retry. Regression tests: existing flush test starts with live timer and checks cancellation; new hot-contracts test checks info log, no error. Upstream-ready branches on blockscout/blockscout 6275a9c: fix/buffered-task-single-flush-timer (fa32904a9, e8aef41ab), fix/hot-contracts-empty-day-retry (890008a67, 9f923a724). --- apps/indexer/lib/indexer/buffered_task.ex | 6 ++++++ .../indexer/fetcher/stats/hot_smart_contracts.ex | 11 +++++++++++ apps/indexer/test/indexer/buffered_task_test.exs | 8 +++++--- .../fetcher/stats/hot_smart_contracts_test.exs | 13 +++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) diff --git a/apps/indexer/lib/indexer/buffered_task.ex b/apps/indexer/lib/indexer/buffered_task.ex index f9193a445d0..732992d9097 100644 --- a/apps/indexer/lib/indexer/buffered_task.ex +++ b/apps/indexer/lib/indexer/buffered_task.ex @@ -1285,6 +1285,12 @@ defmodule Indexer.BufferedTask do if state.flush_interval == :infinity do state else + # Keep one flush timer per worker. Overwriting a live reference leaves + # the previous timer running and starts another repeating flush chain. + if is_reference(state.flush_timer) do + Process.cancel_timer(state.flush_timer) + end + timer = Process.send_after(self(), :flush, state.flush_interval) %__MODULE__{state | flush_timer: timer} end diff --git a/apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex b/apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex index 7a416e2db7c..8d57ebe1d8a 100644 --- a/apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex +++ b/apps/indexer/lib/indexer/fetcher/stats/hot_smart_contracts.ex @@ -44,6 +44,17 @@ defmodule Indexer.Fetcher.Stats.HotSmartContracts do Logger.info("Hot contracts fetched for #{date}") else + {:error, {:error, :not_found}} -> + if Date.diff(Date.utc_today(), date) < @max_days_ago do + schedule_message_on_the_next_day({:fetch_for_date, date, false}) + end + + if new_day? do + schedule_next_day_fetch() + end + + Logger.info("No indexed blocks for hot contracts on #{date}") + {:error, error} -> Process.send_after(self(), {:fetch_for_date, date, new_day?}, @retry_interval) Logger.error("Error fetching hot contracts for #{date}: #{inspect(error)}") diff --git a/apps/indexer/test/indexer/buffered_task_test.exs b/apps/indexer/test/indexer/buffered_task_test.exs index 0124f04afdf..6f96085dcff 100644 --- a/apps/indexer/test/indexer/buffered_task_test.exs +++ b/apps/indexer/test/indexer/buffered_task_test.exs @@ -239,6 +239,7 @@ defmodule Indexer.BufferedTaskTest do test "with 0 size without maximum size schedules next flush and hibernates" do bound_queue = %BoundQueue{} + old_timer = Process.send_after(self(), :flush, :timer.minutes(5)) refute BoundQueue.shrunk?(bound_queue) @@ -249,15 +250,16 @@ defmodule Indexer.BufferedTaskTest do callback_module: ShrinkableTask, callback_module_state: nil, bound_queue: bound_queue, - flush_interval: 50, - flush_timer: nil, + flush_interval: :timer.minutes(5), + flush_timer: old_timer, task_supervisor: BufferedTaskSup, max_batch_size: 1, max_concurrency: 1, poll: false }) - refute flush_timer == nil + assert Process.read_timer(old_timer) == false + assert is_integer(Process.read_timer(flush_timer)) end test "with 0 size with maximum size calls init/2 to get work that was shed before scheduling next flush and hibernates" do diff --git a/apps/indexer/test/indexer/fetcher/stats/hot_smart_contracts_test.exs b/apps/indexer/test/indexer/fetcher/stats/hot_smart_contracts_test.exs index 2afecaa7337..0e1be2c4d6e 100644 --- a/apps/indexer/test/indexer/fetcher/stats/hot_smart_contracts_test.exs +++ b/apps/indexer/test/indexer/fetcher/stats/hot_smart_contracts_test.exs @@ -356,6 +356,19 @@ defmodule Indexer.Fetcher.Stats.HotSmartContractsTest do end end + test "logs an unavailable date without an error" do + insert(:block, number: 0, timestamp: DateTime.add(DateTime.utc_now(), -40, :day)) + date = Date.add(Date.utc_today(), -1) + + log = + capture_log([level: :info], fn -> + assert {:noreply, :ok} = HotSmartContracts.handle_info({:fetch_for_date, date, false}, :ok) + end) + + assert log =~ "No indexed blocks for hot contracts on #{date}" + refute log =~ "Error fetching hot contracts" + end + describe "handle_continue/2" do test "processes chain age check on startup" do # Create blocks older than 30 days