Repository navigation
feat(ledger): gate future Raft protocol capabilities #2327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
127 commits
Select commit
Hold shift + click to select a range
cb43793
feat(ledger): gate future Raft protocol capabilities
653a4bd
feat(ledger): enforce capability barriers end to end
739c5bf
fix(ledger): declare capabilities for barriers
e3a13cb
chore(ledger): apply CI lint formatting
9e84e39
fix(ledger): refresh capabilities on member restart
5d3491f
feat: arm capabilities after membership refresh
4458608
refactor: keep capability barrier feature-neutral
ceaa62c
test: remove obsolete capability activation test
a432368
chore: drop unreleased capability field reservation
6515ab9
test: proxy membership refresh through gateway
6afc22a
feat: gate new members on active capabilities
a902528
refactor: remove unused replay protocol floor
90aeeb6
fix: close capability admission races
500ba97
refactor: remove capability activation arming
6b66f76
docs: define semantic capability behavior
0b66f7d
docs: add protocol capability rollout workflow
773bd5b
fix(consensus): preserve capability evidence during bootstrap
99aba29
feat(consensus): auto-activate converged capabilities
542c892
test(consensus): guard capability rollout contracts
c840bac
fix(consensus): close capability rollout review gaps
1792482
refactor(consensus): make node learner join capability-aware
85bda99
refactor(consensus): pass capability metadata without decode
796ad6b
chore(ledger): apply Go formatter output
07fcb7d
fix(consensus): centralize prepared proposal construction
aebd7bb
fix(consensus): make prepared proposal metadata opaque
d95d0d1
fix(consensus): inject membership policy provider
b97d5a7
fix(consensus): preserve capability activations
406e531
fix(consensus): preserve capabilities across snapshots
0e7676b
fix(consensus): propagate capabilities on learner admission
6c001a6
fix(consensus): retry refresh against older peers
2880013
fix(consensus): allow membership before initial policy
c131aad
fix(consensus): reconcile capability membership changes
4fd3564
fix(consensus): validate membership against apply policy
40882da
docs(consensus): clarify proposal capability scope
58c5203
fix(consensus): gate proposals at Raft boundary
c3739ca
fix(consensus): republish capability gate after recovery
8e8ed1c
fix(consensus): bind proposal metadata to serialization
68190ed
test(consensus): cover capability policy rejection paths
316486c
test(consensus): cover inactive capability rejection
8194393
fix(consensus): gate proposals before index reservation
2208792
docs(consensus): document proposal gate boundary
aa82969
perf(consensus): serialize proposals outside tracker lock
079ef1a
docs(upgrades): declare add-learner CLI break
e400de6
docs(preload): document predicted index patching
fd7ec7e
fix(capabilities): converge policy activation from membership evidence
e38b475
fix: close capability activation races
9803b30
fix: keep membership refresh alive after startup
7699030
test(consensus): remove dead proposal gate and cover refresh retry
7e61a6c
fix(capabilities): gate policy activation on member evidence
5884f25
fix: require capability dependencies in constructors
485b513
cleanup: remove dead capability builder hook
70e3005
test: use deterministic refresh cancellation wait
e592f1c
fix: remove dead membership refresh error state
f44541e
chore: keep generated formatting clean
ad883dc
fix: preserve refresh cancellation error
5d7105a
chore: keep generated policy formatting clean
aad3980
fix: enforce membership refresh startup errors
41cc2bd
chore: record generated membership refresh formatting
68bce13
Revert "refactor(cli): remove manual learner registration command"
f4f929f
test: update membership fixtures for policy provider
7992edb
Reapply "refactor(cli): remove manual learner registration command"
2085c51
refactor(membership): unify learner admission API
3f585ae
fix(membership): bound startup refresh attempt
289eff1
docs(membership): clarify refresh retry handoff
510f78b
test(capabilities): document proposal gate cost evidence
f24b104
fix(bootstrap): preserve policy revision admission guards
ba88ec9
fix(bootstrap): allow capability-only policy revisions
f5810d3
refactor(membership): unify learner join API
c286908
refactor(bootstrap): share membership grpc dialer
4a83dbe
refactor(bootstrap): extract periodic reconciler
4d649bc
refactor(bootstrap): name policy reconciler provider
3890bd4
refactor(capabilities): centralize policy projections
0f6e4be
refactor(membership): unify registration entrypoint
db7cddd
refactor(capabilities): remove redundant proposal stamper
ebfe298
fix: reconcile proposal API after rebase
ad420e5
fix: preserve lightweight proposal test setup
e45cdb0
fix: register available learner command
8b5bae7
fix: respect current learner CLI surface
7fdb750
fix(ledger): submit validated proposals once
1e7870c
fix(ledger): drop legacy refresh compatibility
e0697fb
Revert "fix(ledger): drop legacy refresh compatibility"
376e642
docs(ledger): clarify rc capability rollout boundary
21b6cbc
fix(ledger): remove obsolete proposal policy wrapper
d80396d
test(ledger): adapt proposal composition tests
783438a
test(ledger): update recovery fixture dependencies
10e3ab6
test(ledger): provide membership policy fixture
1b8b355
test(ledger): update store and mirror fixtures
b02dd91
test: use existing learner registration method
4f7548f
chore: reconcile raft audit manifest
838b899
test: reconcile post-rebase constructor coverage
629be60
chore: fix imports after rebase
550331b
test: reconcile post-rebase CI expectations
0c8fbed
chore(proto): regenerate with CI toolchain
955d888
chore: apply CI lint formatting
2af0cac
chore(proto): match CI comment formatting
d94fb29
chore(proto): align audit comment formatting
cfaa493
simplify capability activation for coordinated upgrades
c3a273b
chore(proto): regenerate after main rebase
ea10e69
fix: reconcile capability policy cleanup
1543e09
test: update admission replay machine setup
1439968
fix(ledger): validate native policy activations
2f8f6ce
docs(ledger): restore raft audit evidence scenarios
d6abdfd
fix(ledger): refresh membership from recovered peers
e72cc5b
perf(ledger): validate proposals before Raft queue
b1ed44a
style(ledger): keep membership refresh formatting clean
7ccdbda
docs(ledger): remove retired learner promotion command
9a0c0e9
docs(ledger): mark rc4 stopped upgrade as mandatory
bb4c833
fix(ledger): serialize capability activation with membership
72c33d4
fix(ledger): reject unsupported active capabilities on recovery
0d2d9d8
fix(ledger): retain activation fence after canceled wait
c852e72
fix(ledger): reserve predicted index suffix before tracker lock
0a9b48a
style(ledger): apply generated Go formatting
d7a1af9
fix(ledger): treat startup refresh RPC deadlines as transient
2f2e546
docs(ledger): clarify startup refresh deadline handling
c27bde1
fix(ledger): retry remote membership refresh deadlines
252d551
chore(proto): regenerate common descriptor with pinned protoc
cb1af52
refactor(ledger): inspect dedicated policy proposals for activation
266722e
refactor(ledger): isolate capability policy admission fence
190b172
refactor(ledger): inject capability membership fence separately
7280837
refactor(ledger): consolidate membership evidence writes
0e1aa37
test(ledger): update membership fixture for evidence API
6f87a0c
style(ledger): sync generated mocks and CI formatting
55fd2b8
style(ledger): keep admission formatting stable
22403a7
refactor(ledger): coordinate membership outside raft node
205068c
refactor(ledger): move membership coordinator to membership package
2545f46
refactor(ledger): fence every cluster policy update
2622a3b
test(ledger): supply coordinator in policy fixtures
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
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
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
179 changes: 179 additions & 0 deletions
179
docs/technical/architecture/subsystems/consensus/capabilities.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,179 @@ | ||
| # Coordinated-upgrade capabilities | ||
|
|
||
| Ledger's capability metadata is the protocol foundation for future incompatible | ||
| features deployed with a coordinated stop-and-restart. It is separate from the | ||
| client-facing gRPC protocol revision. This release does not implement a | ||
| mixed-version rolling activation path. | ||
|
|
||
| ## Wire contract | ||
|
|
||
| `common.ClusterPolicy` carries the replicated activation state. | ||
| `raft.Proposal.required_capabilities` carries stable producer-owned capability | ||
| IDs. The fields are optional and therefore backward-readable by binaries that | ||
| predate this foundation. IDs are never reused; semantic dependencies must be | ||
| declared even when they do not add bytes to a proposal. | ||
|
|
||
| The producer must rebuild its plan first, derive the capabilities from that | ||
| final plan, stamp `required_capabilities`, and pass the typed result alongside | ||
| the serialized proposal. Admission reuses that result without decoding the | ||
| proposal a second time. A payload that depends on a capability but omits its | ||
| declaration is invalid; the shared builder and stamping helper are the | ||
| enforcement boundary for producers. | ||
|
|
||
| ## Admission and membership | ||
|
|
||
| The pure decision contract in `internal/infra/capabilities` evaluates every | ||
| current voter and learner. A member is represented by `(node_id, instance_id)`; | ||
| an identity refresh is a new incarnation and cannot silently inherit evidence | ||
| from the old one. Capability activation uses committed support evidence, not a | ||
| live health poll: a member that has already acknowledged the capability may be | ||
| temporarily unreachable when the activation is committed. Reachability still | ||
| blocks admission of capability-dependent writes until that member is reachable | ||
| again. Once a capability is active, every new membership announcement must | ||
| include that capability. | ||
|
|
||
| A proposal that declares a capability is admitted only when that capability is | ||
| active and every member advertises it. Proposals with no declared capability | ||
| remain unaffected. FSM apply never consults node-local capability flags. | ||
| Membership evidence is an authenticated cluster fact, not a proof of code | ||
| inspection: a node can only cause activation for IDs in the leader's local | ||
| registry, and malformed (zero, duplicate, or unsorted) declarations are | ||
| rejected. Operators must therefore treat a compromised binary as outside the | ||
| trust model of this barrier. | ||
|
|
||
| ## Capabilities that change FSM semantics | ||
|
|
||
| A capability can describe a protocol or semantic behavior, not only a payload | ||
| feature. For example, `MirrorIngestRejectInvalidAssetV2` can identify the | ||
| behavior that rejects an asset which an older FSM accepted. | ||
|
|
||
| The capability has two roles in this case: | ||
|
|
||
| 1. The replicated `ClusterPolicy` controls whether the cluster may accept the | ||
| behavior. | ||
| 2. The order declares the capability that selects the behavior during FSM | ||
| apply. | ||
|
|
||
| The serialized proposal currently carries `required_capabilities` at proposal | ||
| scope. That is sufficient only when every order in the proposal has the same | ||
| semantic contract. A proposal can batch multiple orders, so a future change | ||
| that mixes old and new semantics must either add a persisted marker to each | ||
| order (and dispatch apply from that marker) or reject mixed-semantic batches. | ||
| The conceptual marker for a homogeneous proposal is: | ||
|
|
||
| ```text | ||
| old proposal: required_capabilities = [] | ||
| new proposal: required_capabilities = [MirrorIngestRejectInvalidAssetV2] | ||
| ``` | ||
|
|
||
| The FSM must use the persisted declaration to select the behavior. It must not | ||
| infer the behavior from the current policy alone. An old order can be replayed | ||
| after the capability becomes active, and it must keep its old semantics. | ||
|
|
||
| If a producer cannot mark the new orders, the change must remain backward | ||
| compatible or add an explicit semantic version to the order. A capability by | ||
| itself is an admission barrier; it is not a historical replay version. | ||
|
|
||
| ## Adding a protocol-breaking capability | ||
|
|
||
| Use this workflow when a change can make an order apply differently on two | ||
| binary versions. | ||
|
|
||
| 1. Name the semantic contract. Describe the old behavior, the new behavior, | ||
| and the orders that use each behavior. | ||
| 2. Allocate a new capability ID. Never reuse an ID for a different contract. | ||
| 3. Add the capability to the binary registry and derive it from the final | ||
| proposal in `RequiredCapabilities`. | ||
| 4. Stamp the capability on every new order that needs the new behavior. Do not | ||
| infer it from the current policy during FSM apply. | ||
| 5. Keep the old behavior for orders that do not carry the capability. Add | ||
| replay tests that apply both order forms with the new binary. | ||
| 6. Add admission tests for an inactive capability, an active capability, and a | ||
| member that does not advertise it. | ||
| 7. Deploy the compatible binary to all current members. Let each restarted | ||
| member refresh its membership evidence before activating the capability. | ||
| 8. Activate the capability through the replicated `ClusterPolicy`. The FSM | ||
| rejects a new member or membership refresh that omits an active capability. | ||
| 9. Keep the capability active after activation. The policy processor rejects | ||
| removal of an active capability. | ||
| 10. Update this document in the same change. Record the capability ID, the | ||
| semantic contract, the replay rule, and the rollout order. | ||
|
|
||
| Do not add a capability for a bug fix that restores the intended behavior when | ||
| the old behavior was invalid. Add one when valid historical orders need to keep | ||
| their old behavior while new orders use a different rule. | ||
|
|
||
| ## Rollout and recovery | ||
|
|
||
| Deploy the barrier-aware binary to every member, stop the cluster, and restart | ||
| the complete cluster before enabling an incompatible feature. An old leader | ||
| cannot be made safe by upgrading only the other nodes. Restart, snapshot | ||
| install, restore, and downgrade code must preserve the policy. Membership | ||
| changes are not used as a rolling activation mechanism. `Node.Propose` evaluates the authoritative | ||
| policy while holding the proposal and index-tracker guards, before reserving a | ||
| predicted index or queueing work; this prevents a rejected proposal from | ||
| leaving a hole for queued successors. Removing a member remains allowed; | ||
| adding, promoting, or refreshing one without all active capabilities is | ||
| rejected. | ||
|
|
||
| `Node.Propose` is the single final policy boundary for every producer, | ||
| including mirror, events, maintenance, and backup flows; the shared plan | ||
| runner does not duplicate this check. Callers must still stamp their proposal | ||
| before use. Membership `ConfChange` operations carry and validate their | ||
| capability evidence separately. | ||
|
|
||
| ## Admission cost budget and evidence | ||
|
|
||
| The proposal gate has a deliberately bounded cost shape. For a proposal that | ||
| declares `r` capabilities and a current membership of `m` voters and learners, | ||
| the pure decision is `O(r + m*r)` and allocates only its normalized input and | ||
| blocker report. The membership snapshot is copied once per gate evaluation; | ||
| the gate must not perform network calls, wait for Raft, or hold a storage | ||
| transaction while evaluating the result. Policy decoding and the snapshot | ||
| copy remain serialized by the proposal admission boundary, so they are part | ||
| of the write-path budget rather than background work. | ||
|
|
||
| The deterministic behavior matrix is covered by | ||
| `internal/infra/capabilities/capabilities_test.go`: empty declarations, | ||
| inactive policy, active policy with converged voters and learners, missing | ||
| evidence, and unreachable members. The representative cost shape is measured | ||
| by: | ||
|
|
||
| ```text | ||
| go test ./internal/infra/capabilities -run '^$' -bench BenchmarkEvaluate -benchmem -count=5 | ||
| ``` | ||
|
|
||
| The benchmark uses three and seven members with zero, one, and four required | ||
| capabilities. Any future capability registry or policy-provider change must | ||
| rerun it and include the result in the change review. A regression must be | ||
| explained against the `m*r` bound; if the bound no longer fits the admission | ||
| budget, move decoding/snapshot preparation outside the serialized section and | ||
| pass an immutable admission snapshot instead of widening the lock. | ||
|
|
||
| ## Restart refresh | ||
|
|
||
| A normal restart does not run `JoinAsLearner`: the node restores its existing | ||
| WAL `ConfState` and keeps the same `(node_id, instance_id)` incarnation. After | ||
| the node and its Raft gRPC server are started, a node whose WAL carries | ||
| `CLUSTER_JOINED` sends an internal `RefreshMembership` request to a configured | ||
| peer. Followers forward that request to the current leader over the Raft gRPC | ||
| server. The node can therefore receive inbound Raft traffic before the refresh | ||
| commits; the refresh is an evidence update, not a pre-traffic barrier. During | ||
| the rc.2/rc.3 rolling upgrade, an older peer that returns `Unimplemented` is | ||
| treated as transient and the node keeps retrying in the background until a | ||
| compatible peer is available. The capability-dependent protocol cutover is | ||
| reserved for the coordinated rc.3/rc.4 restart, after every node runs the | ||
| barrier-aware binary. | ||
| The leader validates the identity and capability evidence against the active | ||
| policy, then proposes a | ||
| `ConfChangeUpdateNode` containing the current endpoints and capabilities. | ||
|
|
||
| Only the committed change is authoritative. The update is replicated through | ||
| Raft and applied to every member's peer store; a local restart-time upsert is | ||
| not sufficient to activate a capability for admission. A fresh WAL instead | ||
| uses `JoinAsLearner` and `ConfChangeAddLearnerNode`. | ||
|
|
||
| The cluster-policy reconciler preserves committed capability activations while | ||
| reconciling ordinary policy configuration; it does not arm capabilities from | ||
| membership advertisements. A future feature must define its activation policy | ||
| explicitly and enable it only after the coordinated restart has completed. |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.