Skip to content

Share service-opening steps between creation and recovery; make recovery_with_election assert outcomes - #8412

Merged
Eddy Ashton (eddyashton) merged 2 commits into
mainfrom
agents/open-recovered-service-test
Sep 22, 2026
Merged

Eddy Ashton (eddyashton) merged 2 commits into
mainfrom
agents/open-recovered-service-test

Conversation

@eddyashton

@eddyashton Eddy Ashton (eddyashton) commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Motivation

recovery_with_election (runs under LONG_TESTS) tried to SIGTERM the recovering primary while it was still reading the private ledger, by polling quickly enough after submitting the shares. Nothing guaranteed that: the read takes well under a second for the 2000 padding entries, and the test did not start polling until recover_with_shares() returned, which is 0.3-1.5s after the final share is accepted depending on client turnaround. The test is inherently racy and fails whenever the turnaround exceeds the read; it surfaced as a deterministic CI failure on #8405, which does not change the read start or rate.

The property the test protects - a node which has lost leadership must not open the service, and opening must happen at most once - is decided in one guarded block at the end of private recovery. That block had no direct test, and the repository has no NodeState unit harness.

Extracted from #8405 so it can be reviewed and merged independently.

Implementation summary

  • Service creation (transition_service_to_open) and the end of private recovery both issued recovery shares, marked the service open, endorsed the previous identity and requested a snapshot, each spelled out inline with its own copy of trigger_snapshot(). The shared steps are now ccf::open_service() (src/node/open_service.h), with ccf::trigger_snapshot() as the single implementation NodeState::trigger_snapshot() delegates to. ccf::open_recovered_service() adds the recovery-specific at-most-once guard (service must be WAITING_FOR_RECOVERY_SHARES) and clears the submitted shares before calling open_service(). Both NodeState call sites keep their existing failure semantics: recovery throws if the service cannot be opened, creation logs and continues as before.
  • New open_service_test covers both paths on a real kv::Store: the service becomes OPEN, submitted shares are cleared (recovery) and fresh ones issued, the previous identity is endorsed, a snapshot is requested; an unopenable status is reported rather than thrown; and open_recovered_service throws for OPEN/RECOVERING/OPENING.
  • recovery_with_election now asserts outcomes rather than timing: submit shares to the primary, SIGTERM it as soon as the final share is accepted, then check a new view is elected, every survivor reaches PartOfNetwork, the service is open, and no survivor hit the at-most-once guard. Whether the stop notice lands before or after the read finishes is a race the test no longer needs to win: if the primary wrote the opening transaction and stepped down before it replicated, the new leader rolls it back and opens the service itself (observed in local runs).

The 2000-entry padding is unchanged here; making it cheaper is a separate follow-up.

Safety and compatibility

Test-only change plus a behaviour-preserving extraction in NodeState; the steps and their order at both call sites are unchanged, as are their failure semantics. No API, data-format, configuration, or mixed-version impact. No CHANGELOG entry.

Validation: open_service_test, raft_test; e2e_logging (exercises the creation path); recovery_test with LONG_TESTS on a fresh main-based build; format, CMake, include-policy, Python format/lint, ASCII, copyright, test-bucket checks.

@eddyashton
Eddy Ashton (eddyashton) marked this pull request as ready for review September 22, 2026 11:56
@eddyashton
Eddy Ashton (eddyashton) requested a review from a team as a code owner September 22, 2026 11:56
Copilot AI lite review requested due to automatic review settings September 22, 2026 11:56

Copilot AI 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.

Copilot review overview

🟡 Changes recommended

Add coverage for the snapshot flag and correct the inaccurate test comment.

Get a fresh assessment by requesting another Copilot review.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

Refactors recovered-service opening into a unit-tested helper and makes election recovery testing outcome-based.

Changes:

  • Adds unit coverage for service opening and at-most-once guards.
  • Reuses the helper during private ledger recovery.
  • Removes timing-sensitive recovery polling.
  • Registers the new unit test target.
File Summary
tests/​recovery.py Makes election recovery assertions outcome-based.
src/​node/​test/​open_recovered_service.cpp Tests service opening behavior and state guards.
src/​node/​open_recovered_service.h Extracts guarded service-opening logic.
src/​node/​node_state.h Calls the extracted helper during recovery.
CMakeLists.txt Registers the new unit test.

💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread tests/recovery.py Outdated
Comment thread src/node/open_recovered_service.h Outdated
Comment thread tests/recovery.py
@eddyashton
Eddy Ashton (eddyashton) force-pushed the agents/open-recovered-service-test branch from 8385b17 to 55698e3 Compare September 22, 2026 13:05
@eddyashton Eddy Ashton (eddyashton) changed the title Unit-test opening a recovered service; make recovery_with_election assert outcomes Share service-opening steps between creation and recovery; make recovery_with_election assert outcomes Sep 22, 2026
Service creation (transition_service_to_open) and the end of private
recovery both issue recovery shares, mark the service open, endorse the
previous identity, and request a snapshot, but each spelled this out
inline, including its own copy of trigger_snapshot(). Extract the shared
steps into ccf::open_service(), with ccf::trigger_snapshot() as the single
implementation NodeState::trigger_snapshot() now delegates to, and the
recovery-specific guard and share clearing into ccf::open_recovered_service().

Both call sites keep their existing failure semantics: recovery throws if
the service cannot be opened, creation logs and continues as before.

These are testable without a NodeState harness, of which the repository
has none. open_service_test covers both paths on a real kv::Store: the
service becomes OPEN, shares are cleared (recovery) and reissued, the
previous identity is endorsed, and a snapshot is requested; that an
unopenable status is reported rather than thrown; and that
open_recovered_service throws for any status other than
WAITING_FOR_RECOVERY_SHARES, the at-most-once guard which prevents a second
node from opening a service another has already opened.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
The test tried to SIGTERM the primary inside the window where it is reading
the private ledger, by polling quickly enough. Nothing guaranteed that: the
window is under a second and shrinks relative to the client's turnaround
between accepting the final share and the first poll, so the test fails
whenever that turnaround exceeds the read.

Instead, submit the shares to the primary and SIGTERM it as soon as the final
share is accepted, then assert the outcome: a new view is elected, every
survivor reaches PartOfNetwork, the service is open, and no survivor died
attempting a second opening. Whether the stop notice lands before or after
the primary finishes reading is a race the test no longer needs to win: if
the primary wrote the opening transaction and then stepped down before it
replicated, the new leader rolls it back and opens the service itself. A
node which is not primary refusing to open, and opening being at-most-once,
are covered by open_recovered_service_test.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@eddyashton
Eddy Ashton (eddyashton) force-pushed the agents/open-recovered-service-test branch from 55698e3 to dab1418 Compare September 22, 2026 14:13
Eddy Ashton (eddyashton) added a commit that referenced this pull request Sep 22, 2026
Resolve the service-opening helper and test renames from #8412, removing the superseded copies.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@eddyashton
Eddy Ashton (eddyashton) merged commit f7b5fa3 into main Sep 22, 2026
12 checks passed
@eddyashton
Eddy Ashton (eddyashton) deleted the agents/open-recovered-service-test branch September 22, 2026 14:35
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.

3 participants