Skip to main content

mz_persist_client/internal/
gc.rs

1// Copyright Materialize, Inc. and contributors. All rights reserved.
2//
3// Use of this software is governed by the Business Source License
4// included in the LICENSE file.
5//
6// As of the Change Date specified in that file, in accordance with
7// the Business Source License, use of this software will be governed
8// by the Apache License, Version 2.0.
9
10use std::borrow::Borrow;
11use std::collections::BTreeSet;
12use std::fmt::Debug;
13use std::marker::PhantomData;
14use std::mem;
15use std::sync::Arc;
16use std::time::Instant;
17
18use differential_dataflow::difference::Monoid;
19use differential_dataflow::lattice::Lattice;
20use futures_util::StreamExt;
21use futures_util::stream::FuturesUnordered;
22use prometheus::Counter;
23use timely::progress::Timestamp;
24use tokio::sync::mpsc::UnboundedSender;
25use tokio::sync::{Semaphore, mpsc, oneshot};
26use tracing::{Instrument, Span, debug, debug_span, warn};
27
28use crate::async_runtime::IsolatedRuntime;
29use crate::batch::PartDeletes;
30use crate::cfg::{GC_BLOB_DELETE_CONCURRENCY_LIMIT, GC_REMOVE_FINALIZED_ROLLUPS};
31
32use mz_ore::cast::CastFrom;
33use mz_ore::collections::HashSet;
34use mz_ore::soft_assert_or_log;
35use mz_persist::location::{Blob, SeqNo};
36use mz_persist_types::{Codec, Codec64};
37
38use crate::ShardId;
39use crate::internal::machine::{Machine, retry_external};
40use crate::internal::maintenance::RoutineMaintenance;
41use crate::internal::metrics::{GcStepTimings, RetryMetrics};
42use crate::internal::paths::{BlobKey, PartialBlobKey, PartialRollupKey};
43use crate::internal::state::{GC_USE_ACTIVE_GC, HollowBlobRef};
44use crate::internal::state_versions::{InspectDiff, StateVersionsIter, UntypedStateVersionsIter};
45
46#[derive(Debug, Clone, PartialEq)]
47pub struct GcReq {
48    pub shard_id: ShardId,
49    pub new_seqno_since: SeqNo,
50}
51
52#[derive(Debug)]
53pub struct GarbageCollector<K, V, T, D> {
54    sender: UnboundedSender<(GcReq, oneshot::Sender<RoutineMaintenance>)>,
55    _phantom: PhantomData<fn() -> (K, V, T, D)>,
56}
57
58impl<K, V, T, D> Clone for GarbageCollector<K, V, T, D> {
59    fn clone(&self) -> Self {
60        GarbageCollector {
61            sender: self.sender.clone(),
62            _phantom: PhantomData,
63        }
64    }
65}
66
67/// Cleanup for no longer necessary blobs and consensus versions.
68///
69/// - Every read handle, snapshot, and listener is given a capability on seqno
70///   with a very long lease (allowing for infrequent heartbeats). This is a
71///   guarantee that no blobs referenced by the state at that version will be
72///   deleted (even if they've been compacted in some newer version of the
73///   state). This is called a seqno_since in the code as it has obvious
74///   parallels to how sinces work at the shard/collection level. (Is reusing
75///   "since" here a good idea? We could also call it a "seqno capability" or
76///   something else instead.)
77/// - Every state transition via apply_unbatched_cmd has the opportunity to
78///   determine that the overall seqno_since for the shard has changed. In the
79///   common case in production, this will be in response to a snapshot
80///   finishing or a listener emitting some batch.
81/// - It would be nice if this only ever happened in response to read-y things
82///   (there'd be a nice parallel to how compaction background work is only
83///   spawned by write activity), but if there are no readers, we still very
84///   much want to continue to garbage collect. Notably, if there are no
85///   readers, we naturally only need to hold a capability on the current
86///   version of state. This means that if there are only writers, a write
87///   commands will result in the seqno_since advancing immediately from the
88///   previous version of the state to the new one.
89/// - Like Compacter, GarbageCollector uses a heuristic to ignore some requests
90///   to save work. In this case, the tradeoff is between consensus traffic
91///   (plus a bit of cpu) and keeping blobs around longer than strictly
92///   necessary. This is correct because a process could always die while
93///   executing one of these requests (or be slow and still working on it when
94///   the next request is generated), so we anyway need to handle them being
95///   dropped.
96/// - GarbageCollector works by `Consensus::scan`-ing for every live version of
97///   state (ignoring what the request things the prev_state_seqno was for the
98///   reasons mentioned immediately above). It then walks through them in a
99///   loop, accumulating a BTreeSet of every referenced blob key. When it finds
100///   the version corresponding to the new_seqno_since, it removes every blob in
101///   that version of the state from the BTreeSet and exits the loop. This
102///   results in the BTreeSet containing every blob eligible for deletion. It
103///   deletes those blobs and then truncates the state to the new_seqno_since to
104///   indicate that this work doesn't need to be done again.
105/// - Note that these requests are being processed concurrently, so it's always
106///   possible that some future request has already deleted the blobs and
107///   truncated consensus. It's also possible that this is the future request.
108///   As a result, the only guarantee that we get is that the current version of
109///   head is >= new_seqno_since.
110/// - (Aside: The above also means that if Blob is not linearizable, there is a
111///   possible race where a blob gets deleted before it written and thus is
112///   leaked. We anyway always have the possibility of a write process being
113///   killed between when it writes a blob and links it into state, so this is
114///   fine; it'll be caught and fixed by the same mechanism.)
115impl<K, V, T, D> GarbageCollector<K, V, T, D>
116where
117    K: Debug + Codec,
118    V: Debug + Codec,
119    T: Timestamp + Lattice + Codec64 + Sync,
120    D: Monoid + Codec64,
121{
122    pub fn new(machine: Machine<K, V, T, D>, isolated_runtime: Arc<IsolatedRuntime>) -> Self {
123        let (gc_req_sender, mut gc_req_recv) =
124            mpsc::unbounded_channel::<(GcReq, oneshot::Sender<RoutineMaintenance>)>();
125
126        // spin off a single task responsible for executing GC requests.
127        // work is enqueued into the task through a channel
128        let _worker_handle = mz_ore::task::spawn(|| "PersistGcWorker", async move {
129            while let Some((req, completer)) = gc_req_recv.recv().await {
130                let mut consolidated_req = req;
131                let mut gc_completed_senders = vec![completer];
132
133                // check if any further gc requests have built up. we'll merge their requests
134                // together and run a single GC pass to satisfy all of them
135                while let Ok((req, completer)) = gc_req_recv.try_recv() {
136                    assert_eq!(req.shard_id, consolidated_req.shard_id);
137                    gc_completed_senders.push(completer);
138                    consolidated_req.new_seqno_since =
139                        std::cmp::max(req.new_seqno_since, consolidated_req.new_seqno_since);
140                }
141
142                let merged_requests = gc_completed_senders.len() - 1;
143                if merged_requests > 0 {
144                    machine
145                        .applier
146                        .metrics
147                        .gc
148                        .merged
149                        .inc_by(u64::cast_from(merged_requests));
150                    debug!(
151                        "Merged {} gc requests together for shard {}",
152                        merged_requests, consolidated_req.shard_id
153                    );
154                }
155
156                let gc_span = debug_span!(parent: None, "gc_and_truncate", shard_id=%consolidated_req.shard_id);
157                gc_span.follows_from(&Span::current());
158
159                let start = Instant::now();
160                machine.applier.metrics.gc.started.inc();
161                let (mut maintenance, _stats) = {
162                    let name = format!("gc_and_truncate ({})", consolidated_req.shard_id);
163                    let machine = machine.clone();
164                    isolated_runtime
165                        .spawn_named(|| name, async move {
166                            Self::gc_and_truncate(&machine, consolidated_req)
167                                .instrument(gc_span)
168                                .await
169                        })
170                        .await
171                };
172                machine.applier.metrics.gc.finished.inc();
173                machine.applier.shard_metrics.gc_finished.inc();
174                machine
175                    .applier
176                    .metrics
177                    .gc
178                    .seconds
179                    .inc_by(start.elapsed().as_secs_f64());
180
181                // inform all callers who enqueued GC reqs that their work is complete
182                for sender in gc_completed_senders {
183                    // we can safely ignore errors here, it's possible the caller
184                    // wasn't interested in waiting and dropped their receiver.
185                    // maintenance will be somewhat-arbitrarily assigned to the first oneshot.
186                    let _ = sender.send(mem::take(&mut maintenance));
187                }
188            }
189        });
190
191        GarbageCollector {
192            sender: gc_req_sender,
193            _phantom: PhantomData,
194        }
195    }
196
197    /// Enqueues a [GcReq] to be consumed by the GC background task when available.
198    ///
199    /// Returns a future that indicates when GC has cleaned up to at least [GcReq::new_seqno_since]
200    pub fn gc_and_truncate_background(
201        &self,
202        req: GcReq,
203    ) -> Option<oneshot::Receiver<RoutineMaintenance>> {
204        let (gc_completed_sender, gc_completed_receiver) = oneshot::channel();
205        let new_gc_sender = self.sender.clone();
206        let send = new_gc_sender.send((req, gc_completed_sender));
207
208        if let Err(e) = send {
209            // In the steady state we expect this to always succeed, but during
210            // shutdown it is possible the destination task has already spun down
211            warn!(
212                "gc_and_truncate_background failed to send gc request: {}",
213                e
214            );
215            return None;
216        }
217
218        Some(gc_completed_receiver)
219    }
220
221    pub(crate) async fn gc_and_truncate(
222        machine: &Machine<K, V, T, D>,
223        req: GcReq,
224    ) -> (RoutineMaintenance, GcResults) {
225        let mut step_start = Instant::now();
226        let mut report_step_timing = |counter: &Counter| {
227            let now = Instant::now();
228            counter.inc_by(now.duration_since(step_start).as_secs_f64());
229            step_start = now;
230        };
231        assert_eq!(req.shard_id, machine.shard_id());
232        let remove_finalized = GC_REMOVE_FINALIZED_ROLLUPS.get(&machine.applier.cfg);
233
234        // Double check our GC req: seqno_since will never regress
235        // so we can verify it's not somehow greater than the last-
236        // known seqno_since
237        if req.new_seqno_since > machine.applier.seqno_since() {
238            machine
239                .applier
240                .fetch_and_update_state(Some(req.new_seqno_since))
241                .await;
242            let current_seqno_since = machine.applier.seqno_since();
243            assert!(
244                req.new_seqno_since <= current_seqno_since,
245                "invalid gc req: {:?} vs machine seqno_since {}",
246                req,
247                current_seqno_since
248            );
249        }
250
251        // First, check the latest known state to this process to see
252        // if there's relevant GC work for this seqno_since
253        let gc_rollups =
254            GcRollups::new(machine.applier.rollups_lte_seqno(req.new_seqno_since), &req);
255        let rollups_to_remove_from_state = gc_rollups.rollups_to_remove_from_state();
256        report_step_timing(&machine.applier.metrics.gc.steps.find_removable_rollups);
257
258        let mut gc_results = GcResults::default();
259
260        if rollups_to_remove_from_state.is_empty() {
261            // Either the work has already been done, or there aren't enough rollups
262            // <= seqno_since to have any to delete.
263            return Self::skip_gc(machine, gc_results, remove_finalized).await;
264        }
265
266        debug!(
267            "Finding all rollups <= ({}). Will truncate: {:?}. Will remove rollups from state: {:?}",
268            req.new_seqno_since,
269            gc_rollups.truncate_seqnos().collect::<Vec<_>>(),
270            rollups_to_remove_from_state,
271        );
272
273        let use_active_gc = GC_USE_ACTIVE_GC.get(&machine.applier.cfg);
274        let mut states = if use_active_gc {
275            let diffs = machine
276                .applier
277                .state_versions
278                .fetch_live_diffs_through(&req.shard_id, req.new_seqno_since)
279                .await;
280
281            // An empty result means Consensus holds no diff at or below `new_seqno_since`.
282            // Some other process has already truncated past this request, so the request's
283            // goal is met and there are no diffs left from which to derive blob deletions.
284            // Common on tombstones, whose GC skips the `active_gc` check.
285            let Some(initial_seqno) = diffs.first().map(|x| x.seqno) else {
286                debug!(
287                    ?req,
288                    "skipping gc - consensus already truncated past seqno_since. concurrent GC?"
289                );
290                return Self::skip_gc(machine, gc_results, remove_finalized).await;
291            };
292
293            let Some(initial_rollup) = gc_rollups.get(initial_seqno) else {
294                // The latest state is always expected to have a reference to a rollup for the
295                // earliest seqno. If our state doesn't, that could mean:
296                // - Our diffs are too old, and some other process has already truncated past this point.
297                //   (But currently we fetch diffs _after_ checking state, so that shouldn't happen.)
298                // - Our diffs are too new... someone has added a rollup for this seqno _after_ we fetched
299                //   state, then truncated to it.
300                // In either case we're working on outdated data and should stop.
301                debug!(
302                    ?initial_seqno,
303                    ?gc_rollups,
304                    "skipping gc - no rollup at initial seqno. concurrent GC?"
305                );
306                return Self::skip_gc(machine, gc_results, remove_finalized).await;
307            };
308
309            let Some(state) = machine
310                .applier
311                .state_versions
312                .fetch_rollup_at_key::<T>(&req.shard_id, initial_rollup)
313                .await
314            else {
315                debug!(
316                    ?initial_seqno,
317                    ?gc_rollups,
318                    "skipping gc - deleted rollup at initial seqno. concurrent GC?"
319                );
320                return Self::skip_gc(machine, gc_results, remove_finalized).await;
321            };
322
323            UntypedStateVersionsIter::new(
324                req.shard_id,
325                machine.applier.cfg.clone(),
326                Arc::clone(&machine.applier.metrics),
327                state,
328                diffs,
329            )
330            .check_ts_codec()
331            .expect("ts codec has not changed")
332        } else {
333            machine
334                .applier
335                .state_versions
336                .fetch_all_live_states(req.shard_id)
337                .await
338                .expect("state is initialized")
339                .check_ts_codec()
340                .expect("ts codec has not changed")
341        };
342
343        let initial_seqno = states.state().seqno;
344        report_step_timing(&machine.applier.metrics.gc.steps.fetch_seconds);
345
346        machine
347            .applier
348            .shard_metrics
349            .gc_live_diffs
350            .set(u64::cast_from(states.len()));
351
352        debug!(
353            "gc seqno_since: ({}) got {} versions from scan",
354            req.new_seqno_since,
355            states.len()
356        );
357
358        Self::incrementally_delete_and_truncate(
359            &mut states,
360            &gc_rollups,
361            machine,
362            &mut report_step_timing,
363            &mut gc_results,
364        )
365        .await;
366
367        // Now that the blobs are deleted / Consensus is truncated, remove
368        // the rollups from state. Doing this at the end ensures that our
369        // invariant is maintained that the current state contains a rollup
370        // to the earliest state in Consensus, and ensures that if GC crashes
371        // part-way through, we still have a reference to these rollups to
372        // resume their deletion.
373        //
374        // This does mean that if GC crashes part-way through we would
375        // repeat work when it resumes. However the redundant work should
376        // be minimal as Consensus is incrementally truncated, allowing
377        // the next run of GC to skip any work needed for rollups less
378        // than the last truncation.
379        //
380        // In short, while this step is not incremental, it does not need
381        // to be for GC to efficiently resume. And in fact, making it
382        // incremental could be quite expensive (e.g. more CaS operations).
383        let (removed_rollups, maintenance) = machine
384            .remove_rollups(rollups_to_remove_from_state, remove_finalized)
385            .await;
386        report_step_timing(&machine.applier.metrics.gc.steps.remove_rollups_from_state);
387        debug!("CaS removed rollups from state: {:?}", removed_rollups);
388        gc_results.rollups_removed_from_state = removed_rollups;
389
390        // Everything here and below is not strictly needed for GC to complete,
391        // but it's a good opportunity, while we have all live states in hand,
392        // to run some metrics and assertions.
393
394        // Apply all remaining live states to rollup some metrics, like how many
395        // parts are being held (in Blob) that are not part of the latest state.
396        let mut seqno_held_parts = 0;
397        while let Some(_) = states.next(|diff| match diff {
398            InspectDiff::FromInitial(_) => {}
399            InspectDiff::Diff(diff) => {
400                seqno_held_parts += diff.part_deletes().count();
401            }
402        }) {}
403
404        machine
405            .applier
406            .shard_metrics
407            .gc_seqno_held_parts
408            .set(u64::cast_from(seqno_held_parts));
409
410        // verify that the current state contains a rollup to the earliest state
411        // we fetched. this invariant isn't affected by the GC work we just
412        // performed, but it is a property of GC correctness overall / is a
413        // convenient place to run the assertion.
414        //
415        // The check is only meaningful when `states` was built from
416        // `fetch_all_live_states`, where iterating to the end leaves
417        // `states.state()` at the genuine current state. Under active GC we only
418        // fetch diffs through `seqno_since`, so `states.state()` is the
419        // reconstructed state at `seqno_since`, not the current state. A rollup's
420        // entry is inserted into the rollups map by the `add_rollup` transition
421        // at a seqno strictly greater than the seqno it rolls up; when
422        // `seqno_since` precedes that insertion the reconstructed state legitimately
423        // lacks the entry and this check would false-fire. The active-GC path
424        // already validates the same invariant against fresh data when it resolves
425        // the rollup for `initial_seqno` above.
426        if !use_active_gc {
427            let valid_pre_gc_state = states
428                .state()
429                .collections
430                .rollups
431                .contains_key(&initial_seqno);
432
433            // this should never be true in the steady-state, but may be true the
434            // first time GC runs after fixing any correctness bugs related to our
435            // state version invariants. we'll make it an error so we can track
436            // any violations in Sentry, but opt not to panic because the root
437            // cause of the violation cannot be from this GC run (in fact, this
438            // GC run, assuming it's correct, should have fixed the violation!)
439            soft_assert_or_log!(
440                valid_pre_gc_state,
441                "earliest state fetched during GC did not have corresponding rollup: rollups = {:?}, state seqno = {}",
442                states.state().collections.rollups,
443                initial_seqno
444            );
445        }
446
447        report_step_timing(
448            &machine
449                .applier
450                .metrics
451                .gc
452                .steps
453                .post_gc_calculations_seconds,
454        );
455
456        (maintenance, gc_results)
457    }
458
459    /// Finishes a GC pass that turned out to have no work to do, either because
460    /// another process already did it or because there was never any to begin with.
461    ///
462    /// This still calls `remove_rollups` to clear `active_gc` if it was set, so
463    /// the next GC isn't suppressed. A finalized shard leaves it set, which
464    /// `maybe_gc` ignores for tombstones.
465    async fn skip_gc(
466        machine: &Machine<K, V, T, D>,
467        gc_results: GcResults,
468        remove_finalized: bool,
469    ) -> (RoutineMaintenance, GcResults) {
470        let (_removed, maintenance) = machine.remove_rollups(&[], remove_finalized).await;
471        machine.applier.metrics.gc.noop.inc();
472        (maintenance, gc_results)
473    }
474
475    /// Physically deletes all blobs from Blob and live diffs from Consensus that
476    /// are safe to delete, given the `seqno_since`, ensuring that the earliest
477    /// live diff in Consensus has a rollup of seqno `<= seqno_since`.
478    ///
479    /// Internally, performs deletions for each rollup encountered, ensuring that
480    /// incremental progress is made even if the process is interrupted before
481    /// completing all gc work.
482    async fn incrementally_delete_and_truncate<F>(
483        states: &mut StateVersionsIter<T>,
484        gc_rollups: &GcRollups,
485        machine: &Machine<K, V, T, D>,
486        timer: &mut F,
487        gc_results: &mut GcResults,
488    ) where
489        F: FnMut(&Counter),
490    {
491        assert_eq!(states.state().shard_id, machine.shard_id());
492        let shard_id = states.state().shard_id;
493        let mut batch_parts_to_delete = PartDeletes::default();
494        let mut rollups_to_delete: BTreeSet<PartialRollupKey> = BTreeSet::new();
495
496        for truncate_lt in gc_rollups.truncate_seqnos() {
497            assert!(batch_parts_to_delete.is_empty());
498            assert!(rollups_to_delete.is_empty());
499
500            // our state is already past the truncation point. there's no work to do --
501            // some process already truncated this far
502            if states.state().seqno >= truncate_lt {
503                continue;
504            }
505
506            // By our invariant, `states` should always begin on a rollup.
507            assert!(
508                gc_rollups.contains_seqno(&states.state().seqno),
509                "must start with a present rollup before searching for blobs: rollups = {:#?}, state seqno = {}",
510                gc_rollups,
511                states.state().seqno
512            );
513
514            Self::find_removable_blobs(
515                states,
516                truncate_lt,
517                &machine.applier.metrics.gc.steps,
518                timer,
519                &mut batch_parts_to_delete,
520                &mut rollups_to_delete,
521            );
522
523            // After finding removable blobs, our state should be exactly `truncate_lt`,
524            // to ensure we've seen all blob deletions in the diffs needed to reach
525            // this seqno.
526            //
527            // That we can always reach `truncate_lt` given the live diffs we fetched
528            // earlier is a little subtle:
529            // * Our GC request was generated after `seqno_since` was written.
530            // * If our initial seqno on this loop was < `truncate_lt`, then our read
531            //   to `fetch_all_live_states` must have seen live diffs through at least
532            //   `seqno_since`, because the diffs were not yet truncated.
533            // * `seqno_since` >= `truncate_lt`, therefore we must have enough live
534            //   diffs to reach `truncate_lt`.
535            assert_eq!(states.state().seqno, truncate_lt);
536            // `truncate_lt` _is_ the seqno of a rollup, but let's very explicitly
537            // assert that we're about to truncate everything less than a rollup
538            // to maintain our invariant.
539            assert!(
540                gc_rollups.contains_seqno(&states.state().seqno),
541                "must start with a present rollup after searching for blobs: rollups = {:#?}, state seqno = {}",
542                gc_rollups,
543                states.state().seqno
544            );
545
546            // Extra paranoia: verify that none of the blobs we're about to delete
547            // are in our current state (we should only be truncating blobs from
548            // before this state!)
549            states.state().blobs().for_each(|blob| match blob {
550                HollowBlobRef::Batch(batch) => {
551                    for live_part in &batch.parts {
552                        assert!(!batch_parts_to_delete.contains(live_part));
553                    }
554                }
555                HollowBlobRef::Rollup(live_rollup) => {
556                    assert_eq!(rollups_to_delete.get(&live_rollup.key), None);
557                    // And double check that the rollups we're about to delete are
558                    // earlier than our truncation point:
559                    match BlobKey::parse_ids(&live_rollup.key.complete(&shard_id)) {
560                        Ok((_shard, PartialBlobKey::Rollup(rollup_seqno, _rollup))) => {
561                            assert!(rollup_seqno < truncate_lt);
562                        }
563                        _ => {
564                            panic!("invalid rollup during deletion: {:?}", live_rollup);
565                        }
566                    }
567                }
568            });
569
570            gc_results.truncated_consensus_to.push(truncate_lt);
571            gc_results.batch_parts_deleted_from_blob += batch_parts_to_delete.len();
572            gc_results.rollups_deleted_from_blob += rollups_to_delete.len();
573
574            Self::delete_and_truncate(
575                truncate_lt,
576                &mut batch_parts_to_delete,
577                &mut rollups_to_delete,
578                machine,
579                timer,
580            )
581            .await;
582        }
583    }
584
585    /// Iterates through `states`, accumulating all deleted blobs (both batch parts
586    /// and rollups) until reaching the seqno `truncate_lt`.
587    ///
588    /// * The initial seqno of `states` MUST be less than `truncate_lt`.
589    /// * The seqno of `states` after this fn will be exactly `truncate_lt`.
590    fn find_removable_blobs<F>(
591        states: &mut StateVersionsIter<T>,
592        truncate_lt: SeqNo,
593        metrics: &GcStepTimings,
594        timer: &mut F,
595        batch_parts_to_delete: &mut PartDeletes<T>,
596        rollups_to_delete: &mut BTreeSet<PartialRollupKey>,
597    ) where
598        F: FnMut(&Counter),
599    {
600        assert!(states.state().seqno < truncate_lt);
601        while let Some(state) = states.next(|diff| match diff {
602            InspectDiff::FromInitial(_) => {}
603            InspectDiff::Diff(diff) => {
604                diff.rollup_deletes().for_each(|rollup| {
605                    // we use BTreeSets for fast lookups elsewhere, but we should never
606                    // see repeat rollup insertions within a single GC run, otherwise we
607                    // have a logic error or our diffs are incorrect (!)
608                    assert!(rollups_to_delete.insert(rollup.key.to_owned()));
609                });
610                diff.part_deletes().for_each(|part| {
611                    assert!(batch_parts_to_delete.add(part));
612                });
613            }
614        }) {
615            if state.seqno == truncate_lt {
616                break;
617            }
618        }
619        timer(&metrics.find_deletable_blobs_seconds);
620    }
621
622    /// Deletes `batch_parts` and `rollups` from Blob.
623    /// Truncates Consensus to `truncate_lt`.
624    async fn delete_and_truncate<F>(
625        truncate_lt: SeqNo,
626        batch_parts: &mut PartDeletes<T>,
627        rollups: &mut BTreeSet<PartialRollupKey>,
628        machine: &Machine<K, V, T, D>,
629        timer: &mut F,
630    ) where
631        F: FnMut(&Counter),
632    {
633        let shard_id = machine.shard_id();
634        let concurrency_limit = GC_BLOB_DELETE_CONCURRENCY_LIMIT.get(&machine.applier.cfg);
635        let delete_semaphore = Semaphore::new(concurrency_limit);
636
637        let batch_parts = std::mem::take(batch_parts);
638        batch_parts
639            .delete(
640                machine.applier.state_versions.blob.borrow(),
641                shard_id,
642                concurrency_limit,
643                &*machine.applier.metrics,
644                &machine.applier.metrics.retries.external.batch_delete,
645            )
646            .instrument(debug_span!("batch::delete"))
647            .await;
648        timer(&machine.applier.metrics.gc.steps.delete_batch_part_seconds);
649
650        Self::delete_all(
651            machine.applier.state_versions.blob.borrow(),
652            rollups.iter().map(|k| k.complete(&shard_id)),
653            &machine.applier.metrics.retries.external.rollup_delete,
654            debug_span!("rollup::delete"),
655            &delete_semaphore,
656        )
657        .await;
658        rollups.clear();
659        timer(&machine.applier.metrics.gc.steps.delete_rollup_seconds);
660
661        machine
662            .applier
663            .state_versions
664            .truncate_diffs(&shard_id, truncate_lt)
665            .await;
666        timer(&machine.applier.metrics.gc.steps.truncate_diff_seconds);
667    }
668
669    // There's also a bulk delete API in s3 if the performance of this
670    // becomes an issue. Maybe make Blob::delete take a list of keys?
671    //
672    // https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html
673    async fn delete_all(
674        blob: &dyn Blob,
675        keys: impl Iterator<Item = BlobKey>,
676        metrics: &RetryMetrics,
677        span: Span,
678        semaphore: &Semaphore,
679    ) {
680        let futures = FuturesUnordered::new();
681        for key in keys {
682            futures.push(
683                retry_external(metrics, move || {
684                    let key = key.clone();
685                    async move {
686                        let _permit = semaphore
687                            .acquire()
688                            .await
689                            .expect("acquiring permit from open semaphore");
690                        blob.delete(&key).await.map(|_| ())
691                    }
692                })
693                .instrument(span.clone()),
694            )
695        }
696
697        futures.collect().await
698    }
699}
700
701#[derive(Debug, Default)]
702pub(crate) struct GcResults {
703    pub(crate) batch_parts_deleted_from_blob: usize,
704    pub(crate) rollups_deleted_from_blob: usize,
705    pub(crate) truncated_consensus_to: Vec<SeqNo>,
706    pub(crate) rollups_removed_from_state: Vec<SeqNo>,
707}
708
709#[derive(Debug)]
710struct GcRollups {
711    rollups_lte_seqno_since: Vec<(SeqNo, PartialRollupKey)>,
712    rollup_seqnos: HashSet<SeqNo>,
713}
714
715impl GcRollups {
716    fn new(rollups_lte_seqno_since: Vec<(SeqNo, PartialRollupKey)>, gc_req: &GcReq) -> Self {
717        assert!(
718            rollups_lte_seqno_since
719                .iter()
720                .all(|(seqno, _rollup)| *seqno <= gc_req.new_seqno_since)
721        );
722        let rollup_seqnos = rollups_lte_seqno_since.iter().map(|(x, _)| *x).collect();
723        Self {
724            rollups_lte_seqno_since,
725            rollup_seqnos,
726        }
727    }
728
729    /// Return the rollup key for the given seqno, if it exists.
730    fn get(&self, seqno: SeqNo) -> Option<&PartialRollupKey> {
731        let index = self
732            .rollups_lte_seqno_since
733            .binary_search_by_key(&seqno, |(k, _)| *k)
734            .ok()?;
735        Some(&self.rollups_lte_seqno_since[index].1)
736    }
737
738    fn contains_seqno(&self, seqno: &SeqNo) -> bool {
739        self.rollup_seqnos.contains(seqno)
740    }
741
742    /// Returns the seqnos we can safely truncate state to when performing
743    /// incremental GC (all rollups with seqnos <= seqno_since).
744    fn truncate_seqnos(&self) -> impl Iterator<Item = SeqNo> + '_ {
745        self.rollups_lte_seqno_since
746            .iter()
747            .map(|(seqno, _rollup)| *seqno)
748    }
749
750    /// Returns the rollups we can safely remove from state (all rollups
751    /// `<` than the latest rollup `<=` seqno_since).
752    ///
753    /// See the full explanation in [crate::internal::state_versions::StateVersions]
754    /// for how this is derived.
755    fn rollups_to_remove_from_state(&self) -> &[(SeqNo, PartialRollupKey)] {
756        match self.rollups_lte_seqno_since.split_last() {
757            None => &[],
758            Some((_rollup_to_keep, rollups_to_remove_from_state)) => rollups_to_remove_from_state,
759        }
760    }
761}