Skip to content

fix(syncing): make Syncer shutdown idempotent after start failure - #3418

Open
racequite wants to merge 1 commit into
evstack:mainfrom
racequite:fix-syncer-idempotent-shutdown
Open

fix(syncing): make Syncer shutdown idempotent after start failure#3418
racequite wants to merge 1 commit into
evstack:mainfrom
racequite:fix-syncer-idempotent-shutdown

Conversation

@racequite

@racequite racequite commented Aug 1, 2026

Copy link
Copy Markdown

Overview

Syncer.Start() automatically calls Stop() when initialization fails. The upper-level component cleanup may subsequently call Stop() again.

Previously, Stop() did not clear s.cancel or otherwise record that shutdown had completed. The second call therefore reached:

close(s.heightInCh)

and panicked with:

panic: close of closed channel

The explicit lifecycle state ensures that only the first Stop() performs shutdown and closes heightInCh. It also preserves the existing contract that a stopped Syncer cannot be restarted.

Summary by CodeRabbit

  • Bug Fixes
    • Improved synchronization lifecycle handling to prevent duplicate starts and ensure safe restarts after stopping.
    • Made shutdown operations safely repeatable, including after startup failures.
    • Improved cleanup of related processes and resources during shutdown.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Changes

Syncer lifecycle control

Layer / File(s) Summary
Lifecycle state and shutdown coordination
block/internal/syncing/syncer.go
Syncer now tracks New, Started, and Stopped states under a mutex. Start rejects invalid transitions. Stop performs one-time cancellation and cleanup.
Lifecycle regression coverage
block/internal/syncing/syncer_test.go, block/internal/syncing/syncer_benchmark_test.go
Tests cover failed-start shutdown, repeated Stop calls, retriever cleanup, channel closure, callback clearing, and explicit started-state setup.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Syncer
  participant ForcedInclusionRetriever
  participant DAFollower
  participant HeightChannel
  participant RaftCallback
  Syncer->>Syncer: Capture and clear cancel function
  Syncer->>Syncer: Mark lifecycle stopped
  Syncer->>ForcedInclusionRetriever: Stop once
  Syncer->>DAFollower: Stop once
  Syncer->>HeightChannel: Close
  Syncer->>RaftCallback: Clear callback
Loading

Suggested reviewers: julienrbrt, tac0turtle

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the Syncer shutdown fix and follows the repository's semantic commit format.
Description check ✅ Passed The description explains the failure scenario, root cause, fix, and preserved restart behavior required by the Overview section.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
block/internal/syncing/syncer_test.go (1)

1115-1156: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover the no-restart contract.

The test verifies repeated Stop calls and one-time cleanup. It does not verify that a stopped Syncer rejects a later Start.

As per PR objectives, preserve the behavior that a stopped Syncer cannot be restarted.

Suggested assertion
 	require.NoError(t, s.Stop(t.Context()))
 	require.NoError(t, s.Stop(t.Context()))
+	require.ErrorContains(t, s.Start(t.Context()), "syncer cannot be restarted after stopping")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@block/internal/syncing/syncer_test.go` around lines 1115 - 1156, Extend
TestSyncer_Stop_IsIdempotentAfterStartFailure to call Start again after the
syncer has reached syncerLifecycleStopped and assert that it returns the
expected error. Preserve the existing repeated-Stop and one-time cleanup
assertions, and verify the restart attempt does not alter the stopped state or
cleanup counts.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@block/internal/syncing/syncer.go`:
- Around line 178-190: Serialize Start and Stop through the syncer lifecycle
gate: add explicit starting and stopping states (or an equivalent completion
signal), keep initialization and worker registration under lifecycle protection,
and publish started only after initialization completes. Prevent Start once
stopping or stopped, make concurrent Stop callers wait for the first cleanup
instead of returning early, and protect fiRetriever and daFollower accesses with
lifecycleMu; add a concurrent lifecycle test covering shutdown races and run it
with -race.

---

Nitpick comments:
In `@block/internal/syncing/syncer_test.go`:
- Around line 1115-1156: Extend TestSyncer_Stop_IsIdempotentAfterStartFailure to
call Start again after the syncer has reached syncerLifecycleStopped and assert
that it returns the expected error. Preserve the existing repeated-Stop and
one-time cleanup assertions, and verify the restart attempt does not alter the
stopped state or cleanup counts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f9469652-1921-4da7-bfba-2e9ca33b0fc1

📥 Commits

Reviewing files that changed from the base of the PR and between f2e2f97 and dfb0889.

📒 Files selected for processing (3)
  • block/internal/syncing/syncer.go
  • block/internal/syncing/syncer_benchmark_test.go
  • block/internal/syncing/syncer_test.go

Comment on lines +178 to +190
s.lifecycleMu.Lock()
switch s.lifecycleState {
case syncerLifecycleStarted:
s.lifecycleMu.Unlock()
return errors.New("syncer already started")
case syncerLifecycleStopped:
s.lifecycleMu.Unlock()
return errors.New("syncer cannot be restarted after stopping")
}
ctx, cancel := context.WithCancel(ctx)
s.ctx, s.cancel = ctx, cancel
s.lifecycleState = syncerLifecycleStarted
s.lifecycleMu.Unlock()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🔴 Critical | 🏗️ Heavy lift

Serialize startup and shutdown completion.

At Line 189, Start publishes syncerLifecycleStarted before initialization completes. A concurrent Stop can cancel the context, observe an empty s.wg, and close s.heightInCh. Start can then start s.raftRetriever, create s.daFollower, or launch workers after shutdown. This can cause sends to a closed channel and leave workers outside the shutdown wait.

Stop also reads s.fiRetriever and s.daFollower while Start writes them without lifecycleMu. A second Stop returns at Line 264 before the first cleanup completes.

Add explicit starting and stopping states with a completion signal, or use an equivalent lifecycle gate. Do not allow startup after shutdown begins. Make concurrent Stop callers wait for the first cleanup. Add a concurrent lifecycle test and run it with -race.

As per coding guidelines, protect shared state and prevent goroutine leaks.

Also applies to: 261-271

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@block/internal/syncing/syncer.go` around lines 178 - 190, Serialize Start and
Stop through the syncer lifecycle gate: add explicit starting and stopping
states (or an equivalent completion signal), keep initialization and worker
registration under lifecycle protection, and publish started only after
initialization completes. Prevent Start once stopping or stopped, make
concurrent Stop callers wait for the first cleanup instead of returning early,
and protect fiRetriever and daFollower accesses with lifecycleMu; add a
concurrent lifecycle test covering shutdown races and run it with -race.

Source: Coding guidelines

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant