From fe293c4bf3cc6d08df06a057c27b3df485d1161f Mon Sep 17 00:00:00 2001 From: Ivaylo Badinov <632282+venimus@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:41:00 +0300 Subject: [PATCH] fix: don't crash exec_all_mem_mutations on an empty batch List.last(blocks_mutations) returns nil when the batch has zero blocks (e.g. a resume/extension check that turns out to have nothing new past the already-synced height), and matching {{height, _mbi}, _block, _} against nil raises MatchError, crashing the sync task. Confirmed live on mainnet mdw1 (157.180.104.172): crashed 48 times in a tight burst, hit @max_restarts, and AeMdw.Sync.Server went idle (no sync progress, mdw_gens_per_minute: 0.0) until manually hot-patched. Live patch verified sync resumed immediately after (512+ gens/min, no new crashes). Fix: treat an empty batch as a no-op (return state unchanged) instead of crashing. --- lib/ae_mdw/sync/server.ex | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/lib/ae_mdw/sync/server.ex b/lib/ae_mdw/sync/server.ex index 748d6cbf4..3cd9d5570 100644 --- a/lib/ae_mdw/sync/server.ex +++ b/lib/ae_mdw/sync/server.ex @@ -491,22 +491,31 @@ defmodule AeMdw.Sync.Server do |> List.flatten() |> maybe_add_accounts_balance_mutations() - {{height, _mbi}, _block, _mutations} = List.last(blocks_mutations) - Log.info("[sync_mem] exec until height=#{height}") + # An empty batch (no blocks to apply - e.g. a resume/extension check that + # turns out to have nothing new past the already-synced height) is a + # legitimate no-op, not an error - List.last/1 would return nil and crash + # the match below if this weren't guarded. + case List.last(blocks_mutations) do + nil -> + state + + {{height, _mbi}, _block, _mutations} -> + Log.info("[sync_mem] exec until height=#{height}") + + {ts, new_state} = + :timer.tc(fn -> + all_mutations = + Enum.map(blocks_mutations, fn {_height, _block, mutations} -> mutations end) - {ts, new_state} = - :timer.tc(fn -> - all_mutations = - Enum.map(blocks_mutations, fn {_height, _block, mutations} -> mutations end) + State.commit_mem(state, all_mutations) + end) - State.commit_mem(state, all_mutations) - end) + :ok = profile_sync("sync_mem", height, ts, blocks_mutations, gens_in_batch) - :ok = profile_sync("sync_mem", height, ts, blocks_mutations, gens_in_batch) + broadcast_blocks(gens_mutations) - broadcast_blocks(gens_mutations) - - new_state + new_state + end end defp spawn_task(fun) do