Skip to content

Commit 428d168

Browse files
Merge branch 'alin/MR-427-state_tool-split' into 'master'
feat: [MR-427] Implement state splitting in `state-tool` Implement a split command with mutually exclusive `--retain` and `--drop` arguments, so the same canister ID ranges can be passed when splitting off both subnets A' (`--retain`) and B (`--drop`). See merge request dfinity-lab/public/ic!12534
2 parents 03c3eba + fd29c80 commit 428d168

File tree

6 files changed

+137
-1
lines changed

6 files changed

+137
-1
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rs/state_tool/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ DEPENDENCIES = [
88
"//rs/monitoring/logger",
99
"//rs/monitoring/metrics",
1010
"//rs/protobuf",
11+
"//rs/registry/routing_table",
1112
"//rs/registry/subnet_type",
1213
"//rs/replicated_state",
1314
"//rs/state_layout",

rs/state_tool/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ ic-config = { path = "../config" }
1616
ic-logger = { path = "../monitoring/logger" }
1717
ic-metrics = { path = "../monitoring/metrics" }
1818
ic-protobuf = { path = "../protobuf" }
19+
ic-registry-routing-table = { path = "../registry/routing_table" }
1920
ic-registry-subnet-type = { path = "../registry/subnet_type" }
2021
ic-replicated-state = { path = "../replicated_state" }
2122
ic-state-layout = { path = "../state_layout" }

rs/state_tool/src/commands.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,5 +6,6 @@ pub mod decode;
66
pub mod import_state;
77
pub mod list;
88
pub mod manifest;
9+
pub mod split;
910
mod utils;
1011
pub mod verify_manifest;

rs/state_tool/src/commands/split.rs

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
//! Prunes a replicated state, as part of a subnet split.
2+
3+
use ic_logger::replica_logger::no_op_logger;
4+
use ic_metrics::MetricsRegistry;
5+
use ic_registry_routing_table::{difference, CanisterIdRange, CanisterIdRanges, WellFormedError};
6+
use ic_state_manager::split::split;
7+
use ic_types::{CanisterId, PrincipalId};
8+
use std::{iter::once, path::PathBuf};
9+
10+
/// Loads the latest checkpoint under the given root; splits off the state of
11+
/// `subnet_id`, retaining or dropping the provided canister ID ranges (exactly
12+
/// one of which must be non-empty); and writes back the split state as a new
13+
/// checkpoint, under the same root.
14+
pub fn do_split(
15+
root: PathBuf,
16+
subnet_id: PrincipalId,
17+
retain: Vec<CanisterIdRange>,
18+
drop: Vec<CanisterIdRange>,
19+
) -> Result<(), String> {
20+
let canister_id_ranges = resolve(retain, drop).map_err(|e| format!("{:?}", e))?;
21+
let metrics_registry = MetricsRegistry::new();
22+
let log = no_op_logger();
23+
24+
split(root, subnet_id, canister_id_ranges, &metrics_registry, log)
25+
}
26+
27+
/// Converts a pair of `retain` and `drop` range vectors (exactly one of which
28+
/// is expected to be non-empty) into a well-formed `CanisterIdRanges` covering
29+
/// all canisters to be retained. Returns an error if the provided inputs are
30+
/// not well formed.
31+
///
32+
/// Panics if none or both of the inputs are empty.
33+
fn resolve(
34+
retain: Vec<CanisterIdRange>,
35+
drop: Vec<CanisterIdRange>,
36+
) -> Result<CanisterIdRanges, WellFormedError> {
37+
if !retain.is_empty() && drop.is_empty() {
38+
// Validate and return `retain`.
39+
CanisterIdRanges::try_from(retain)
40+
} else if retain.is_empty() && !drop.is_empty() {
41+
// Validate `drop` and return the diff between all possible canisters and it.
42+
let all_canister_ids = CanisterIdRange {
43+
start: CanisterId::from_u64(0),
44+
end: CanisterId::from_u64(u64::MAX),
45+
};
46+
difference(
47+
once(&all_canister_ids),
48+
CanisterIdRanges::try_from(drop)?.iter(),
49+
)
50+
} else {
51+
panic!("Expecting exactly one of `retain` and `drop` to be non-empty");
52+
}
53+
}
54+
55+
#[cfg(test)]
56+
mod tests {
57+
use super::*;
58+
59+
fn make_range(start: u64, end: u64) -> CanisterIdRange {
60+
CanisterIdRange {
61+
start: CanisterId::from_u64(start),
62+
end: CanisterId::from_u64(end),
63+
}
64+
}
65+
66+
#[test]
67+
fn test_resolve_retain() {
68+
let retain = make_range(3, 4);
69+
assert_eq!(
70+
CanisterIdRanges::try_from(vec![retain]),
71+
resolve(vec![retain], vec![])
72+
);
73+
}
74+
75+
#[test]
76+
fn test_resolve_drop() {
77+
let drop = make_range(3, 4);
78+
assert_eq!(
79+
CanisterIdRanges::try_from(vec![make_range(0, 2), make_range(5, u64::MAX)]),
80+
resolve(vec![], vec![drop])
81+
);
82+
}
83+
84+
#[test]
85+
fn test_resolve_not_well_formed() {
86+
let retain = make_range(4, 3);
87+
resolve(vec![retain], vec![]).unwrap_err();
88+
}
89+
90+
#[test]
91+
#[should_panic]
92+
fn test_resolve_both_non_empty() {
93+
let range = make_range(3, 4);
94+
resolve(vec![range], vec![range]).ok();
95+
}
96+
97+
#[test]
98+
#[should_panic]
99+
fn test_resolve_both_empty() {
100+
resolve(vec![], vec![]).ok();
101+
}
102+
}

rs/state_tool/src/main.rs

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
//! checkpoint manifests, import state trees).
66
77
use clap::Parser;
8+
use ic_registry_routing_table::CanisterIdRange;
9+
use ic_types::PrincipalId;
810
use std::path::PathBuf;
911

1012
mod commands;
@@ -99,7 +101,7 @@ enum Opt {
99101
/// their path.
100102
///
101103
/// To make sure that accidentally passing something that matches
102-
/// unwanted file paths, the list of processed files is explititly
104+
/// unwanted file paths, the list of processed files is explicitly
103105
/// printed.
104106
#[clap(long = "canister")]
105107
canister: String,
@@ -142,6 +144,28 @@ enum Opt {
142144
#[clap(long = "bytes")]
143145
bytes: String,
144146
},
147+
148+
/// Prunes a replicated state, as part of a subnet split.
149+
#[clap(name = "split")]
150+
#[clap(group(
151+
clap::ArgGroup::new("ranges")
152+
.required(true)
153+
.args(&["retain", "drop"]),
154+
))]
155+
Split {
156+
/// Path to the state layout.
157+
#[clap(long, required = true)]
158+
root: PathBuf,
159+
/// The ID of the subnet being split off.
160+
#[clap(long, required = true)]
161+
subnet_id: PrincipalId,
162+
/// Canister ID ranges to retain (assigned to the subnet in the routing table).
163+
#[clap(long, multiple_values(true))]
164+
retain: Vec<CanisterIdRange>,
165+
/// Canister ID ranges to drop (assigned to other subnet in the routing table).
166+
#[clap(long, multiple_values(true))]
167+
drop: Vec<CanisterIdRange>,
168+
},
145169
}
146170

147171
fn main() {
@@ -175,6 +199,12 @@ fn main() {
175199
Opt::PrincipalFromBytes { bytes } => {
176200
commands::convert_ids::do_principal_from_byte_string(bytes)
177201
}
202+
Opt::Split {
203+
root,
204+
subnet_id,
205+
retain,
206+
drop,
207+
} => commands::split::do_split(root, subnet_id, retain, drop),
178208
};
179209

180210
if let Err(e) = result {

0 commit comments

Comments
 (0)