1use std::collections::HashMap;
23use std::fmt;
24use std::sync::{Arc, Mutex};
25use std::time::{Duration, SystemTime};
26
27use sentry::{Hub, SentryFutureExt};
28use tokio_util::task::TaskTracker;
29use tokio_util::task::task_tracker::TaskTrackerToken;
30
31use crate::backend::common::{HighVolumeBackend, MultipartUploadBackend, TieredMetadata};
32use crate::error::Result;
33use crate::id::ObjectId;
34
35const INITIAL_BACKOFF: Duration = Duration::from_millis(100);
37const MAX_BACKOFF: Duration = Duration::from_secs(30);
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash)]
45pub struct ChangeId(uuid::Uuid);
46
47impl ChangeId {
48 #[allow(clippy::new_without_default)]
50 pub fn new() -> Self {
51 Self(uuid::Uuid::now_v7())
52 }
53}
54
55impl fmt::Display for ChangeId {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 self.0.fmt(f)
58 }
59}
60
61#[derive(Debug, Clone)]
67pub struct Change {
68 pub id: ObjectId,
72 pub new: Option<ObjectId>,
76 pub old: Option<ObjectId>,
80 pub cleanup_after: Option<SystemTime>,
84}
85
86#[derive(Debug)]
92pub struct ChangeManager {
93 pub(crate) high_volume: Box<dyn HighVolumeBackend>,
95 pub(crate) long_term: Box<dyn MultipartUploadBackend>,
97 pub(crate) changelog: Box<dyn ChangeLog>,
99 pub(crate) tracker: TaskTracker,
101}
102
103impl ChangeManager {
104 pub fn new(
106 high_volume: Box<dyn HighVolumeBackend>,
107 long_term: Box<dyn MultipartUploadBackend>,
108 changelog: Box<dyn ChangeLog>,
109 ) -> Arc<Self> {
110 Arc::new(Self {
111 high_volume,
112 long_term,
113 changelog,
114 tracker: TaskTracker::new(),
115 })
116 }
117
118 #[tracing::instrument(level = "debug", fields(id = ?change.id, new = ?change.new, old = ?change.old), skip_all)]
126 pub async fn record(self: Arc<Self>, change: Change) -> Result<ChangeGuard> {
127 let token = self.tracker.token();
128
129 let id = ChangeId::new();
130 self.changelog.record(&id, &change).await?;
131
132 let state = ChangeState {
133 id,
134 change,
135 phase: ChangePhase::Recorded,
136 manager: self.clone(),
137 _token: token,
138 };
139
140 Ok(ChangeGuard {
141 state: Some(state),
142 hub: Hub::current(),
143 })
144 }
145
146 #[tracing::instrument(level = "debug", fields(id = ?change.id, new = ?change.new, old = ?change.old), skip_all)]
152 pub async fn record_assembling(self: Arc<Self>, change: Change) -> Result<ChangeGuard> {
153 let token = self.tracker.token();
154
155 let id = ChangeId::new();
156 self.changelog.record(&id, &change).await?;
157
158 let state = ChangeState {
159 id,
160 change,
161 phase: ChangePhase::Assembling,
162 manager: self.clone(),
163 _token: token,
164 };
165
166 Ok(ChangeGuard {
167 state: Some(state),
168 hub: Hub::current(),
169 })
170 }
171
172 #[tracing::instrument(level = "debug", skip_all)]
177 pub async fn recover(self: Arc<Self>) -> Result<()> {
178 let _token = self.tracker.token();
180
181 let entries =
182 self.changelog.scan().await.inspect_err(|e| {
183 objectstore_log::error!(!!e, "Failed to run changelog recovery")
184 })?;
185
186 for (id, change) in entries {
188 let state = ChangeState {
189 id,
190 change,
191 phase: ChangePhase::Recovered,
192 manager: self.clone(),
193 _token: self.tracker.token(),
194 };
195
196 state.cleanup().await;
197 }
198
199 Ok(())
200 }
201}
202
203#[async_trait::async_trait]
217pub trait ChangeLog: fmt::Debug + Send + Sync {
218 async fn record(&self, id: &ChangeId, change: &Change) -> Result<()>;
223
224 async fn remove(&self, id: &ChangeId) -> Result<()>;
229
230 async fn scan(&self) -> Result<Vec<(ChangeId, Change)>>;
239}
240
241#[derive(Debug, Clone, Default)]
246pub struct InMemoryChangeLog {
247 entries: Arc<Mutex<HashMap<ChangeId, Change>>>,
248}
249
250#[async_trait::async_trait]
251impl ChangeLog for InMemoryChangeLog {
252 async fn record(&self, id: &ChangeId, change: &Change) -> Result<()> {
253 let mut entries = self.entries.lock().expect("lock poisoned");
254 entries.insert(id.clone(), change.clone());
255 Ok(())
256 }
257
258 async fn remove(&self, id: &ChangeId) -> Result<()> {
259 let mut entries = self.entries.lock().expect("lock poisoned");
260 entries.remove(id);
261 Ok(())
262 }
263
264 async fn scan(&self) -> Result<Vec<(ChangeId, Change)>> {
265 let now = SystemTime::now();
266 let entries = self.entries.lock().expect("lock poisoned");
267 let result = entries
268 .iter()
269 .filter(|(_, change)| match change.cleanup_after {
270 None => true,
271 Some(deadline) => now >= deadline,
272 })
273 .map(|(id, change)| (id.clone(), change.clone()))
274 .collect();
275 Ok(result)
276 }
277}
278
279#[cfg(test)]
280impl InMemoryChangeLog {
281 pub fn expire_all(&self) {
283 let mut entries = self.entries.lock().expect("lock poisoned");
284 for change in entries.values_mut() {
285 change.cleanup_after = Some(SystemTime::UNIX_EPOCH);
286 }
287 }
288}
289
290#[derive(Debug, Default)]
296pub struct NoopChangeLog;
297
298#[async_trait::async_trait]
299impl ChangeLog for NoopChangeLog {
300 async fn record(&self, _id: &ChangeId, _change: &Change) -> Result<()> {
301 Ok(())
302 }
303
304 async fn remove(&self, _id: &ChangeId) -> Result<()> {
305 Ok(())
306 }
307
308 async fn scan(&self) -> Result<Vec<(ChangeId, Change)>> {
309 Ok(Vec::new())
310 }
311}
312
313#[derive(Debug, PartialEq, Eq)]
315pub enum ChangePhase {
316 Recovered,
318 Recorded,
320 Assembling,
326 Written,
328 Lost,
330 Updated,
332 Completed,
334}
335
336impl ChangePhase {
337 pub fn compare_and_write(succeeded: bool) -> Self {
339 if succeeded { Self::Updated } else { Self::Lost }
340 }
341}
342
343#[derive(Debug)]
347struct ChangeState {
348 id: ChangeId,
349 change: Change,
350 phase: ChangePhase,
351 manager: Arc<ChangeManager>,
352 _token: TaskTrackerToken,
353}
354
355impl ChangeState {
356 fn mark_completed(mut self) {
358 self.phase = ChangePhase::Completed;
359 }
360
361 #[tracing::instrument(level = "debug", skip_all, fields(phase= ?self.phase, new = ?self.change.new, old = ?self.change.old))]
363 async fn cleanup(self) {
364 let current = match self.phase {
365 ChangePhase::Recovered => self.read_tombstone().await,
367 ChangePhase::Recorded => self.change.old.clone(),
368 ChangePhase::Written => self.read_tombstone().await,
370 ChangePhase::Lost => self.change.old.clone(),
371 ChangePhase::Updated => self.change.new.clone(),
372 ChangePhase::Assembling | ChangePhase::Completed => return, };
374
375 if current != self.change.old
376 && let Some(ref old) = self.change.old
377 {
378 self.cleanup_lt(old).await;
379 }
380
381 if current != self.change.new
382 && let Some(ref new) = self.change.new
383 {
384 self.cleanup_lt(new).await;
385 }
386
387 self.cleanup_log().await;
388 self.mark_completed();
389 }
390
391 async fn read_tombstone(&self) -> Option<ObjectId> {
395 let mut delay = INITIAL_BACKOFF;
396 loop {
397 match self
398 .manager
399 .high_volume
400 .get_tiered_metadata(&self.change.id)
401 .await
402 {
403 Ok(TieredMetadata::Tombstone(t)) => return Some(t.target),
404 Ok(TieredMetadata::Object(_)) => return None,
405 Ok(TieredMetadata::NotFound) => return None,
406 Err(_) => {
407 tokio::time::sleep(delay).await;
408 delay = (delay.mul_f32(1.5)).min(MAX_BACKOFF);
409 }
410 }
411 }
412 }
413
414 async fn cleanup_lt(&self, target: &ObjectId) {
416 let mut delay = INITIAL_BACKOFF;
417 while self.manager.long_term.delete_object(target).await.is_err() {
418 tokio::time::sleep(delay).await;
419 delay = (delay.mul_f32(1.5)).min(MAX_BACKOFF);
420 }
421 }
422
423 async fn cleanup_log(&self) {
425 let mut delay = INITIAL_BACKOFF;
426 while self.manager.changelog.remove(&self.id).await.is_err() {
427 tokio::time::sleep(delay).await;
428 delay = (delay.mul_f32(1.5)).min(MAX_BACKOFF);
429 }
430 }
431}
432
433impl Drop for ChangeState {
434 fn drop(&mut self) {
435 match self.phase {
436 ChangePhase::Completed => {}
437 ChangePhase::Assembling => {
438 objectstore_log::warn!(
439 change = ?self.change,
440 "Operation dropped in Assembling state, cleanup deferred to ChangeLog recovery"
441 );
442 }
443 _ => {
444 objectstore_log::error!(
445 change = ?self.change,
446 phase = ?self.phase,
447 "Operation dropped without completing cleanup"
448 );
449 }
450 }
451 }
452}
453
454pub struct ChangeGuard {
460 state: Option<ChangeState>,
461 hub: Arc<Hub>,
462}
463
464impl ChangeGuard {
465 pub(crate) fn advance(&mut self, phase: ChangePhase) {
467 if let Some(ref mut state) = self.state {
468 state.phase = phase;
469 }
470 }
471}
472
473impl fmt::Debug for ChangeGuard {
474 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
475 f.debug_struct("ChangeGuard")
476 .field("state", &self.state)
477 .finish()
479 }
480}
481
482impl Drop for ChangeGuard {
483 fn drop(&mut self) {
484 if let Some(state) = self.state.take()
485 && state.phase != ChangePhase::Assembling
486 && state.phase != ChangePhase::Completed
487 && let Ok(handle) = tokio::runtime::Handle::try_current()
488 {
489 let hub = Hub::new_from_top(&self.hub);
490 handle.spawn(state.cleanup().bind_hub(hub));
491 }
492
493 }
495}
496
497#[cfg(test)]
498mod tests {
499 use objectstore_types::scope::{Scope, Scopes};
500
501 use super::*;
502 use crate::id::ObjectContext;
503
504 fn make_id(key: &str) -> ObjectId {
505 ObjectId::new(
506 ObjectContext {
507 usecase: "testing".into(),
508 scopes: Scopes::from_iter([Scope::create("testing", "value").unwrap()]),
509 },
510 key.into(),
511 )
512 }
513
514 #[tokio::test]
515 async fn record_then_scan_returns_entry() {
516 let log = InMemoryChangeLog::default();
517 let id = ChangeId::new();
518 let change = Change {
519 id: make_id("object-key"),
520 new: Some(make_id("object-key/rev1")),
521 old: None,
522 cleanup_after: None,
523 };
524
525 log.record(&id, &change).await.unwrap();
526
527 let entries = log.scan().await.unwrap();
528 assert_eq!(entries.len(), 1);
529 assert_eq!(entries[0].0, id);
530 }
531
532 #[tokio::test]
533 async fn remove_then_scan_does_not_return_entry() {
534 let log = InMemoryChangeLog::default();
535 let id = ChangeId::new();
536 let change = Change {
537 id: make_id("object-key"),
538 new: None,
539 old: Some(make_id("object-key/rev1")),
540 cleanup_after: None,
541 };
542
543 log.record(&id, &change).await.unwrap();
544 log.remove(&id).await.unwrap();
545
546 let entries = log.scan().await.unwrap();
547 assert!(entries.is_empty());
548 }
549
550 #[tokio::test]
551 async fn remove_nonexistent_entry_is_not_an_error() {
552 let log = InMemoryChangeLog::default();
553 let id = ChangeId::new();
554
555 log.remove(&id).await.unwrap();
556 }
557
558 #[test]
562 fn runtime_drop_while_pending_preserves_log_entry() {
563 use crate::backend::in_memory::InMemoryBackend;
564
565 let log = InMemoryChangeLog::default();
566 let manager = ChangeManager::new(
567 Box::new(InMemoryBackend::new("hv")),
568 Box::new(InMemoryBackend::new("lt")),
569 Box::new(log.clone()),
570 );
571
572 let guard = {
573 let rt = tokio::runtime::Runtime::new().unwrap();
574 rt.block_on(manager.record(Change {
576 id: make_id("crash-test"),
577 new: Some(make_id("crash-test/rev")),
578 old: None,
579 cleanup_after: None,
580 }))
581 .unwrap()
582 };
584
585 drop(guard);
587
588 let rt = tokio::runtime::Runtime::new().unwrap();
590 let entries = rt.block_on(log.scan()).unwrap();
591 assert_eq!(entries.len(), 1, "log entry must persist");
592 }
593
594 #[tokio::test]
595 async fn scan_filters_by_cleanup_after() {
596 let log = InMemoryChangeLog::default();
597
598 let ready_id = ChangeId::new();
599 log.record(
600 &ready_id,
601 &Change {
602 id: make_id("ready"),
603 new: Some(make_id("ready/rev")),
604 old: None,
605 cleanup_after: None,
606 },
607 )
608 .await
609 .unwrap();
610
611 let expired_id = ChangeId::new();
612 log.record(
613 &expired_id,
614 &Change {
615 id: make_id("expired"),
616 new: Some(make_id("expired/rev")),
617 old: None,
618 cleanup_after: Some(SystemTime::now() - Duration::from_secs(1)),
619 },
620 )
621 .await
622 .unwrap();
623
624 let deferred_id = ChangeId::new();
625 log.record(
626 &deferred_id,
627 &Change {
628 id: make_id("deferred"),
629 new: Some(make_id("deferred/rev")),
630 old: None,
631 cleanup_after: Some(SystemTime::now() + Duration::from_hours(24)),
632 },
633 )
634 .await
635 .unwrap();
636
637 let entries = log.scan().await.unwrap();
638 assert_eq!(entries.len(), 2);
639
640 let ids: Vec<_> = entries.iter().map(|(id, _)| id).collect();
641 assert!(ids.contains(&&ready_id));
642 assert!(ids.contains(&&expired_id));
643 }
644}