Skip to content
Merged
Show file tree
Hide file tree
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
Oct 19, 2026
653a4bd
feat(ledger): enforce capability barriers end to end
Oct 21, 2026
739c5bf
fix(ledger): declare capabilities for barriers
Oct 21, 2026
e3a13cb
chore(ledger): apply CI lint formatting
Oct 21, 2026
9e84e39
fix(ledger): refresh capabilities on member restart
Oct 22, 2026
5d3491f
feat: arm capabilities after membership refresh
Oct 22, 2026
4458608
refactor: keep capability barrier feature-neutral
Oct 22, 2026
ceaa62c
test: remove obsolete capability activation test
Oct 22, 2026
a432368
chore: drop unreleased capability field reservation
Oct 22, 2026
6515ab9
test: proxy membership refresh through gateway
Oct 22, 2026
6afc22a
feat: gate new members on active capabilities
Oct 22, 2026
a902528
refactor: remove unused replay protocol floor
Oct 22, 2026
90aeeb6
fix: close capability admission races
Oct 22, 2026
500ba97
refactor: remove capability activation arming
Oct 22, 2026
6b66f76
docs: define semantic capability behavior
Oct 22, 2026
0b66f7d
docs: add protocol capability rollout workflow
Oct 22, 2026
773bd5b
fix(consensus): preserve capability evidence during bootstrap
Oct 22, 2026
99aba29
feat(consensus): auto-activate converged capabilities
Oct 22, 2026
542c892
test(consensus): guard capability rollout contracts
Oct 22, 2026
c840bac
fix(consensus): close capability rollout review gaps
Oct 22, 2026
1792482
refactor(consensus): make node learner join capability-aware
Oct 22, 2026
85bda99
refactor(consensus): pass capability metadata without decode
Oct 22, 2026
796ad6b
chore(ledger): apply Go formatter output
Oct 22, 2026
07fcb7d
fix(consensus): centralize prepared proposal construction
Oct 22, 2026
aebd7bb
fix(consensus): make prepared proposal metadata opaque
Oct 22, 2026
d95d0d1
fix(consensus): inject membership policy provider
Oct 22, 2026
b97d5a7
fix(consensus): preserve capability activations
Oct 22, 2026
406e531
fix(consensus): preserve capabilities across snapshots
Oct 22, 2026
0e7676b
fix(consensus): propagate capabilities on learner admission
Oct 22, 2026
6c001a6
fix(consensus): retry refresh against older peers
Oct 22, 2026
2880013
fix(consensus): allow membership before initial policy
Oct 22, 2026
c131aad
fix(consensus): reconcile capability membership changes
Oct 22, 2026
4fd3564
fix(consensus): validate membership against apply policy
Oct 22, 2026
40882da
docs(consensus): clarify proposal capability scope
Oct 22, 2026
58c5203
fix(consensus): gate proposals at Raft boundary
Oct 23, 2026
c3739ca
fix(consensus): republish capability gate after recovery
Oct 23, 2026
8e8ed1c
fix(consensus): bind proposal metadata to serialization
Oct 23, 2026
68190ed
test(consensus): cover capability policy rejection paths
Oct 23, 2026
316486c
test(consensus): cover inactive capability rejection
Oct 23, 2026
8194393
fix(consensus): gate proposals before index reservation
Oct 23, 2026
2208792
docs(consensus): document proposal gate boundary
Oct 23, 2026
aa82969
perf(consensus): serialize proposals outside tracker lock
Oct 23, 2026
079ef1a
docs(upgrades): declare add-learner CLI break
Oct 23, 2026
e400de6
docs(preload): document predicted index patching
Oct 23, 2026
fd7ec7e
fix(capabilities): converge policy activation from membership evidence
Oct 26, 2026
e38b475
fix: close capability activation races
Oct 26, 2026
9803b30
fix: keep membership refresh alive after startup
Oct 26, 2026
7699030
test(consensus): remove dead proposal gate and cover refresh retry
Oct 26, 2026
7e61a6c
fix(capabilities): gate policy activation on member evidence
Oct 26, 2026
5884f25
fix: require capability dependencies in constructors
Oct 26, 2026
485b513
cleanup: remove dead capability builder hook
Oct 26, 2026
70e3005
test: use deterministic refresh cancellation wait
Oct 26, 2026
e592f1c
fix: remove dead membership refresh error state
Oct 26, 2026
f44541e
chore: keep generated formatting clean
Oct 26, 2026
ad883dc
fix: preserve refresh cancellation error
Oct 26, 2026
5d7105a
chore: keep generated policy formatting clean
Oct 26, 2026
aad3980
fix: enforce membership refresh startup errors
Oct 26, 2026
41cc2bd
chore: record generated membership refresh formatting
Oct 26, 2026
68bce13
Revert "refactor(cli): remove manual learner registration command"
Oct 26, 2026
f4f929f
test: update membership fixtures for policy provider
Oct 26, 2026
7992edb
Reapply "refactor(cli): remove manual learner registration command"
Oct 26, 2026
2085c51
refactor(membership): unify learner admission API
Oct 26, 2026
3f585ae
fix(membership): bound startup refresh attempt
Oct 27, 2026
289eff1
docs(membership): clarify refresh retry handoff
Oct 27, 2026
510f78b
test(capabilities): document proposal gate cost evidence
Oct 27, 2026
f24b104
fix(bootstrap): preserve policy revision admission guards
Oct 27, 2026
ba88ec9
fix(bootstrap): allow capability-only policy revisions
Oct 27, 2026
f5810d3
refactor(membership): unify learner join API
Oct 27, 2026
c286908
refactor(bootstrap): share membership grpc dialer
Oct 27, 2026
4a83dbe
refactor(bootstrap): extract periodic reconciler
Oct 27, 2026
4d649bc
refactor(bootstrap): name policy reconciler provider
Oct 27, 2026
3890bd4
refactor(capabilities): centralize policy projections
Oct 27, 2026
0f6e4be
refactor(membership): unify registration entrypoint
Oct 27, 2026
db7cddd
refactor(capabilities): remove redundant proposal stamper
Oct 27, 2026
ebfe298
fix: reconcile proposal API after rebase
Oct 30, 2026
ad420e5
fix: preserve lightweight proposal test setup
Oct 30, 2026
e45cdb0
fix: register available learner command
Oct 30, 2026
8b5bae7
fix: respect current learner CLI surface
Oct 30, 2026
7fdb750
fix(ledger): submit validated proposals once
Nov 2, 2026
1e7870c
fix(ledger): drop legacy refresh compatibility
Nov 2, 2026
e0697fb
Revert "fix(ledger): drop legacy refresh compatibility"
Nov 3, 2026
376e642
docs(ledger): clarify rc capability rollout boundary
Nov 3, 2026
21b6cbc
fix(ledger): remove obsolete proposal policy wrapper
Nov 3, 2026
d80396d
test(ledger): adapt proposal composition tests
Nov 3, 2026
783438a
test(ledger): update recovery fixture dependencies
Nov 3, 2026
10e3ab6
test(ledger): provide membership policy fixture
Nov 3, 2026
1b8b355
test(ledger): update store and mirror fixtures
Nov 3, 2026
b02dd91
test: use existing learner registration method
Nov 3, 2026
4f7548f
chore: reconcile raft audit manifest
Nov 3, 2026
838b899
test: reconcile post-rebase constructor coverage
Nov 5, 2026
629be60
chore: fix imports after rebase
Nov 5, 2026
550331b
test: reconcile post-rebase CI expectations
Nov 5, 2026
0c8fbed
chore(proto): regenerate with CI toolchain
Nov 5, 2026
955d888
chore: apply CI lint formatting
Nov 5, 2026
2af0cac
chore(proto): match CI comment formatting
Nov 5, 2026
d94fb29
chore(proto): align audit comment formatting
Nov 5, 2026
cfaa493
simplify capability activation for coordinated upgrades
Nov 6, 2026
c3a273b
chore(proto): regenerate after main rebase
Nov 6, 2026
ea10e69
fix: reconcile capability policy cleanup
Nov 6, 2026
1543e09
test: update admission replay machine setup
Nov 6, 2026
1439968
fix(ledger): validate native policy activations
Nov 9, 2026
2f8f6ce
docs(ledger): restore raft audit evidence scenarios
Nov 9, 2026
d6abdfd
fix(ledger): refresh membership from recovered peers
Nov 9, 2026
e72cc5b
perf(ledger): validate proposals before Raft queue
Nov 9, 2026
b1ed44a
style(ledger): keep membership refresh formatting clean
Nov 9, 2026
7ccdbda
docs(ledger): remove retired learner promotion command
Nov 9, 2026
9a0c0e9
docs(ledger): mark rc4 stopped upgrade as mandatory
Nov 9, 2026
bb4c833
fix(ledger): serialize capability activation with membership
Nov 11, 2026
72c33d4
fix(ledger): reject unsupported active capabilities on recovery
Nov 11, 2026
0d2d9d8
fix(ledger): retain activation fence after canceled wait
Nov 11, 2026
c852e72
fix(ledger): reserve predicted index suffix before tracker lock
Nov 11, 2026
0a9b48a
style(ledger): apply generated Go formatting
Nov 11, 2026
d7a1af9
fix(ledger): treat startup refresh RPC deadlines as transient
Nov 11, 2026
2f2e546
docs(ledger): clarify startup refresh deadline handling
Nov 11, 2026
c27bde1
fix(ledger): retry remote membership refresh deadlines
Nov 11, 2026
252d551
chore(proto): regenerate common descriptor with pinned protoc
Nov 11, 2026
cb1af52
refactor(ledger): inspect dedicated policy proposals for activation
Nov 11, 2026
266722e
refactor(ledger): isolate capability policy admission fence
Nov 11, 2026
190b172
refactor(ledger): inject capability membership fence separately
Nov 11, 2026
7280837
refactor(ledger): consolidate membership evidence writes
Nov 11, 2026
0e1aa37
test(ledger): update membership fixture for evidence API
Nov 11, 2026
6f87a0c
style(ledger): sync generated mocks and CI formatting
Nov 11, 2026
55fd2b8
style(ledger): keep admission formatting stable
Nov 11, 2026
22403a7
refactor(ledger): coordinate membership outside raft node
Nov 12, 2026
205068c
refactor(ledger): move membership coordinator to membership package
Nov 12, 2026
2545f46
refactor(ledger): fence every cluster policy update
Nov 12, 2026
2622a3b
test(ledger): supply coordinator in policy fixtures
Nov 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions cmd/ledgerctl/store/bootstrap_retry_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/formancehq/ledger/v3/internal/infra/backup"
"github.com/formancehq/ledger/v3/internal/infra/cache"
"github.com/formancehq/ledger/v3/internal/infra/health"
"github.com/formancehq/ledger/v3/internal/infra/membership"
"github.com/formancehq/ledger/v3/internal/infra/node"
"github.com/formancehq/ledger/v3/internal/infra/plan"
"github.com/formancehq/ledger/v3/internal/infra/state"
Expand Down Expand Up @@ -61,7 +62,7 @@ func newBootstrapRetryAdmission(t *testing.T, store *dal.Store) *admission.Admis
notifier.EXPECT().NotifyConfigChanged().AnyTimes()
machine, err := state.NewMachine(logger, registry, state.NewCacheSnapshotter(logger, registry, nil), store,
dal.NewSentinelFactory(store, false), meters, keys, shared, notifier, nil,
"", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil })
"", 0, func(*raftpb.Entry, *dal.WriteSession) error { return nil }, state.NoopCapabilityPolicyCommitHandler)
require.NoError(t, err)
require.NoError(t, state.NewRecovery(machine, store).RecoverState())
next := machine.State.LastAppliedIndex + 1
Expand Down Expand Up @@ -90,7 +91,7 @@ func newBootstrapRetryAdmission(t *testing.T, store *dal.Store) *admission.Admis
return future, nil
}).AnyTimes()

return admission.NewAdmission(store, logger, proposer,
return admission.NewAdmission(store, logger, proposer, &membership.Coordinator{},
builder, meters, gate, keys, shared, attrs, numscript.NewNumscriptCache(0), func(context.Context) error { return nil })
}

Expand Down
1 change: 1 addition & 0 deletions cmd/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,7 @@ func discoverPeersFromCluster(raftAddr string, tlsCfg bootstrap.TLSConfig, clust
Address: p.GetRaftAddress(),
ServiceAddress: p.GetServiceAddress(),
InstanceID: p.GetInstanceId(),
Capabilities: append([]uint32(nil), p.GetCapabilities()...),
})
}

Expand Down
2 changes: 1 addition & 1 deletion docs/ops/cluster-operations.md
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,6 @@ After the reset, the preloader falls back to Pebble reads (0xF1 zone) for all ca
- [Data Flows](../technical/architecture/data-flows.md) - Spool mechanics, entry processing
- [Spool](../technical/architecture/subsystems/storage/spool.md) - Spool technical implementation
- [Deployment](./deployment.md) - Helm chart configuration, CLI flags
- [CLI Reference](./cli.md) - cluster commands
- [CLI Reference](./cli.md) - `cluster promote-learner` and other cluster commands
- [Memory Management](./memory.md) - Cache rotation threshold memory impact
- [Availability and Disaster Recovery](./availability-and-recovery.md) - Failure-domain placement and quorum-loss recovery
42 changes: 34 additions & 8 deletions docs/ops/upgrades.md
Comment thread
shipfox-ai[bot] marked this conversation as resolved.
Resolved
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,20 @@ through each adjacent RC in order; do not jump to the newest RC. If the source
cannot be identified or the current Operator already rejects the pinning step,
stop and plan a separately validated migration. Do not delete the StatefulSet
or PVCs to bypass the source check.
### Maintainer-declared CLI break

The `ledgerctl cluster add-learner` command is intentionally removed from the
Comment thread
shipfox-ai[bot] marked this conversation as resolved.
Resolved
capability-evidence release. Existing scripts or runbooks invoking that command
must be updated; the command is not kept as a compatibility alias and no
protocol-version bump is required because the service gRPC contract is
unchanged.

To add a new node, configure its peer seed and start the Ledger binary. The
bootstrap sequence calls `JoinAsLearner` and advertises the capabilities
compiled into that binary. A node restarting with an existing WAL uses
`RefreshMembership` to publish updated endpoints and capability evidence.
The administrative `AddLearner` RPC remains an internal recovery API, but it is
not a supported replacement for the removed CLI workflow.

Before tagging a new release, verify the upgrade with state created by the
supported source releases. Cover affected Pebble records, Raft log and snapshot
Expand Down Expand Up @@ -328,6 +342,16 @@ validation passes. For RC.1 to RC.2, follow the
[maintenance-barrier rolling procedure](#account-metadata-address-validation).
For RC.3 to RC.4, use the [operator-required stopped restart](#operator-required-stopped-restart).

Capability metadata is the prerequisite for any future incompatible Raft entry,
execution-plan, replay, or snapshot format. This release does not support
mixed-version activation: stop every Ledger node, deploy the barrier-aware
binary everywhere, restart the complete cluster, and only then enable the new
feature. The capability reconciler does not arm features from membership
advertisements. See the [capability contract](../technical/architecture/subsystems/consensus/capabilities.md)
for the persisted replay boundary and coordinated rollout.

Use this for RC updates after the mixed-version validation above passes:

1. Record the current leader and voter set with `ledgerctl cluster status`.
2. Restart one follower on the target binary.
3. Wait until it reports normal, has caught up to the leader's commit index,
Expand Down Expand Up @@ -527,25 +551,27 @@ installation, backup/restore, rollback, or the dormant stopped-upgrade path.
The pinned binary-level rc.1 test and relevant model, restore and migration
suites remain separate gates.

The same harness has a separate, future `rc3-to-rc4` transition:
The same harness has a separate `rc3-to-rc4` transition for the mandatory
operator-required stopped restart:

```bash
UPGRADE_TRANSITION=rc3-to-rc4 RELEASE_BRANCH=release/v3.0 just test-release-operator-upgrade
```

Run it against a release branch whose operator lists `v3.0.0-rc.4` as a
mandatory stop boundary. The target operator and Ledger are still built from
one exact release-branch HEAD and tagged locally as rc.4; the harness publishes no tag
Run it after `v3.0.0-rc.3` has been published, against a release branch whose
operator lists `v3.0.0-rc.4` as a mandatory stop boundary. The target operator
and Ledger are built from one exact release-branch HEAD and tagged locally as
rc.4; the harness publishes no tag
or image. This path requires the operator's drain barrier to advance past
`Draining`, observes the StatefulSet and all old Pods absent before accepting
any target Pod, and checks that the replacement StatefulSet has a new UID while
PVC UIDs persist. It restarts a target voter to exercise replay, verifies the
pre-upgrade account, commits another transaction, checks Raft progress and
runs each voter's audit/derived-state checker after drain release. It fails
against an operator without the rc.4 boundary, so it is a release gate for the
format-change PR, not evidence that rc.3 itself needs a stop. The rc.4 release
still needs format-specific migration, snapshot, backup/restore and rollback
validation outside this scenario.
against an operator without the rc.4 boundary or if the mandatory stop is
skipped, so it is a release gate for the format-change PR, not evidence that
rc.3 itself needs a stop. The rc.4 release still needs format-specific migration,
snapshot, backup/restore and rollback validation outside this scenario.

## Coordinated Restart

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ The replication and ordering layer (`internal/infra/node`, `internal/infra/trans
| [hybrid-logical-clock.md](hybrid-logical-clock.md) | Monotonic HLC timestamps across leader changes and clock skew. |
| [removed-member-registry.md](removed-member-registry.md) | Universal member-identity invariant, committed-only peer routing, and replicated `(nodeID, instanceID)` set that prevents a removed member from silently rejoining and being auto-promoted; replay-safe per-event admission barriers, lookup costs, removal write ordering and durable restart states. |
| [controlled-shutdown.md](controlled-shutdown.md) | Raft-ordered admission cutoff, local persistence confirmation, restart behavior, rolling-upgrade capability gate, and operator completion contract. |
| [capabilities.md](capabilities.md) | Stable capability declarations, persisted activation policy, membership evidence, and mixed-version rollout boundaries. |

## Related

Expand Down
179 changes: 179 additions & 0 deletions docs/technical/architecture/subsystems/consensus/capabilities.md
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.
Loading
Loading