krabka_format/format/
output.rs1use {
11 crate::{ids::ClusterId, meta_properties::MetaProperties},
12 base64::{Engine as _, engine::general_purpose::STANDARD},
13 krabka_metadata::MetadataRecord,
14 serde::Serialize,
15 serde_wincode::SerdeCompat,
16 std::{env, ffi::OsString, fs, io, path::Path},
17 wincode::Serialize as _,
18};
19
20pub(super) const ZERO_CHECKPOINT_NAME: &str = "00000000000000000000-0000000000.checkpoint";
21
22pub const FAIL_AFTER_ENV: &str = "KRABKA_FORMAT_FAIL_AFTER";
26
27#[derive(Debug, Clone, Default)]
29pub(super) struct Fault(Option<OsString>);
30
31impl Fault {
32 pub(super) fn from_env() -> Self { Self(env::var_os(FAIL_AFTER_ENV)) }
34
35 fn after(&self, path: &Path) -> Result<(), String> {
37 match (&self.0, path.file_name()) {
38 (Some(name), Some(written)) if name == written => Err(format!(
39 "injected failure after {} ({FAIL_AFTER_ENV})",
40 path.display()
41 )),
42 _ => Ok(()),
43 }
44 }
45}
46
47#[tracing::instrument(
56 level = "debug",
57 name = "cli.write_meta_properties",
58 skip_all,
59 fields(log_dir = %log_dir.display(), cluster_id = %meta.cluster_id, node_id = meta.node_id),
60 err
61)]
62pub(super) fn write_meta_properties(
63 log_dir: &Path,
64 meta: &MetaProperties,
65 fault: &Fault,
66) -> Result<(), String> {
67 meta.write_observed(log_dir, |path| fault.after(path).map_err(io::Error::other))
68 .map_err(|e| {
69 format!(
70 "Error while writing meta.properties file {}: {e}",
71 log_dir.display()
72 )
73 })
74}
75
76pub(super) const BOOTSTRAP_RECORDS_VERSION: i16 = 0;
83
84pub(super) const BOOTSTRAP_MANIFEST_VERSION: u32 = 1;
92
93#[derive(Debug, Serialize)]
95struct BootstrapManifest {
96 version: u32,
98 cluster_id: ClusterId,
100 record_count: usize,
101 records_b64: Vec<String>,
105}
106
107pub(super) fn write_dynamic_checkpoint(
116 metadata_log_dir: &Path,
117 cluster_id: ClusterId,
118 control_records: &[MetadataRecord],
119 metadata_records: &[MetadataRecord],
120 fault: &Fault,
121) -> Result<(), String> {
122 let mut controls = krabka_metadata::MetadataImage::new(cluster_id.into());
123 for record in control_records {
124 controls.apply(record);
125 }
126 let bytes = krabka_raft::serialize_bootstrap_snapshot(
127 controls.kraft_version(),
128 controls.voters(),
129 metadata_records,
130 0,
131 )
132 .map_err(|e| format!("serialize offset-zero checkpoint: {e}"))?;
133 let checkpoint_dir = krabka_raft::metadata_partition_dir(metadata_log_dir);
134 fs::create_dir_all(&checkpoint_dir).map_err(|e| format!("create checkpoint directory: {e}"))?;
135 let path = checkpoint_dir.join(ZERO_CHECKPOINT_NAME);
136 fs::write(&path, bytes).map_err(|e| format!("write offset-zero checkpoint: {e}"))?;
137 fault.after(&path)
138}
139
140#[tracing::instrument(
143 level = "debug",
144 name = "cli.write_bootstrap_files",
145 skip_all,
146 fields(log_dir = %log_dir.display(), record_count = records.len()),
147 err
148)]
149pub(super) fn write_bootstrap_files(
150 log_dir: &Path,
151 cluster_id: ClusterId,
152 records: &[MetadataRecord],
153 fault: &Fault,
154) -> Result<(), String> {
155 let mut record_blobs: Vec<Vec<u8>> = Vec::with_capacity(records.len());
157 for rec in records {
158 let bytes = <SerdeCompat<MetadataRecord>>::serialize(rec)
159 .map_err(|e| format!("serialize record: {e}"))?;
160 record_blobs.push(bytes);
161 }
162
163 let mut bin = BOOTSTRAP_RECORDS_VERSION.to_be_bytes().to_vec();
166 for blob in &record_blobs {
167 let len: u32 = u32::try_from(blob.len())
168 .map_err(|_| format!("record too large: {} bytes", blob.len()))?;
169 bin.extend_from_slice(&len.to_le_bytes());
170 bin.extend_from_slice(blob);
171 }
172 let bin_path = log_dir.join("bootstrap.records.bin");
173 fs::write(&bin_path, &bin).map_err(|e| format!("write bootstrap.records.bin: {e}"))?;
174 fault.after(&bin_path)?;
175
176 let records_b64: Vec<String> = record_blobs.iter().map(|b| STANDARD.encode(b)).collect();
178 let manifest = BootstrapManifest {
179 version: BOOTSTRAP_MANIFEST_VERSION,
180 cluster_id,
181 record_count: records.len(),
182 records_b64,
183 };
184 let json =
185 serde_json::to_string_pretty(&manifest).map_err(|e| format!("serialize manifest: {e}"))?;
186 let json_path = log_dir.join("bootstrap.json");
187 fs::write(&json_path, json).map_err(|e| format!("write bootstrap.json: {e}"))?;
188 fault.after(&json_path)
189}
190
191#[cfg(test)]
192mod tests {
193 use super::*;
194
195 fn golden_records() -> Vec<MetadataRecord> {
197 vec![MetadataRecord::V1FeatureLevel(
198 krabka_metadata::FeatureLevelRecord {
199 name: "metadata.version".into(),
200 level: 30,
201 },
202 )]
203 }
204
205 const GOLDEN_RECORDS_BIN: &[u8] = include_bytes!("../../tests/fixtures/bootstrap.records.bin");
209
210 const GOLDEN_MANIFEST: &str = r#"{
212 "version": 1,
213 "cluster_id": "AQIDBAUGBwgJCgsMDQ4PEA",
214 "record_count": 1,
215 "records_b64": [
216 "EQAAABAAAAAAAAAAbWV0YWRhdGEudmVyc2lvbh4A"
217 ]
218}"#;
219
220 #[test]
223 fn the_bootstrap_files_match_their_golden_bytes() {
224 let dir = tempfile::tempdir().unwrap();
225 let cluster_id = ClusterId(uuid::Uuid::from_u128(
226 0x0102_0304_0506_0708_090a_0b0c_0d0e_0f10,
227 ));
228 write_bootstrap_files(dir.path(), cluster_id, &golden_records(), &Fault::default())
229 .unwrap();
230 assert2::assert!(
231 fs::read(dir.path().join("bootstrap.records.bin")).unwrap() == GOLDEN_RECORDS_BIN
232 );
233 assert2::assert!(
234 fs::read_to_string(dir.path().join("bootstrap.json")).unwrap() == GOLDEN_MANIFEST
235 );
236 }
237}