// Copyright 2015 The etcd Authors
// Copyright 2026 Leo Cheng
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
///|
/// Why a configuration was rejected — one variant per etcd `Config.validate`
/// branch that applies to this port. Raised by `Config::validate` /
/// `RaftNode::from_config` (etcd returns an `error` from the same branches).
pub suberror ConfigError {
/// `id` is empty (etcd: "cannot use none as id").
EmptyId
/// `heartbeat_tick <= 0`.
HeartbeatTickNotPositive
/// `election_tick <= heartbeat_tick`.
ElectionTickNotGreater
/// `max_inflight <= 0`.
MaxInflightNotPositive
/// `max_inflight_bytes` is set but below `max_msg_bytes`.
MaxInflightBytesTooSmall
/// `read_only_option` is `LeaseBased` without `check_quorum`.
LeaseBasedNeedsCheckQuorum
} derive(Eq)
///|
/// The parameters to start a server, collected into one explicit value (etcd's
/// `raft.Config`). It gathers what `RaftNode::new` otherwise takes as a dozen
/// loose optional arguments, so a caller can build, inspect and `validate` a
/// configuration before constructing the node. `RaftNode::new` remains as a
/// convenience constructor for callers that just want defaults; `from_config`
/// is the validated path.
pub struct Config {
// The identity of this server; cannot be empty.
id : String
// The other voters at start-up.
peers : Array[String]
// Ticks between elections; must exceed `heartbeat_tick`.
election_tick : Int
// Ticks between heartbeats; must be greater than 0.
heartbeat_tick : Int
// Byte cap on a single AppendEntries batch (etcd's MaxSizePerMsg).
max_msg_bytes : UInt64
// Byte cap on the uncommitted log tail (etcd's MaxUncommittedEntriesSize);
// 0 disables the check (etcd maps 0 to no limit).
max_uncommitted_size : UInt64
// In-flight AppendEntries window per follower (etcd's MaxInflightMsgs); > 0.
max_inflight : Int
// In-flight byte window per follower (etcd's MaxInflightBytes); 0 = no limit,
// otherwise must be >= max_msg_bytes.
max_inflight_bytes : UInt64
// Whether a leader checks quorum liveness and steps down when it lapses
// (etcd's CheckQuorum). Required when `read_only_option` is `LeaseBased`.
check_quorum : Bool
// Whether to run pre-vote (etcd's PreVote).
pre_vote : Bool
// How linearizable reads are confirmed (etcd's ReadOnlyOption).
read_only_option : ReadOnlyOption
// Whether a removed/demoted leader steps down (etcd's StepDownOnRemoval).
step_down_on_removal : Bool
// Whether a follower drops rather than forwards proposals (etcd's
// DisableProposalForwarding).
disable_proposal_forwarding : Bool
// Whether propose-time conf-change validation is off (etcd's
// DisableConfChangeValidation).
disable_conf_change_validation : Bool
// The last index the application has already applied (etcd's Config.Applied),
// set only when restarting so the core does not re-deliver applied entries.
applied : UInt64
// Diagnostics sink (etcd's Config.Logger).
logger : &Logger
// State-transition tracer (etcd's Config.TraceLogger).
tracer : &Tracer
// Seed for the deterministic election-timeout PRNG (this port's addition for
// reproducible simulations; etcd uses a shared crypto RNG).
seed : UInt64
}
///|
/// Build a `Config` for server `id` with the other voters `peers`, taking etcd's
/// defaults for everything unset. The result is not validated; call `validate`
/// or go through `RaftNode::from_config`.
pub fn Config::new(
id : String,
peers : Array[String],
election_tick? : Int = 10,
heartbeat_tick? : Int = 1,
max_msg_bytes? : UInt64 = 18446744073709551615UL,
max_uncommitted_size? : UInt64 = 0,
max_inflight? : Int = 256,
max_inflight_bytes? : UInt64 = 0,
check_quorum? : Bool = false,
// etcd's Config.PreVote is opt-in; its zero value is false.
pre_vote? : Bool = false,
read_only_option? : ReadOnlyOption = Safe,
step_down_on_removal? : Bool = false,
disable_proposal_forwarding? : Bool = false,
disable_conf_change_validation? : Bool = false,
applied? : UInt64 = 0,
logger? : &Logger = NopLogger::{ },
tracer? : &Tracer = NopTracer::{ },
seed? : UInt64 = 1,
) -> Config {
{
id,
peers,
election_tick,
heartbeat_tick,
max_msg_bytes,
max_uncommitted_size,
max_inflight,
max_inflight_bytes,
check_quorum,
pre_vote,
read_only_option,
step_down_on_removal,
disable_proposal_forwarding,
disable_conf_change_validation,
applied,
logger,
tracer,
seed,
}
}
///|
/// Reject an unusable configuration, mirroring etcd's `Config.validate` branch
/// for branch. The etcd checks that do not apply to this port (a nil `Storage`,
/// a local-message-target id, `MaxCommittedSizePerReady` which lives on the
/// async-storage path) are noted in `GAP_core.md` rather than enforced here.
pub fn Config::validate(self : Config) -> Unit raise ConfigError {
if self.id == "" {
raise EmptyId
}
if self.heartbeat_tick <= 0 {
raise HeartbeatTickNotPositive
}
if self.election_tick <= self.heartbeat_tick {
raise ElectionTickNotGreater
}
if self.max_inflight <= 0 {
raise MaxInflightNotPositive
}
if self.max_inflight_bytes != 0 &&
self.max_inflight_bytes < self.max_msg_bytes {
raise MaxInflightBytesTooSmall
}
if self.read_only_option is LeaseBased && !self.check_quorum {
raise LeaseBasedNeedsCheckQuorum
}
}
///|
/// Build a server from a validated `Config` (etcd's `newRaft`, which panics on an
/// invalid config; here the error is raised so the caller can handle it). This is
/// the explicit, checked counterpart to `RaftNode::new`.
pub fn RaftNode::from_config(config : Config) -> RaftNode raise ConfigError {
config.validate()
let r = RaftNode::new(
config.id,
config.peers,
seed=config.seed,
election_timeout=config.election_tick,
heartbeat_timeout=config.heartbeat_tick,
max_msg_bytes=config.max_msg_bytes,
max_uncommitted_size=config.max_uncommitted_size,
max_inflight=config.max_inflight,
max_inflight_bytes=config.max_inflight_bytes,
check_quorum=config.check_quorum,
pre_vote=config.pre_vote,
step_down_on_removal=config.step_down_on_removal,
disable_conf_change_validation=config.disable_conf_change_validation,
read_only_option=config.read_only_option,
logger=config.logger,
tracer=config.tracer,
)
if config.disable_proposal_forwarding {
r.disable_proposal_forwarding()
}
// Seed the applied watermark on a restart (etcd's Config.Applied), so the core
// does not re-deliver entries the application already applied. A no-op on a
// fresh start (empty log, applied 0).
if config.applied > 0 {
r.advance_applied(config.applied)
}
r
}