zeppelin_embed/lifecycle/
snapshot_copy.rs1use std::path::{Path, PathBuf};
27use std::sync::atomic::{AtomicU64, Ordering};
28
29use crate::manifest::decode_manifest;
30use crate::manifest::io::MANIFEST_FILE;
31use crate::vfs::{SyncKind, Vfs};
32use crate::wal::header::encode_header;
33use crate::wal::{LogSeq, VisibleRecord, WalWriteError};
34
35use super::{PublishedSnapshot, SnapshotLease, Store, StoreError, StoreState};
36
37const WAL_FILE: &str = "wal.ze";
38const COPY_CHUNK_BYTES: usize = 8 * 1024 * 1024;
40
41static STAGING_SEQUENCE: AtomicU64 = AtomicU64::new(0);
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub enum SnapshotTargetReason {
46 NoFinalName,
48 ParentMissing,
50 InsideStore,
52 NotADirectory,
54 NotEmpty,
56}
57
58impl std::fmt::Display for SnapshotTargetReason {
59 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
60 formatter.write_str(match self {
61 Self::NoFinalName => "has no final name",
62 Self::ParentMissing => "parent directory does not exist",
63 Self::InsideStore => "is inside the store",
64 Self::NotADirectory => "exists and is not a directory",
65 Self::NotEmpty => "is not empty",
66 })
67 }
68}
69
70struct Target {
72 parent: PathBuf,
73 path: PathBuf,
74 existed: bool,
76}
77
78struct Pinned {
80 lease: SnapshotLease,
81 manifest: Option<Vec<u8>>,
83 wal_first_seq: LogSeq,
84 wal_records: Vec<VisibleRecord>,
85}
86
87impl Store {
88 pub fn write_snapshot(&self, target: impl AsRef<Path>) -> Result<u64, StoreError> {
109 let target = resolve_target(&self.directory, target.as_ref())?;
110 let pinned = self.pin_snapshot()?;
111 let vfs = self.vfs.as_ref();
112 let staging = target.staging_path();
113 vfs.create_directory(&staging)
114 .map_err(|source| io(&staging, source))?;
115 if let Err(error) =
116 stage(vfs, &staging, &pinned).and_then(|()| target.publish(vfs, &staging))
117 {
118 remove_staging(vfs, &staging);
121 return Err(error);
122 }
123 Ok(pinned.lease.generation())
124 }
125
126 fn pin_snapshot(&self) -> Result<Pinned, StoreError> {
127 let state = self
128 .state
129 .lock()
130 .map_err(|_| StoreError::Synchronization { component: "state" })?;
131 match *state {
132 StoreState::Open => {}
133 StoreState::Closing => return Err(StoreError::Closing),
134 StoreState::Closed => return Err(StoreError::Closed),
135 }
136 let wal = self
140 .wal_writer
141 .lock()
142 .map_err(|_| StoreError::Synchronization {
143 component: "WAL writer",
144 })?;
145 let writer = wal.as_ref().ok_or(StoreError::ReadOnly)?;
146 let active = self
147 .active
148 .lock()
149 .map_err(|_| StoreError::Synchronization {
150 component: "active segment",
151 })?;
152 let generation = active.as_ref().ok_or(StoreError::Closed)?.generation;
153 let intent = self
156 .directory
157 .join(crate::ingest::purge_support::PURGE_INTENT_FILE);
158 match self.vfs.open(&intent) {
159 Ok(_) => return Err(StoreError::SnapshotPurgePending),
160 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {}
161 Err(source) => return Err(io(&intent, source)),
162 }
163 let published = self
164 .snapshot
165 .read()
166 .map_err(|_| StoreError::Synchronization {
167 component: "published snapshot",
168 })?
169 .as_ref()
170 .cloned()
171 .ok_or(StoreError::Closed)?;
172 let manifest = self.pinned_manifest(&published)?;
173 let absorbed_through = published.absorbed_through();
174 let wal_records = writer.unabsorbed_records(absorbed_through)?;
175 drop(active);
176 drop(wal);
177 drop(state);
178 Ok(Pinned {
179 lease: SnapshotLease::new_at(published, generation),
180 manifest,
181 wal_first_seq: LogSeq::new(absorbed_through.saturating_add(1)),
182 wal_records,
183 })
184 }
185
186 fn pinned_manifest(
188 &self,
189 published: &PublishedSnapshot,
190 ) -> Result<Option<Vec<u8>>, StoreError> {
191 let path = self.directory.join(MANIFEST_FILE);
192 let bytes = match self.vfs.read(&path) {
193 Ok(bytes) => bytes,
194 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
195 if published.all_segments().is_empty() && published.absorbed_through() == 0 {
196 return Ok(None);
197 }
198 return Err(StoreError::SnapshotPin {
199 detail: "the published snapshot has no committed manifest",
200 });
201 }
202 Err(source) => return Err(io(&path, source)),
203 };
204 let manifest =
205 decode_manifest(&path.display().to_string(), &bytes).map_err(StoreError::Manifest)?;
206 let mut committed = manifest
207 .segments
208 .iter()
209 .map(|segment| segment.id)
210 .collect::<Vec<_>>();
211 let mut pinned = published
212 .all_segments()
213 .iter()
214 .map(|segment| segment.meta().id)
215 .collect::<Vec<_>>();
216 committed.sort_unstable();
217 pinned.sort_unstable();
218 if manifest.generation != published.generation()
219 || manifest.log_seq != published.absorbed_through()
220 || committed != pinned
221 {
222 return Err(StoreError::SnapshotPin {
223 detail: "the committed manifest does not describe the published snapshot",
224 });
225 }
226 Ok(Some(bytes))
227 }
228}
229
230fn io(path: &Path, source: std::io::Error) -> StoreError {
231 StoreError::Io {
232 path: path.to_path_buf(),
233 source,
234 }
235}
236
237fn resolve_target(store_directory: &Path, target: &Path) -> Result<Target, StoreError> {
238 let reject = |reason| StoreError::SnapshotTarget {
239 path: target.to_path_buf(),
240 reason,
241 };
242 let name = target
243 .file_name()
244 .ok_or_else(|| reject(SnapshotTargetReason::NoFinalName))?;
245 let parent = match target.parent() {
246 Some(parent) if !parent.as_os_str().is_empty() => parent,
247 _ => Path::new("."),
248 };
249 let parent = match std::fs::canonicalize(parent) {
250 Ok(parent) => parent,
251 Err(source) if source.kind() == std::io::ErrorKind::NotFound => {
252 return Err(reject(SnapshotTargetReason::ParentMissing));
253 }
254 Err(source) => return Err(io(parent, source)),
255 };
256 let store =
257 std::fs::canonicalize(store_directory).map_err(|source| io(store_directory, source))?;
258 let path = parent.join(name);
259 if path.starts_with(&store) {
260 return Err(reject(SnapshotTargetReason::InsideStore));
261 }
262 let existed = match std::fs::symlink_metadata(&path) {
263 Ok(metadata) if metadata.is_dir() => {
264 let mut entries = std::fs::read_dir(&path).map_err(|source| io(&path, source))?;
265 if entries.next().is_some() {
266 return Err(reject(SnapshotTargetReason::NotEmpty));
267 }
268 true
269 }
270 Ok(_) => return Err(reject(SnapshotTargetReason::NotADirectory)),
271 Err(source) if source.kind() == std::io::ErrorKind::NotFound => false,
272 Err(source) => return Err(io(&path, source)),
273 };
274 Ok(Target {
275 parent,
276 path,
277 existed,
278 })
279}
280
281impl Target {
282 fn staging_path(&self) -> PathBuf {
285 let name = self
286 .path
287 .file_name()
288 .map(|name| name.to_string_lossy().into_owned())
289 .unwrap_or_default();
290 let nanos = std::time::SystemTime::now()
291 .duration_since(std::time::UNIX_EPOCH)
292 .map_or(0, |elapsed| elapsed.as_nanos());
293 let sequence = STAGING_SEQUENCE.fetch_add(1, Ordering::Relaxed);
294 self.parent.join(format!(
295 ".{name}.snapshot-{}-{nanos}-{sequence}.tmp",
296 std::process::id()
297 ))
298 }
299
300 fn publish(&self, vfs: &dyn Vfs, staging: &Path) -> Result<(), StoreError> {
301 if self.existed {
302 vfs.remove_directory(&self.path)
303 .map_err(|source| io(&self.path, source))?;
304 }
305 vfs.rename(staging, &self.path)
306 .map_err(|source| io(&self.path, source))?;
307 vfs.sync(&self.parent, SyncKind::Full)
308 .map_err(|source| io(&self.parent, source))
309 }
310}
311
312fn stage(vfs: &dyn Vfs, staging: &Path, pinned: &Pinned) -> Result<(), StoreError> {
313 let lease = &pinned.lease;
314 for segment in lease.all_segments() {
315 let bytes = segment.file_bytes();
316 if u64::try_from(bytes.len()).ok() != Some(segment.meta().file_size) {
317 return Err(StoreError::SnapshotPin {
318 detail: "a pinned segment mapping differs from its manifest length",
319 });
320 }
321 write_file(
322 vfs,
323 &staging.join(segment.meta().id.file_name()),
324 lease,
325 bytes.chunks(COPY_CHUNK_BYTES).map(Ok),
326 )?;
327 }
328 write_wal(vfs, &staging.join(WAL_FILE), pinned)?;
329 if let Some(manifest) = &pinned.manifest {
330 write_file(
331 vfs,
332 &staging.join(MANIFEST_FILE),
333 lease,
334 std::iter::once(Ok(manifest.as_slice())),
335 )?;
336 }
337 vfs.sync(staging, SyncKind::Full)
338 .map_err(|source| io(staging, source))
339}
340
341fn write_wal(vfs: &dyn Vfs, path: &Path, pinned: &Pinned) -> Result<(), StoreError> {
344 let header = encode_header(pinned.wal_first_seq)
345 .map_err(|error| StoreError::WalWrite(WalWriteError::Header(error)))?;
346 let records = pinned.wal_records.iter().map(|record| {
347 record.encoded().map_err(|source| StoreError::WalRecord {
348 seq: record.seq,
349 source,
350 })
351 });
352 write_file(
353 vfs,
354 path,
355 &pinned.lease,
356 std::iter::once(Ok(header.as_slice())).chain(records),
357 )
358}
359
360fn write_file<'a>(
361 vfs: &dyn Vfs,
362 path: &Path,
363 lease: &SnapshotLease,
364 chunks: impl IntoIterator<Item = Result<&'a [u8], StoreError>>,
365) -> Result<(), StoreError> {
366 let mut file = vfs.open_append(path).map_err(|source| io(path, source))?;
367 for chunk in chunks {
368 lease.check_active()?;
369 file.append(chunk?).map_err(|source| io(path, source))?;
370 }
371 file.sync(SyncKind::Full).map_err(|source| io(path, source))
372}
373
374fn remove_staging(vfs: &dyn Vfs, staging: &Path) {
375 if let Ok(paths) = vfs.list(staging) {
376 for path in paths {
377 let _ = vfs.delete(&path);
378 }
379 }
380 let _ = vfs.remove_directory(staging);
381}