Skip to content

[2.0.x] fix(core): retry a failed auto-restart instead of stopping for good (#959) (backport of #1330) - #1340

Open
satran004 wants to merge 7 commits into
release/2.0.xfrom
backport/2.0.x/1330-failed-restart-recovery
Open

satran004 wants to merge 7 commits into
release/2.0.xfrom
backport/2.0.x/1330-failed-restart-recovery

Conversation

@satran004

Copy link
Copy Markdown
Member

Backport of #1330 to release/2.0.x. Tracked in #1339.

What it fixes: see #1330. The backport changes the same code in the same way.

How it was made: The 4 commits of #1330. The docs/app/docs/v3/... page is dropped because it isn't on this branch. Commits are cherry-picked with -x, and the changed lines were compared one by one with #1330.

Checks: 134 components/core tests pass; admin, admin-ui, the starter and applications:all compile.

Fault-injection run: the same scenarios as #1328 are running on release/2.0.x with all the backports. The results will be posted here and in #1339.

🤖 Generated with Claude Code

satran004 and others added 4 commits November 7, 2026 14:56
…y off (#959)

- When the auto-restart stops the sync and then cannot start it again (for example,
  the database is down), clear the "scheduled to stop" flag and set the error flag.
  The health check then retries. Before, the flag stayed set and auto-recovery
  treated it as a deliberate stop forever.
- StartService.start() leaves the service startable when it throws.
- A new sync session gets the full block-receive delay before the health check
  calls it stalled. A fresh session that never receives a block is now restarted.
- After max-attempts is reached, keep restarting once per
  store.auto-restart.retry-interval-after-max-ms (default 60 s) instead of pausing
  for the whole window. 0 restores the old behaviour.

Part of #1328.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit a2f6c5f)
… restart backoff

- The auto-restart now skips its start when the sync was stopped or started
  by someone else while it waited, and only a real failure marks the restart failed.
- Cap the backoff exponent. Attempts keep counting during a long outage and
  5000 * 2^n overflowed to a negative sleep after ~64 attempts.
- start/stop are synchronized and alreadyStarted is volatile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 5d8581f)
… honour stops after a failed restart

- A restart request dropped by the debounce, the attempt limit or a running
  restart, and a restart that failed, are checked again after the debounce
  window. The restart runs again if the sync is still stopped by an error.
  The health-check service is optional and off by default, so it cannot be
  the only thing that asks again.
- stop() always records the stop, even when no session is running, and the
  admin stop endpoints always call it. A stop after a failed restart now keeps
  the sync stopped.
- start() clears the error flag left by a failed restart, so a retry on a
  fresh database can write the genesis cursor.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit ab0a437)
Found by the fault-injection re-test (scenario C06: a fresh JVM whose node
connection is accepted but never answered).

- yaci 0.4.7 waits for the handshake with no time limit. The block check
  (BlockFinder), the genesis lookup and the tip lookup now run through
  NodeCalls, which gives up after 40 s when auto-restart is on (no limit
  when it is off, as before) and cleans up the stuck connection.
- BlockFinder no longer treats "no answer" as "block not on the chain".
  That made the startup step back to an older cursor and delete newer ones.
- The health check no longer throws NullPointerException before the sync
  client was started, and skips while the sync is starting.
- A first start that fails because the node, the database or the network
  is not reachable is retried by auto-restart instead of closing the app.
  Other failures (configuration errors) still stop the app.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 9722c0a)
@satran004 satran004 added fault-injection Found or verified by the auto-recovery fault-injection test 2.0.x Fix or backport for the release/2.0.x stable line labels Nov 7, 2026

Copy link
Copy Markdown

Initial automated review · Aster

Hi @satran004, thanks for the PR! 🙏 This is an automatic first check by an AI reviewer. It does not replace maintainer review and may be wrong.

Reviewed commit: cf79af4963f17f0037061803f0e282995e99945a

Issues found:

  • components/core/src/main/java/com/bloxbean/cardano/yaci/store/core/processor/RequiredRestartProcessor.java:159 — An admin stop during restart backoff can be undone because the retry takes a fresh stop token after the wait. Could we check the stop generation from before backoff?
  • components/core/src/main/java/com/bloxbean/cardano/yaci/store/core/processor/RequiredRestartProcessor.java:103 — If a new session gets IntersectionNotFound during the debounce window, the follow-up skips it because that event leaves the error flag clear, so sync can stay stalled with the optional health check off.
  • components/core/src/main/java/com/bloxbean/cardano/yaci/store/core/service/StartService.java:178 — If the cursor and tip checks succeed but the live sync socket never answers the handshake, startup holds the restart lock indefinitely. Could we also bound the live handshake?
  • components/core/src/main/java/com/bloxbean/cardano/yaci/store/core/service/CursorServiceImpl.java:61 — With auto-restart disabled, a node that completes the handshake but never answers the cursor check can now block startup indefinitely instead of timing out after 40 seconds.

Not tested: I reviewed the code and CI without running the build, tests, or live fault scenarios.

@satran004

Copy link
Copy Markdown
Member Author

Fault-injection result on release/2.0.x

The same scenarios as #1328 ran on plain release/2.0.x and on release/2.0.x with all 8 backports (#1340–#1347). With the backports:

  • all 104 scenarios recovered on their own, with no manual restart (plain 2.0.x: 6 stuck);
  • no duplicate rows (plain 2.0.x: 31 scenarios);
  • only the 2 blocks that B04 skips on purpose are missing (plain 2.0.x: 9 blocks).

Full results: #1339.

This PR in the run: C01 and C04 (a restart fails while Postgres is down or StartService fails) got stuck on plain 2.0.x in both the initial sync and at the tip, and recovered on their own with this fix (40 s and 68 s). D04 (DB network partition, initial sync) got stuck on plain 2.0.x and recovered in 3 s with this fix. C06 (the node accepts the connection but never answers) recovered in 34 s at the tip.

Copy link
Copy Markdown

reviewer1 — review of cf79af4963f17f0037061803f0e282995e99945a

Changes requested. The failed-start state reset, retry-after-cap behavior, and distinction between timeout and “intersection not found” are useful fixes. I independently traced the remaining cases below; they also substantiate the earlier Aster feedback.

  1. [P1] Bound the final live-session startup as well as the preliminary lookups. StartService.java:175–184 still calls startFetch/startSync synchronously, outside NodeCalls. If the cursor and tip connections succeed but the subsequent live connection accepts TCP and never answers the handshake, start() never reaches its finally: starting=true, the synchronized start monitor is held, and an automatic restart also retains restartLock. The health checker now explicitly skips this state. I checked the cached yaci-core 0.4.7 bytecode: Session.handshake() loops without a deadline. Apply a cancellable deadline to the live connection too, with cleanup that cannot deadlock on the startup monitor. Add a test where only this last connection stalls.

  2. [P1] Preserve a dropped intersection-failure restart even when isError=false. RequiredRestartProcessor.java:103 retries only isSyncFailed(). However, BlockFetchService.intersactNotFound() publishes a restart without setting the error flag. If the replacement session receives this response while the current restart holds its lock or within the debounce window, the event is dropped and the follow-up does nothing. With the optional admin health checker disabled, recovery can stop permanently. Track a pending restart against the session generation, or mark the current session failed before publishing. Test this with health recovery disabled.

  3. [P2] Capture deliberate-stop intent before the backoff. RequiredRestartProcessor.java:157–173 obtains a fresh stop token only after Thread.sleep(backoffMs). An admin/snapshot stop during that 5–60-second wait increments stopCount, but stopForRestart() then increments it again and treats that later value as its own valid token. Sync restarts despite the deliberate stop. The added test covers the later shutdown wait only. Capture/validate the generation before backoff and recheck atomically before stopping/restarting.

  4. [P2] Retain a finite cursor-check timeout when auto-restart is disabled. CursorServiceImpl.java:60–61 now passes zero in that configuration. Previously, after a successful handshake, the intersection response had a 40-second wait. A peer that completes the handshake but never answers now blocks startup indefinitely. Keep the timeout and fail visibly without treating a timeout as an invalid cursor; disabling retries should not remove the response deadline.

The reported C01/C04/D04/C06 results support the paths exercised, but do not cover these narrower interleavings. Review method: exact-head source/diff and test inspection, plus yaci bytecode inspection; I have not rerun the unit suite or VM scenarios.

satran004 and others added 3 commits November 8, 2026 19:26
…ackoff

Review follow-ups for #1330.

- The live connection opened last by StartService.start (startFetch or
  startSync) had no time limit. yaci 0.4.7 opens the connection and waits
  for the handshake inside that call (Session.handshake loops with no
  deadline, bloxbean/yaci#241). A node that answered the cursor and tip
  checks and then stalled held start(), the restart lock and the
  "starting" flag the health check skips. With auto-restart on, the start
  now gives up after 40 s like the other node calls: the connection is
  closed without taking the fetch service's lock (the stalled start holds
  it), the thread is released, and the start fails so the restart retries.
  With auto-restart off it still waits as before.
- IntersectionNotFound on the live session published a restart without
  setting the error flag. A request dropped because a restart was running
  or within the debounce window (always the case right after a restart)
  was then never retried, since the follow-up check only retries while the
  sync is in the error state. The session is now marked failed first.
- The auto-restart took its stop token only after the 5-60 s backoff, so
  an admin or snapshot stop during the backoff was overridden. The stop
  count is now read before the backoff; if it changed, the restart does
  nothing.
- With auto-restart off the cursor check waited forever for the node's
  answer (before the PR: 40 s). The answer is waited for at most 40 s
  again; a timeout fails the start and is still not taken as "block not
  on the chain".
- BlockFetchServiceRestartStateTest started a session with a mocked
  keep-alive interval of 0, which spins and records calls on the mock.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUCWhJpLCQR5Asv7nDb
(cherry picked from commit c2a7b43)
…meout/startSyncWithTimeout

#1313 adds startFetch(from, to, Long fromBlockNumber). A startFetch(from, to, long timeoutMs) overload next to it
would let a call silently pick the wrong one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUCWhJpLCQR5Asv7nDb
(cherry picked from commit 9d50730)
…e the restart read its count

A start can finish just after its time limit, when NodeCalls no longer runs the cancel, and the caller then
treats it as failed with its connection still open. Close the connection whenever the timed start fails.

A restart request can wait in the queue, or come from the closing connection, after a deliberate stop. Its
stop-count check then passes. Refuse the restart while the sync is stopped on purpose (scheduledToStop): a failed
restart clears that flag, so only a deliberate stop leaves it set.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MJrQUCWhJpLCQR5Asv7nDb
(cherry picked from commit 090dde9)
@sonarqubecloud

sonarqubecloud Bot commented Nov 8, 2026

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
C Reliability Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

Catch issues before they fail your Quality Gate with our IDE extension SonarQube for IDE

@satran004

Copy link
Copy Markdown
Member Author

Thanks, reviewer1. Handled as follows:

  1. Live session start: now time-limited, and the cancel doesn't take the BlockFetchService lock. A timed start that fails always closes its connection.
  2. Intersection not found: marks the session failed before asking for a restart, so a dropped request is retried.
  3. Stop during the backoff: the stop count is read before the backoff, and a restart is refused while the sync is stopped on purpose.
  4. Cursor check: the answer is waited for at most 40 s even with auto-restart off. A timeout fails the start and is never read as an invalid cursor.

Commits: 31027bd, 2f5a265, c388bff here, c2a7b43, 9d50730, 090dde9 on #1330.

The fixes were made in #1330 first and cherry-picked here. Each finding, its verdict and its test are in the reply on #1339. The fault suite is running again on the updated combined build.

@satran004

Copy link
Copy Markdown
Member Author

Verdict: APPROVED

reviewer1 — round 2. Reviewed c388bff8a7fc9d5464c944b06571173883a65c3b, compared with cf79af4963f17f0037061803f0e282995e99945a.

All four original findings are resolved for the startup/retry paths changed here.

Finding Status Reason
F1 [P1] Resolved Live starts have timeout wrappers; cancellation bypasses the held service monitor and failed timed starts close the connection.
F2 [P1] Resolved Intersection-not-found marks the session failed before requesting a restart.
F3 [P2] Resolved The stop token is captured before backoff; an intentional stop also blocks queued restart requests.
F4 [P2] Resolved Intersection responses retain a finite timeout with auto-restart disabled.

I agree to defer the hung yaci DNS lookup and the unbounded range→chain transition in batchDone, as explicitly recorded in the author reply. Please carry both into a follow-up issue linked there. These remain paths where automatic recovery can stall; this approval does not assert that all network waits are bounded.

The combined startFetchWithTimeout(from, to, fromBlockNumber, timeoutMs) / sync signatures preserve the processed-cursor argument and timeout separately. I accept that resolution at 121ee4599fcdc41226239b9195ce45daa47cfc9d.

Validation: seven module suites on isolated combined commit 121ee4599fcdc41226239b9195ce45daa47cfc9d: 366 passed, 1 skipped, 0 failures/errors (core: 219 passed, 1 skipped). This is combined-build validation, not a separate run on every backport.

Exact command
JAVA_HOME=/Users/satya/.sdkman/candidates/java/21.0.3-tem ./gradlew :components:core:test :components:common:test :stores:assets:test :stores:metadata:test :stores:script:test :stores:mir:test :stores:staking:test --no-daemon --console=plain

No implementation changes made by this review. The updated VM fault-suite results are still pending. This verdict covers the reviewed head only.

@satran004

Copy link
Copy Markdown
Member Author

Fault-injection rerun on 2.0.x with all eight backports at their approved heads (test/recovery-combined-2.0.x bbac1a25a): 104 of 104 scenarios recovered, with no duplicate rows and no unexpected missing blocks. The full report is on #1339: #1339 (comment)

For this PR:

The unpatched baseline is release/2.0.x (b2x), run on the same matrix.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

2.0.x Fix or backport for the release/2.0.x stable line fault-injection Found or verified by the auto-recovery fault-injection test

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants