1use {
5 crate::{
6 error::LogError,
7 io::{IoTarget, LogIo},
8 name,
9 },
10 krabka_ids::Offset,
11 std::{
12 io::ErrorKind,
13 path::{Path, PathBuf},
14 sync::atomic::{AtomicU64, Ordering},
15 time::{Duration, SystemTime, UNIX_EPOCH},
16 },
17 tracing::instrument,
18};
19
20pub const DELETED_SUFFIX: &str = "deleted";
23
24pub fn deleted_path(path: &Path) -> PathBuf {
26 let mut name = path.as_os_str().to_os_string();
27 name.push(".");
28 name.push(DELETED_SUFFIX);
29 PathBuf::from(name)
30}
31
32#[must_use]
35pub fn now_ms(now: SystemTime) -> i64 {
36 let millis = now
37 .duration_since(UNIX_EPOCH)
38 .unwrap_or(Duration::ZERO)
39 .as_millis();
40 i64::try_from(millis).unwrap_or(i64::MAX)
41}
42
43#[instrument(level = "debug", skip_all, fields(dir = %dir.display(), base_offset = base_offset.0), err)]
66pub fn delete_segment_files(
67 io: &dyn LogIo,
68 dir: &Path,
69 base_offset: Offset,
70) -> Result<(), LogError> {
71 for tombstone in retire_segment_files(io, dir, base_offset)? {
72 remove_optional(io, &tombstone)?;
73 }
74 Ok(())
75}
76
77pub fn retire_segment_files(
79 io: &dyn LogIo,
80 dir: &Path,
81 base_offset: Offset,
82) -> Result<Vec<PathBuf>, LogError> {
83 let mut tombstones = Vec::with_capacity(6);
84 for path in [
85 name::log_path(dir, base_offset.0),
86 name::index_path(dir, base_offset.0),
87 name::timeindex_path(dir, base_offset.0),
88 name::txnindex_path(dir, base_offset.0),
89 name::stampindex_path(dir, base_offset.0),
90 name::producer_snapshot_path(dir, base_offset.0),
91 ] {
92 let tombstone = deleted_path(&path);
93 match io.rename(IoTarget::SegmentDeletion, &path, &tombstone) {
94 Ok(()) => tombstones.push(tombstone),
95 Err(error) if error.kind() == ErrorKind::NotFound => {
98 if tombstone.exists() {
99 tombstones.push(tombstone);
100 }
101 }
102 Err(error) => return Err(LogError::Io(error)),
103 }
104 }
105 Ok(tombstones)
106}
107
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum DirSync {
111 Now,
113 Deferred,
116}
117
118pub fn retire_files(
122 io: &dyn LogIo,
123 dir: &Path,
124 base: Offset,
125 extensions: &[&str],
126 retired: &mut Vec<PathBuf>,
127 sync: DirSync,
128) -> Result<(), LogError> {
129 static GENERATION: AtomicU64 = AtomicU64::new(0);
130 let generation = GENERATION.fetch_add(1, Ordering::Relaxed);
131 let mut renamed = Vec::new();
132 let result = (|| {
133 for extension in extensions {
134 let path = dir.join(format!("{}.{extension}", name::format_base_offset(base.0)));
135 let tombstone = dir.join(format!(
136 "{}.{extension}.{generation}.deleted",
137 name::format_base_offset(base.0)
138 ));
139 match io.rename(IoTarget::SegmentDeletion, &path, &tombstone) {
140 Ok(()) => renamed.push((path, tombstone)),
141 Err(error) if error.kind() == ErrorKind::NotFound => {}
142 Err(error) => return Err(LogError::Io(error)),
143 }
144 }
145 if sync == DirSync::Now {
146 io.sync_dir(dir)?;
147 }
148 Ok(())
149 })();
150 if let Err(error) = result {
151 for (path, tombstone) in renamed.iter().rev() {
154 io.rename(IoTarget::SegmentDeletion, tombstone, path)?;
155 }
156 io.sync_dir(dir)?;
157 return Err(error);
158 }
159 retired.extend(renamed.into_iter().map(|(_, tombstone)| tombstone));
160 Ok(())
161}
162
163pub fn remove_optional(io: &dyn LogIo, path: &Path) -> Result<(), LogError> {
164 remove_optional_counted(io, path).map(|_| ())
165}
166
167pub fn remove_optional_counted(io: &dyn LogIo, path: &Path) -> Result<bool, LogError> {
169 match io.remove_file(IoTarget::SegmentDeletion, path) {
170 Ok(()) => Ok(true),
171 Err(error) if error.kind() == ErrorKind::NotFound => Ok(false),
172 Err(error) => Err(LogError::Io(error)),
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use {
179 super::*,
180 crate::io::FileIo,
181 assert2::check,
182 std::{
183 fs,
184 io::{self, ErrorKind},
185 path::PathBuf,
186 },
187 tempfile::tempdir,
188 };
189
190 struct SegmentFiles {
192 dir: tempfile::TempDir,
193 paths: Vec<PathBuf>,
194 }
195
196 fn segment_files(base: Offset, extensions: &[&str], contents: &[u8]) -> SegmentFiles {
197 let dir = tempdir().unwrap();
198 let paths = extensions
199 .iter()
200 .map(|extension| {
201 dir.path()
202 .join(format!("{}.{extension}", name::format_base_offset(base.0)))
203 })
204 .collect::<Vec<_>>();
205 for path in &paths {
206 fs::write(path, contents).unwrap();
207 }
208 SegmentFiles { dir, paths }
209 }
210
211 #[test]
212 fn delete_segment_files_removes_required_and_optional_sidecars() {
213 let base = Offset(7);
214 let SegmentFiles { dir, paths } = segment_files(
215 base,
216 &[
217 "log",
218 "index",
219 "timeindex",
220 "txnindex",
221 "stampindex",
222 "snapshot",
223 ],
224 &[],
225 );
226
227 delete_segment_files(&FileIo, dir.path(), base).unwrap();
228
229 assert2::assert!(paths.iter().all(|path| !path.exists()));
230 }
231
232 #[test]
233 fn delete_segment_files_accepts_missing_optional_sidecars() {
234 let base = Offset(8);
235 let SegmentFiles { dir, .. } = segment_files(base, &["log", "index", "timeindex"], &[]);
236
237 delete_segment_files(&FileIo, dir.path(), base).unwrap();
238 }
239
240 #[test]
244 fn deletion_renames_every_file_to_a_tombstone_before_it_unlinks_any() {
245 #[derive(Debug)]
247 struct NoUnlink;
248
249 impl LogIo for NoUnlink {
250 fn remove_file(&self, _target: IoTarget, _path: &Path) -> io::Result<()> {
251 Err(ErrorKind::PermissionDenied.into())
252 }
253 }
254
255 let base = Offset(9);
256 let SegmentFiles { dir, paths: live } =
257 segment_files(base, &["log", "index", "timeindex", "stampindex"], b"bytes");
258
259 let error = delete_segment_files(&NoUnlink, dir.path(), base)
260 .expect_err("the refused unlink must be reported");
261
262 check!(matches!(error, LogError::Io(_)));
263 for path in &live {
264 check!(!path.exists(), "renamed away: {}", path.display());
265 check!(
266 deleted_path(path).exists(),
267 "tombstoned: {}",
268 deleted_path(path).display()
269 );
270 }
271
272 delete_segment_files(&FileIo, dir.path(), base).unwrap();
275 for path in &live {
276 check!(!deleted_path(path).exists());
277 }
278 }
279
280 #[test]
281 fn remove_optional_propagates_non_missing_errors() {
282 let dir = tempdir().unwrap();
283 let path = dir.path().join("not-a-file");
284 fs::create_dir(&path).unwrap();
285
286 assert2::assert!(let Err(LogError::Io(_)) = remove_optional(&FileIo, &path));
287 }
288
289 #[test]
290 fn remove_optional_ignores_missing_file() {
291 let dir = tempdir().unwrap();
292 let path = dir.path().join("nonexistent-file");
293 assert2::assert!(remove_optional(&FileIo, &path).is_ok());
294 }
295}