qualia_core_db/q42/volume/
publish.rs1use std::collections::HashMap;
4use std::io;
5use std::path::{Path, PathBuf};
6
7use super::super::{
8 write_volume_root, Q42Volume, Q42VolumeManifest, StreamingQ42VolumeWriter,
9};
10use crate::NQuin;
11
12pub const DEFAULT_SEGMENT_MAX_BYTES: u64 = 512 * 1024 * 1024;
14
15pub struct Q42RolloverPublisher {
18 dir: PathBuf,
19 stem: String,
20 lex: HashMap<u64, String>,
21 max_bytes: u64,
22 writer: StreamingQ42VolumeWriter,
23 child_paths: Vec<PathBuf>,
24 next_index: u32,
25}
26
27impl Q42RolloverPublisher {
28 pub fn new(dir: impl Into<PathBuf>, stem: impl Into<String>, lex: HashMap<u64, String>) -> io::Result<Self> {
29 Self::with_limit(dir, stem, lex, DEFAULT_SEGMENT_MAX_BYTES)
30 }
31
32 pub fn with_limit(
33 dir: impl Into<PathBuf>,
34 stem: impl Into<String>,
35 lex: HashMap<u64, String>,
36 max_bytes: u64,
37 ) -> io::Result<Self> {
38 if max_bytes < 8 * 1024 {
39 return Err(io::Error::new(
40 io::ErrorKind::InvalidInput,
41 "Q42 segment cap is too small",
42 ));
43 }
44 let dir = dir.into();
45 std::fs::create_dir_all(&dir)?;
46 Ok(Self {
47 writer: StreamingQ42VolumeWriter::new(&lex)?,
48 lex,
49 dir,
50 stem: stem.into(),
51 max_bytes,
52 child_paths: Vec::new(),
53 next_index: 0,
54 })
55 }
56
57 pub fn push_block(&mut self, seq_id: u64, quins: &[NQuin]) -> io::Result<()> {
58 let projected = self.writer.maximum_final_length_after_next_block()?;
59 if self.writer.block_count() > 0 && projected > self.max_bytes {
60 self.roll()?;
61 }
62 self.writer.push_block(seq_id, quins)
63 }
64
65 fn roll(&mut self) -> io::Result<()> {
66 let path = self.child_path(self.next_index);
67 let writer = std::mem::replace(
68 &mut self.writer,
69 StreamingQ42VolumeWriter::new(&self.lex)?,
70 );
71 writer.finish(&path)?;
72 self.child_paths.push(path);
73 self.next_index += 1;
74 Ok(())
75 }
76
77 fn child_path(&self, index: u32) -> PathBuf {
78 self.dir.join(format!("{}-{:04}.q42", self.stem, index))
79 }
80
81 pub fn finish(mut self) -> io::Result<PathBuf> {
83 if self.writer.block_count() > 0 || self.child_paths.is_empty() {
84 let path = self.child_path(self.next_index);
85 self.writer.finish(&path)?;
86 self.child_paths.push(path);
87 }
88 let mut segments = Vec::new();
89 for path in &self.child_paths {
90 let name = path
91 .file_name()
92 .and_then(|n| n.to_str())
93 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "Q42 child name"))?
94 .to_string();
95 segments.push(Q42VolumeManifest::segment_from_file(path, name)?);
96 }
97 let root = self.dir.join(format!("{}-root.q42", self.stem));
98 write_volume_root(
99 &root,
100 &Q42VolumeManifest {
101 generation: 1,
102 segments,
103 lexicon_segments: Vec::new(),
104 },
105 )?;
106 Ok(root)
107 }
108}
109
110pub fn append_segment_to_root(root_path: &Path, new_child: &Path) -> io::Result<()> {
113 let parent = root_path
114 .parent()
115 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "Q42 root has no parent"))?;
116 let volume = Q42Volume::open(root_path)?;
117 let mut manifest = volume.volume_manifest()?.ok_or_else(|| {
118 io::Error::new(
119 io::ErrorKind::InvalidInput,
120 "append requires a volume-root catalog",
121 )
122 })?;
123 let name = new_child
124 .file_name()
125 .and_then(|n| n.to_str())
126 .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "child name"))?
127 .to_string();
128 manifest
129 .segments
130 .push(Q42VolumeManifest::segment_from_file(new_child, name)?);
131 manifest.generation = manifest.generation.saturating_add(1);
132 let tmp = parent.join(format!(
133 ".{}.tmp",
134 root_path.file_name().and_then(|n| n.to_str()).unwrap_or("root.q42")
135 ));
136 write_volume_root(&tmp, &manifest)?;
137 std::fs::rename(&tmp, root_path)?;
138 Ok(())
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use crate::NQuin;
145
146 fn quin(object: u64) -> NQuin {
147 NQuin {
148 subject: object,
149 predicate: 1,
150 object,
151 context: 0,
152 metadata: 0,
153 parity: object ^ 1 ^ object,
154 }
155 }
156
157 #[test]
158 fn rolls_when_the_next_block_would_exceed_the_cap() {
159 let dir = tempfile::TempDir::new().unwrap();
160 let mut lex = HashMap::new();
161 lex.insert(1, "p".into());
162 let mut pubr = Q42RolloverPublisher::with_limit(dir.path(), "set", lex, 16 * 1024).unwrap();
163 for i in 0..8u64 {
164 pubr.push_block(i, &[quin(i + 1)]).unwrap();
165 }
166 let root = pubr.finish().unwrap();
167 let volume = Q42Volume::open(&root).unwrap();
168 let manifest = volume.volume_manifest().unwrap().unwrap();
169 assert!(
170 manifest.segments.len() >= 2,
171 "expected rollover, got {} children",
172 manifest.segments.len()
173 );
174 }
175
176 #[test]
177 fn append_publishes_a_new_generation() {
178 let dir = tempfile::TempDir::new().unwrap();
179 let mut lex = HashMap::new();
180 lex.insert(1, "p".into());
181 let mut pubr = Q42RolloverPublisher::with_limit(dir.path(), "gen", lex.clone(), 1024 * 1024)
182 .unwrap();
183 pubr.push_block(0, &[quin(1)]).unwrap();
184 let root = pubr.finish().unwrap();
185 let extra = dir.path().join("extra.q42");
186 let mut w = StreamingQ42VolumeWriter::new(&lex).unwrap();
187 w.push_block(1, &[quin(2)]).unwrap();
188 w.finish(&extra).unwrap();
189 append_segment_to_root(&root, &extra).unwrap();
190 let volume = Q42Volume::open(&root).unwrap();
191 let manifest = volume.volume_manifest().unwrap().unwrap();
192 assert_eq!(manifest.generation, 2);
193 assert_eq!(manifest.segments.len(), 2);
194 }
195}