Skip to content

Commit af5bc7f

Browse files
committed
fix: address review comments
1 parent c8f9151 commit af5bc7f

21 files changed

Lines changed: 993 additions & 322 deletions

File tree

core/binary_protocol/src/consensus/error.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,15 @@ pub enum ConsensusError {
3030
InvalidChecksum,
3131

3232
#[error(
33-
"{command:?}: header checksum {found:#034x} does not cover the frame (expected {expected:#034x})"
33+
"{command:?}: header checksum {found:#034x} does not cover the frame (expected \
34+
{expected:#034x}){}",
35+
if *found == 0 {
36+
". A zeroed checksum is the signature of a peer predating the frame seal, \
37+
which is a hard version break: replicas must be upgraded together, with the \
38+
cluster down"
39+
} else {
40+
""
41+
}
3442
)]
3543
FrameChecksumMismatch {
3644
command: Command2,

core/binary_protocol/src/consensus/header.rs

Lines changed: 85 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,11 @@ pub trait ConsensusHeader: Sized + CheckedBitPattern + NoUninit {
119119
/// boundary, so sealing them is an SDK change on both ends; [`GenericHeader`] is
120120
/// the type-erased pre-dispatch view and defers to the typed parse, where
121121
/// [`Self::verify_frame`] runs.
122-
const FRAME_SEALED: bool = true;
122+
///
123+
/// Required, not defaulted: [`Self::seal`] on an unsealed type overwrites the
124+
/// identity checksum with a frame checksum, and in release only a `debug_assert`
125+
/// stands in the way.
126+
const FRAME_SEALED: bool;
123127

124128
/// # Errors
125129
/// Returns `ConsensusError` if the header fields are inconsistent.
@@ -961,6 +965,21 @@ impl ConsensusHeader for RepairPrepareHeader {
961965
found: self.0.command,
962966
});
963967
}
968+
// Same rule as `PrepareHeader::validate`, same reason: the regions sit inside
969+
// `identity_checksum`, and a repaired prepare is journaled and later re-read
970+
// as a DVC suffix entry, where `dvc_blank`'s exact-equality classification is
971+
// what a dirty byte defeats. Not delegated, so the command check above stays
972+
// `RepairPrepare`.
973+
if self.0.reserved_frame.iter().any(|&byte| byte != 0) {
974+
return Err(ConsensusError::InvalidField(
975+
"repair_prepare: reserved_frame bytes must be zero".to_string(),
976+
));
977+
}
978+
if self.0.reserved.iter().any(|&byte| byte != 0) {
979+
return Err(ConsensusError::InvalidField(
980+
"repair_prepare: reserved bytes must be zero".to_string(),
981+
));
982+
}
964983
Ok(())
965984
}
966985
}
@@ -1028,6 +1047,8 @@ impl Default for PrepareOkHeader {
10281047
}
10291048

10301049
impl ConsensusHeader for PrepareOkHeader {
1050+
const FRAME_SEALED: bool = true;
1051+
10311052
const COMMAND: Command2 = Command2::PrepareOk;
10321053

10331054
fn checksum(&self) -> u128 {
@@ -1091,6 +1112,8 @@ const _: () = {
10911112
};
10921113

10931114
impl ConsensusHeader for CommitHeader {
1115+
const FRAME_SEALED: bool = true;
1116+
10941117
const COMMAND: Command2 = Command2::Commit;
10951118

10961119
fn checksum(&self) -> u128 {
@@ -1150,6 +1173,8 @@ const _: () = {
11501173
};
11511174

11521175
impl ConsensusHeader for StartViewChangeHeader {
1176+
const FRAME_SEALED: bool = true;
1177+
11531178
const COMMAND: Command2 = Command2::StartViewChange;
11541179

11551180
fn checksum(&self) -> u128 {
@@ -1256,6 +1281,8 @@ const _: () = {
12561281
pub const DVC_HEADERS_MAX: usize = 128;
12571282

12581283
impl ConsensusHeader for DoViewChangeHeader {
1284+
const FRAME_SEALED: bool = true;
1285+
12591286
const COMMAND: Command2 = Command2::DoViewChange;
12601287

12611288
fn checksum(&self) -> u128 {
@@ -1315,34 +1342,46 @@ impl ConsensusHeader for DoViewChangeHeader {
13151342
impl DoViewChangeHeader {
13161343
/// Number of `PrepareHeader`s in the body.
13171344
///
1318-
/// Zero is valid and means "no suffix": a replica with nothing uncommitted, or a
1319-
/// peer predating the suffix. Both contribute numbers only.
1345+
/// Zero is valid and means "no suffix": a replica with nothing uncommitted
1346+
/// contributes numbers only.
13201347
///
13211348
/// # Errors
13221349
/// [`ConsensusError::InvalidField`] when `size` is short of the header, is not a
13231350
/// whole number of headers, or exceeds what the bitsets can address.
13241351
pub fn suffix_len(&self) -> Result<usize, ConsensusError> {
1325-
let size = self.size as usize;
1326-
let Some(body_len) = size.checked_sub(HEADER_SIZE) else {
1327-
return Err(ConsensusError::InvalidField(format!(
1328-
"do_view_change: size {size} is shorter than the {HEADER_SIZE}-byte header"
1329-
)));
1330-
};
1331-
if body_len % HEADER_SIZE != 0 {
1332-
return Err(ConsensusError::InvalidField(format!(
1333-
"do_view_change: body of {body_len} bytes is not a whole number of headers"
1334-
)));
1335-
}
1336-
let suffix_len = body_len / HEADER_SIZE;
1337-
if suffix_len > DVC_HEADERS_MAX {
1338-
return Err(ConsensusError::InvalidField(format!(
1339-
"do_view_change: {suffix_len} suffix entries exceeds the maximum {DVC_HEADERS_MAX}"
1340-
)));
1341-
}
1342-
Ok(suffix_len)
1352+
suffix_len_of("do_view_change", self.size)
13431353
}
13441354
}
13451355

1356+
/// Body length of a suffix-carrying control frame, in whole [`PrepareHeader`]s.
1357+
///
1358+
/// Shared by `DoViewChange` and `StartView`: same layout, same `DVC_HEADERS_MAX`
1359+
/// bound. `frame` only names the sender in the error text.
1360+
///
1361+
/// # Errors
1362+
/// [`ConsensusError::InvalidField`] when `size` is short of the header, is not a
1363+
/// whole number of headers, or exceeds what a view change can address.
1364+
fn suffix_len_of(frame: &str, size: u32) -> Result<usize, ConsensusError> {
1365+
let size = size as usize;
1366+
let Some(body_len) = size.checked_sub(HEADER_SIZE) else {
1367+
return Err(ConsensusError::InvalidField(format!(
1368+
"{frame}: size {size} is shorter than the {HEADER_SIZE}-byte header"
1369+
)));
1370+
};
1371+
if body_len % HEADER_SIZE != 0 {
1372+
return Err(ConsensusError::InvalidField(format!(
1373+
"{frame}: body of {body_len} bytes is not a whole number of headers"
1374+
)));
1375+
}
1376+
let suffix_len = body_len / HEADER_SIZE;
1377+
if suffix_len > DVC_HEADERS_MAX {
1378+
return Err(ConsensusError::InvalidField(format!(
1379+
"{frame}: {suffix_len} suffix entries exceeds the maximum {DVC_HEADERS_MAX}"
1380+
)));
1381+
}
1382+
Ok(suffix_len)
1383+
}
1384+
13461385
// StartViewHeader - new view announcement (header-only)
13471386

13481387
/// New primary -> all replicas: start new view. Header-only.
@@ -1372,10 +1411,9 @@ pub struct StartViewHeader {
13721411
///
13731412
/// Carved from the tail of the former `reserved` region and placed LAST so it
13741413
/// lands 16-aligned with no padding WITHOUT moving `op`/`commit`/`namespace`.
1375-
/// A peer that predates it sends zeros, decoding as `incarnation == 0`, which
1376-
/// the `handle_start_view` guard treats as no claim rather than as a foreign
1377-
/// one, so a mixed-version rolling upgrade is wire-compatible: the pre-upgrade
1378-
/// peer's `StartView` is judged by the view checks alone, as before the field.
1414+
/// Zero is "no claim", which is what `handle_start_view` keys on and what the
1415+
/// unsolicited completion path sends. NOT mixed-version tolerance: the frame seal
1416+
/// drops a pre-seal peer before any field is read (see this module's header).
13791417
pub incarnation: u128,
13801418
}
13811419
const _: () = {
@@ -1391,6 +1429,8 @@ const _: () = {
13911429
};
13921430

13931431
impl ConsensusHeader for StartViewHeader {
1432+
const FRAME_SEALED: bool = true;
1433+
13941434
const COMMAND: Command2 = Command2::StartView;
13951435

13961436
fn checksum(&self) -> u128 {
@@ -1436,31 +1476,14 @@ impl StartViewHeader {
14361476
/// Number of `PrepareHeader`s in the body: the view's suffix, high-to-low op
14371477
/// from `op` down toward `commit`.
14381478
///
1439-
/// Zero means numbers only, which is what a peer predating the suffix sends and
1440-
/// what the probe-answer path sends. A backup then falls back to trusting `op`.
1479+
/// Zero means numbers only, which is what the probe-answer path sends. A backup
1480+
/// then falls back to trusting `op`.
14411481
///
14421482
/// # Errors
14431483
/// [`ConsensusError::InvalidField`] when `size` is short of the header, is not a
14441484
/// whole number of headers, or exceeds what a view change can address.
14451485
pub fn suffix_len(&self) -> Result<usize, ConsensusError> {
1446-
let size = self.size as usize;
1447-
let Some(body_len) = size.checked_sub(HEADER_SIZE) else {
1448-
return Err(ConsensusError::InvalidField(format!(
1449-
"start_view: size {size} is shorter than the {HEADER_SIZE}-byte header"
1450-
)));
1451-
};
1452-
if body_len % HEADER_SIZE != 0 {
1453-
return Err(ConsensusError::InvalidField(format!(
1454-
"start_view: body of {body_len} bytes is not a whole number of headers"
1455-
)));
1456-
}
1457-
let suffix_len = body_len / HEADER_SIZE;
1458-
if suffix_len > DVC_HEADERS_MAX {
1459-
return Err(ConsensusError::InvalidField(format!(
1460-
"start_view: {suffix_len} suffix entries exceeds the maximum {DVC_HEADERS_MAX}"
1461-
)));
1462-
}
1463-
Ok(suffix_len)
1486+
suffix_len_of("start_view", self.size)
14641487
}
14651488
}
14661489

@@ -1493,9 +1516,9 @@ pub struct RequestStartViewHeader {
14931516
/// `StartView` so a reply from a previous incarnation is detectable.
14941517
///
14951518
/// Carved from the tail of the former `reserved` region and placed LAST so it
1496-
/// lands 16-aligned with no padding WITHOUT moving `namespace`. A peer that
1497-
/// predates it sends zeros, decoding as `incarnation == 0`, so a mixed-version
1498-
/// rolling upgrade is wire-compatible.
1519+
/// lands 16-aligned with no padding WITHOUT moving `namespace`. Zero is "no claim
1520+
/// to echo"; see [`StartViewHeader::incarnation`] on why that is not
1521+
/// mixed-version tolerance.
14991522
pub incarnation: u128,
15001523
}
15011524
const _: () = {
@@ -1511,6 +1534,8 @@ const _: () = {
15111534
};
15121535

15131536
impl ConsensusHeader for RequestStartViewHeader {
1537+
const FRAME_SEALED: bool = true;
1538+
15141539
const COMMAND: Command2 = Command2::RequestStartView;
15151540

15161541
fn checksum(&self) -> u128 {
@@ -1583,6 +1608,8 @@ const _: () = {
15831608
};
15841609

15851610
impl ConsensusHeader for RequestPreparesHeader {
1611+
const FRAME_SEALED: bool = true;
1612+
15861613
const COMMAND: Command2 = Command2::RequestPrepares;
15871614

15881615
fn checksum(&self) -> u128 {
@@ -1654,6 +1681,8 @@ const _: () = {
16541681
};
16551682

16561683
impl ConsensusHeader for RepairRangeReplyHeader {
1684+
const FRAME_SEALED: bool = true;
1685+
16571686
const COMMAND: Command2 = Command2::RepairDone;
16581687

16591688
fn checksum(&self) -> u128 {
@@ -1734,6 +1763,8 @@ const _: () = {
17341763
};
17351764

17361765
impl ConsensusHeader for RequestStateTransferHeader {
1766+
const FRAME_SEALED: bool = true;
1767+
17371768
const COMMAND: Command2 = Command2::RequestStateTransfer;
17381769

17391770
fn checksum(&self) -> u128 {
@@ -1856,6 +1887,8 @@ const _: () = {
18561887
};
18571888

18581889
impl ConsensusHeader for StateTransferTargetHeader {
1890+
const FRAME_SEALED: bool = true;
1891+
18591892
const COMMAND: Command2 = Command2::StateTransferTarget;
18601893

18611894
fn checksum(&self) -> u128 {
@@ -1955,6 +1988,8 @@ const _: () = {
19551988
};
19561989

19571990
impl ConsensusHeader for RequestStateChunkHeader {
1991+
const FRAME_SEALED: bool = true;
1992+
19581993
const COMMAND: Command2 = Command2::RequestStateChunk;
19591994

19601995
fn checksum(&self) -> u128 {
@@ -2037,6 +2072,8 @@ const _: () = {
20372072
};
20382073

20392074
impl ConsensusHeader for StateChunkHeader {
2075+
const FRAME_SEALED: bool = true;
2076+
20402077
const COMMAND: Command2 = Command2::StateChunk;
20412078

20422079
fn checksum(&self) -> u128 {

core/configs/src/server_ng_config/metadata.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@ impl Validatable<ConfigurationError> for MetadataConfig {
132132
}
133133
if self.prepare_queue_depth > MAX_METADATA_PREPARE_QUEUE_DEPTH {
134134
eprintln!(
135-
"{COMPONENT_NG} metadata.prepare_queue_depth ({}) exceeds the maximum ({MAX_METADATA_PREPARE_QUEUE_DEPTH})",
135+
"{COMPONENT_NG} metadata.prepare_queue_depth ({}) exceeds the maximum \
136+
({MAX_METADATA_PREPARE_QUEUE_DEPTH}). The ceiling is the view-change wire, not memory: \
137+
a DoViewChange describes the uncommitted suffix with one bit per op in a u128 \
138+
bitset, and this depth bounds that suffix. Deeper produces entries a new \
139+
primary can neither adopt nor prove dead. Lowered from 256; not raisable.",
136140
self.prepare_queue_depth
137141
);
138142
return Err(ConfigurationError::InvalidConfigurationValue);

core/configs/src/server_ng_config/partition.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -148,7 +148,11 @@ impl Validatable<ConfigurationError> for PartitionConfig {
148148
}
149149
if self.prepare_queue_depth > MAX_PARTITION_PREPARE_QUEUE_DEPTH {
150150
eprintln!(
151-
"{COMPONENT_NG} partition.prepare_queue_depth ({}) exceeds the maximum ({MAX_PARTITION_PREPARE_QUEUE_DEPTH})",
151+
"{COMPONENT_NG} partition.prepare_queue_depth ({}) exceeds the maximum \
152+
({MAX_PARTITION_PREPARE_QUEUE_DEPTH}). The ceiling is the view-change wire, not memory: \
153+
a DoViewChange describes the uncommitted suffix with one bit per op in a u128 \
154+
bitset, and this depth bounds that suffix. Deeper produces entries a new \
155+
primary can neither adopt nor prove dead. Lowered from 256; not raisable.",
152156
self.prepare_queue_depth
153157
);
154158
return Err(ConfigurationError::InvalidConfigurationValue);

0 commit comments

Comments
 (0)