1#[cfg(not(target_os = "wasi"))]
6use {super::page_cache, krabka_units::prelude::ByteSizeExt};
7use {
8 super::{Log, PendingDelete, RollJitter},
9 crate::{
10 error::LogError,
11 io::LogIo,
12 retention::{self, DirSync},
13 segment::Segment,
14 },
15 krabka_ids::Offset,
16 krabka_units::{Time, prelude::TimeExt as _},
17 std::{
18 collections::hash_map::RandomState,
19 hash::BuildHasher,
20 mem,
21 sync::{Arc, PoisonError},
22 time::{Duration, Instant, SystemTime},
23 },
24};
25
26#[derive(Debug, Default)]
34#[must_use = "the retired files stay on disk until `delete` runs"]
35pub struct DueDeletions {
36 io: Option<Arc<dyn LogIo>>,
37 pending: Vec<PendingDelete>,
38}
39
40#[derive(Debug)]
42pub struct FailedDeletion {
43 pub error: LogError,
45 pub remaining: DueDeletions,
48}
49
50impl DueDeletions {
51 #[must_use]
53 pub fn is_empty(&self) -> bool { self.pending.is_empty() }
54
55 pub fn delete(mut self) -> Result<(), FailedDeletion> {
62 let Some(io) = self.io.clone() else {
63 return Ok(());
64 };
65 while let Some(pending) = self.pending.first_mut() {
66 while let Some(path) = pending.paths.last() {
67 if let Err(error) = retention::remove_optional(&*io, path) {
68 return Err(FailedDeletion {
69 error,
70 remaining: self,
71 });
72 }
73 pending.paths.pop();
74 }
75 self.pending.remove(0);
76 }
77 Ok(())
78 }
79}
80
81impl Log {
82 pub fn maintain(&mut self, now: SystemTime) -> Result<(), LogError> {
89 self.rollover_flusher.check()?;
90 self.flush_if_due(Instant::now())?;
91 self.checkpoint_log_start_if_due()?;
92 self.reap_deleted_files(now)
93 }
94
95 pub fn maintain_deferring_deletions(
102 &mut self,
103 now: SystemTime,
104 ) -> Result<DueDeletions, LogError> {
105 self.rollover_flusher.check()?;
106 self.flush_if_due(Instant::now())?;
107 self.checkpoint_log_start_if_due()?;
108 Ok(self.take_due_deletions(now))
109 }
110
111 pub fn take_due_deletions(&mut self, now: SystemTime) -> DueDeletions {
113 let (due, waiting) = mem::take(&mut self.pending_deletes)
114 .into_iter()
115 .partition(|pending| pending.deadline <= now);
116 self.pending_deletes = waiting;
117 DueDeletions {
118 io: Some(Arc::clone(&self.io)),
119 pending: due,
120 }
121 }
122
123 pub fn restore_deletions(&mut self, remaining: DueDeletions, not_before: SystemTime) {
127 self.pending_deletes
128 .extend(remaining.pending.into_iter().map(|pending| PendingDelete {
129 deadline: pending.deadline.max(not_before),
130 paths: pending.paths,
131 }));
132 }
133
134 #[must_use]
137 pub fn maintenance_delay(&self, now: SystemTime) -> Option<Duration> {
138 self.maintenance_delay_at(now, Instant::now())
139 }
140
141 pub(super) fn maintenance_delay_at(
142 &self,
143 now: SystemTime,
144 elapsed: Instant,
145 ) -> Option<Duration> {
146 let config = self.config.read().unwrap_or_else(PoisonError::into_inner);
147 let flush = if self.unflushed_messages == 0 {
148 None
149 } else if config
150 .flush_messages
151 .is_some_and(|limit| self.unflushed_messages >= limit)
152 {
153 Some(Duration::ZERO)
154 } else {
155 config.flush_interval.map(|interval| {
156 Duration::from_millis(u64::try_from(interval.millis_i64_trunc()).unwrap_or(0))
157 .saturating_sub(elapsed.saturating_duration_since(self.last_flush))
158 })
159 };
160 flush
161 .into_iter()
162 .chain(self.start_checkpoint_delay(elapsed))
163 .chain(
164 self.pending_deletes
165 .iter()
166 .map(|pending| pending.deadline.duration_since(now).unwrap_or_default()),
167 )
168 .min()
169 }
170
171 pub(super) fn jittered_roll_interval(&mut self, interval: Time) -> Time {
172 let bound = self
173 .config
174 .read()
175 .unwrap_or_else(PoisonError::into_inner)
176 .segment_jitter
177 .millis_i64_trunc()
178 .min(interval.millis_i64_trunc());
179 let Some(bound) = u64::try_from(bound).ok().filter(|&bound| bound > 0) else {
180 return interval;
181 };
182 let Some(base) = self.active.as_ref().map(Segment::base_offset) else {
185 return interval;
186 };
187 let sample = match self.roll_jitter {
188 Some(RollJitter {
189 segment_base,
190 sample,
191 }) if segment_base == base => sample,
192 _ => {
193 let sample = RandomState::new().hash_one(base);
194 self.roll_jitter = Some(RollJitter {
195 segment_base: base,
196 sample,
197 });
198 sample
199 }
200 };
201 let jitter = i64::try_from(sample % bound).unwrap_or(i64::MAX);
203 Time::from_millis(interval.millis_i64_trunc().saturating_sub(jitter))
204 }
205
206 pub(super) fn flush_after_append(&mut self, delta: i32, force: bool) -> Result<(), LogError> {
207 self.unflushed_messages = self
208 .unflushed_messages
209 .saturating_add(u64::try_from(delta).unwrap_or(0) + 1);
210 let threshold = self
211 .config
212 .read()
213 .unwrap_or_else(PoisonError::into_inner)
214 .flush_messages;
215 if force {
216 self.rollover_flusher.finish()?;
218 self.active_segment_flush()?;
219 self.unflushed_messages = 0;
220 self.last_flush = Instant::now();
221 } else if threshold.is_some_and(|limit| self.unflushed_messages >= limit) {
222 self.sync()?;
223 } else {
224 self.flush_if_due(Instant::now())?;
225 }
226 Ok(())
227 }
228
229 #[cfg(not(target_os = "wasi"))]
244 pub(super) fn pace_writeback(&mut self, appended: usize) -> Result<(), LogError> {
245 let (window, budget) = {
246 let config = self.config.read().unwrap_or_else(PoisonError::into_inner);
247 let window = if config.flush_on_append {
248 0
249 } else {
250 config.writeback_window.bytes_u64()
251 };
252 (window, config.page_cache_budget.map(ByteSizeExt::bytes_u64))
253 };
254 let active = self.active.as_ref().ok_or_else(super::no_active_segment)?;
255 if window == 0 || active.writes_direct() {
256 self.writeback_unsynced = 0;
257 return Ok(());
258 }
259 self.writeback_unsynced = self
260 .writeback_unsynced
261 .saturating_add(u64::try_from(appended).unwrap_or(u64::MAX));
262 if self.writeback_unsynced < window {
263 return Ok(());
264 }
265 self.writeback_unsynced = 0;
266 self.hand_over_pages(budget)
267 }
268
269 #[cfg(not(target_os = "wasi"))]
281 pub(super) fn hand_over_pages(&mut self, budget: Option<u64>) -> Result<(), LogError> {
282 let active = self.active.as_ref().ok_or_else(super::no_active_segment)?;
283 if active.writes_direct() {
284 return Ok(());
285 }
286 let end = active.size().bytes_u64() & !(page_cache::PAGE - 1);
287 let offset = self.writeback_handed;
288 if end <= offset {
289 return Ok(());
290 }
291 while self.writeback_jobs.len() >= 2
292 && let Some(oldest) = self.writeback_jobs.pop_front()
293 {
294 self.rollover_flusher.finish_through(oldest)?;
295 }
296 self.writeback_handed = end;
297 let file = active.log_file_handle();
298 self.rollover_flusher.submit_writeback(
299 file,
300 Arc::clone(&self.io),
301 offset,
302 end - offset,
303 budget,
304 )?;
305 self.writeback_jobs
306 .push_back(self.rollover_flusher.ticket());
307 Ok(())
308 }
309
310 #[cfg(not(target_os = "wasi"))]
312 pub(super) fn page_cache_budget(&self) -> Option<u64> {
313 self.config
314 .read()
315 .unwrap_or_else(PoisonError::into_inner)
316 .page_cache_budget
317 .map(ByteSizeExt::bytes_u64)
318 }
319
320 #[cfg(not(target_os = "wasi"))]
328 pub(super) fn restart_writeback_pacing(&mut self, handed: u64) {
329 self.writeback_unsynced = 0;
330 self.writeback_jobs.clear();
331 self.writeback_handed = handed & !(page_cache::PAGE - 1);
332 }
333
334 pub(super) fn flush_if_due(&mut self, now: Instant) -> Result<(), LogError> {
335 let due = {
336 let config = self.config.read().unwrap_or_else(PoisonError::into_inner);
337 self.unflushed_messages > 0
338 && (config
339 .flush_messages
340 .is_some_and(|limit| self.unflushed_messages >= limit)
341 || config.flush_interval.is_some_and(|interval| {
342 now.saturating_duration_since(self.last_flush).as_millis()
343 >= u128::try_from(interval.millis_i64_trunc()).unwrap_or(0)
344 }))
345 };
346 if due {
347 self.sync()?;
348 self.last_flush = now;
349 }
350 Ok(())
351 }
352
353 pub(super) fn retire_segment(&mut self, base: Offset, now: SystemTime) -> Result<(), LogError> {
354 if self
355 .config
356 .read()
357 .unwrap_or_else(PoisonError::into_inner)
358 .file_delete_delay
359 <= Time::ZERO
360 {
361 return retention::delete_segment_files(&*self.io, &self.dir, base);
362 }
363 self.retire_files(
364 base,
365 now,
366 &[
367 "log",
368 "index",
369 "timeindex",
370 "txnindex",
371 "stampindex",
372 "snapshot",
373 ],
374 DirSync::Now,
375 )
376 }
377
378 pub(super) fn retire_files(
379 &mut self,
380 base: Offset,
381 now: SystemTime,
382 extensions: &[&str],
383 sync: DirSync,
384 ) -> Result<(), LogError> {
385 let delay = self
386 .config
387 .read()
388 .unwrap_or_else(PoisonError::into_inner)
389 .file_delete_delay;
390 let delay = Duration::from_millis(u64::try_from(delay.millis_i64_trunc()).unwrap_or(0));
391 let deadline = now
392 .checked_add(delay)
393 .ok_or_else(|| LogError::InvalidArgument("file deletion deadline overflow".into()))?;
394 let mut paths = Vec::new();
395 let result =
396 retention::retire_files(&*self.io, &self.dir, base, extensions, &mut paths, sync);
397 if !paths.is_empty() {
398 self.pending_deletes.push(PendingDelete { deadline, paths });
399 }
400 result
401 }
402
403 pub(super) fn reap_deleted_files(&mut self, now: SystemTime) -> Result<(), LogError> {
404 self.take_due_deletions(now)
405 .delete()
406 .map_err(|FailedDeletion { error, remaining }| {
407 self.restore_deletions(remaining, now);
408 error
409 })
410 }
411}
412
413#[cfg(test)]
414mod tests;