Repository navigation
Share service-opening steps between creation and recovery; make recovery_with_election assert outcomes - #8412
Merged
Eddy Ashton (eddyashton) merged 2 commits intoSep 22, 2026
Conversation
Contributor
There was a problem hiding this comment.
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
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.
Eddy Ashton (eddyashton)
force-pushed
the
agents/open-recovered-service-test
branch
from
September 22, 2026 12:06
836068a to
8385b17
Compare
Eddy Ashton (eddyashton)
added this pull request to stack #8413
September 22, 2026 12:13
Max (maxtropets)
approved these changes
Sep 22, 2026
Eddy Ashton (eddyashton)
force-pushed
the
agents/open-recovered-service-test
branch
from
September 22, 2026 13:05
8385b17 to
55698e3
Compare
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>
Eddy Ashton (eddyashton)
force-pushed
the
agents/open-recovered-service-test
branch
from
September 22, 2026 14:13
55698e3 to
dab1418
Compare
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>
Eddy Ashton (eddyashton)
deleted the
agents/open-recovered-service-test
branch
September 22, 2026 14:35
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Motivation
recovery_with_election(runs underLONG_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 untilrecover_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
NodeStateunit harness.Extracted from #8405 so it can be reviewed and merged independently.
Implementation summary
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 oftrigger_snapshot(). The shared steps are nowccf::open_service()(src/node/open_service.h), withccf::trigger_snapshot()as the single implementationNodeState::trigger_snapshot()delegates to.ccf::open_recovered_service()adds the recovery-specific at-most-once guard (service must beWAITING_FOR_RECOVERY_SHARES) and clears the submitted shares before callingopen_service(). BothNodeStatecall sites keep their existing failure semantics: recovery throws if the service cannot be opened, creation logs and continues as before.open_service_testcovers both paths on a realkv::Store: the service becomesOPEN, 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; andopen_recovered_servicethrows forOPEN/RECOVERING/OPENING.recovery_with_electionnow 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 reachesPartOfNetwork, 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_testwithLONG_TESTSon a freshmain-based build; format, CMake, include-policy, Python format/lint, ASCII, copyright, test-bucket checks.