Skip to main content

cargo/workspace/
global_cache_tracker.rs

1//! Support for tracking the last time files were used to assist with cleaning
2//! up those files if they haven't been used in a while.
3//!
4//! Tracking of cache files is stored in a sqlite database which contains a
5//! timestamp of the last time the file was used, as well as the size of the
6//! file.
7//!
8//! While cargo is running, when it detects a use of a cache file, it adds a
9//! timestamp to [`DeferredGlobalLastUse`]. This batches up a set of changes
10//! that are then flushed to the database all at once (via
11//! [`DeferredGlobalLastUse::save`]). Ideally saving would only be done once
12//! for performance reasons, but that is not really possible due to the way
13//! cargo works, since there are different ways cargo can be used (like `cargo
14//! generate-lockfile`, `cargo fetch`, and `cargo build` are all very
15//! different ways the code is used).
16//!
17//! All of the database interaction is done through the [`GlobalCacheTracker`]
18//! type.
19//!
20//! There is a single global [`GlobalCacheTracker`] and
21//! [`DeferredGlobalLastUse`] stored in [`GlobalContext`].
22//!
23//! The high-level interface for performing garbage collection is defined in
24//! the [`crate::workspace::gc`] module. The functions there are responsible for
25//! interacting with the [`GlobalCacheTracker`] to handle cleaning of global
26//! cache data.
27//!
28//! ## Automatic gc
29//!
30//! Some commands (primarily the build commands) will trigger an automatic
31//! deletion of files that haven't been used in a while. The high-level
32//! interface for this is the [`crate::workspace::gc::auto_gc`] function.
33//!
34//! The [`GlobalCacheTracker`] database tracks the last time an automatic gc
35//! was performed so that it is only done once per day for performance
36//! reasons.
37//!
38//! ## Manual gc
39//!
40//! The user can perform a manual garbage collection with the `cargo clean`
41//! command. That command has a variety of options to specify what to delete.
42//! Manual gc supports deleting based on age or size or both. From a
43//! high-level, this is done by the [`crate::workspace::gc::Gc::gc`] method, which
44//! calls into [`GlobalCacheTracker`] to handle all the cleaning.
45//!
46//! ## Locking
47//!
48//! Usage of the database requires that the package cache is locked to prevent
49//! concurrent access. Although sqlite has built-in locking support, we want
50//! to use cargo's locking so that the "Blocking" message gets displayed, and
51//! so that locks can block indefinitely for long-running build commands.
52//! [`rusqlite`] has a default timeout of 5 seconds, though that is
53//! configurable.
54//!
55//! When garbage collection is being performed, the package cache lock must be
56//! in [`CacheLockMode::MutateExclusive`] to ensure no other cargo process is
57//! running. See [`crate::util::cache_lock`] for more detail on locking.
58//!
59//! When performing automatic gc, [`crate::workspace::gc::auto_gc`] will skip the
60//! GC if the package cache lock is already held by anything else. Automatic
61//! GC is intended to be opportunistic, and should impose as little disruption
62//! to the user as possible.
63//!
64//! ## Compatibility
65//!
66//! The database must retain both forwards and backwards compatibility between
67//! different versions of cargo. For the most part, this shouldn't be too
68//! difficult to maintain. Generally sqlite doesn't change on-disk formats
69//! between versions (the introduction of WAL is one of the few examples where
70//! version 3 had a format change, but we wouldn't use it anyway since it has
71//! shared-memory requirements cargo can't depend on due to things like
72//! network mounts).
73//!
74//! Schema changes must be managed through [`migrations`] by adding new
75//! entries that make a change to the database. Changes must not break older
76//! versions of cargo. Generally, adding columns should be fine (either with a
77//! default value, or NULL). Adding tables should also be fine. Just don't do
78//! destructive things like removing a column, or changing the semantics of an
79//! existing column.
80//!
81//! Since users may run older versions of cargo that do not do cache tracking,
82//! the [`GlobalCacheTracker::sync_db_with_files`] method helps dealing with
83//! keeping the database in sync in the presence of older versions of cargo
84//! touching the cache directories.
85//!
86//! ## Performance
87//!
88//! A lot of focus on the design of this system is to minimize the performance
89//! impact. Every build command needs to save updates which we try to avoid
90//! having a noticeable impact on build times. Systems like Windows,
91//! particularly with a magnetic hard disk, can experience a fairly large
92//! impact of cargo's overhead. Cargo's benchsuite has some benchmarks to help
93//! compare different environments, or changes to the code here. Please try to
94//! keep performance in mind if making any major changes.
95//!
96//! Performance of `cargo clean` is not quite as important since it is not
97//! expected to be run often. However, it is still courteous to the user to
98//! try to not impact it too much. One part that has a performance concern is
99//! that the clean command will synchronize the database with whatever is on
100//! disk if needed (in case files were added by older versions of cargo that
101//! don't do cache tracking, or if the user manually deleted some files). This
102//! can potentially be very slow, especially if the two are very out of sync.
103//!
104//! ## Filesystems
105//!
106//! Everything here is sensitive to the kind of filesystem it is running on.
107//! People tend to run cargo in all sorts of strange environments that have
108//! limited capabilities, or on things like read-only mounts. The code here
109//! needs to gracefully handle as many situations as possible.
110//!
111//! See also the information in the [Performance](#performance) and
112//! [Locking](#locking) sections when considering different filesystems and
113//! their impact on performance and locking.
114//!
115//! There are checks for read-only filesystems, which is generally ignored.
116
117use crate::ops::CleanContext;
118use crate::util::cache_lock::CacheLockMode;
119use crate::util::data_structures::HashMap;
120use crate::util::interning::InternedString;
121use crate::util::sqlite::{self, Migration, basic_migration};
122use crate::util::{Filesystem, Progress, ProgressStyle};
123use crate::workspace::gc::GcOpts;
124use crate::{CargoResult, GlobalContext};
125use anyhow::{Context as _, bail};
126use cargo_util::paths;
127use cargo_util_terminal::Verbosity;
128use rusqlite::{Connection, ErrorCode, params};
129use std::collections::hash_map;
130use std::path::{Path, PathBuf};
131use std::time::{Duration, SystemTime};
132use tracing::{debug, trace};
133
134/// The filename of the database.
135const GLOBAL_CACHE_FILENAME: &str = ".global-cache";
136
137const REGISTRY_INDEX_TABLE: &str = "registry_index";
138const REGISTRY_CRATE_TABLE: &str = "registry_crate";
139const REGISTRY_SRC_TABLE: &str = "registry_src";
140const GIT_DB_TABLE: &str = "git_db";
141const GIT_CO_TABLE: &str = "git_checkout";
142
143/// How often timestamps will be updated.
144///
145/// As an optimization timestamps are not updated unless they are older than
146/// the given number of seconds. This helps reduce the amount of disk I/O when
147/// running cargo multiple times within a short window.
148const UPDATE_RESOLUTION: u64 = 60 * 5;
149
150/// Type for timestamps as stored in the database.
151///
152/// These are seconds since the Unix epoch.
153type Timestamp = u64;
154
155/// The key for a registry index entry stored in the database.
156#[derive(Clone, Debug, Hash, Eq, PartialEq)]
157pub struct RegistryIndex {
158    /// A unique name of the registry source.
159    pub encoded_registry_name: InternedString,
160}
161
162/// The key for a registry `.crate` entry stored in the database.
163#[derive(Clone, Debug, Hash, Eq, PartialEq)]
164pub struct RegistryCrate {
165    /// A unique name of the registry source.
166    pub encoded_registry_name: InternedString,
167    /// The filename of the compressed crate, like `foo-1.2.3.crate`.
168    pub crate_filename: InternedString,
169    /// The size of the `.crate` file.
170    pub size: u64,
171}
172
173/// The key for a registry src directory entry stored in the database.
174#[derive(Clone, Debug, Hash, Eq, PartialEq)]
175pub struct RegistrySrc {
176    /// A unique name of the registry source.
177    pub encoded_registry_name: InternedString,
178    /// The directory name of the extracted source, like `foo-1.2.3`.
179    pub package_dir: InternedString,
180    /// Total size of the src directory in bytes.
181    ///
182    /// This can be None when the size is unknown. For example, when the src
183    /// directory already exists on disk, and we just want to update the
184    /// last-use timestamp. We don't want to take the expense of computing disk
185    /// usage unless necessary. [`GlobalCacheTracker::populate_untracked`]
186    /// will handle any actual NULL values in the database, which can happen
187    /// when the src directory is created by an older version of cargo that
188    /// did not track sizes.
189    pub size: Option<u64>,
190}
191
192/// The key for a git db entry stored in the database.
193#[derive(Clone, Debug, Hash, Eq, PartialEq)]
194pub struct GitDb {
195    /// A unique name of the git database.
196    pub encoded_git_name: InternedString,
197}
198
199/// The key for a git checkout entry stored in the database.
200#[derive(Clone, Debug, Hash, Eq, PartialEq)]
201pub struct GitCheckout {
202    /// A unique name of the git database.
203    pub encoded_git_name: InternedString,
204    /// A unique name of the checkout without the database.
205    pub short_name: InternedString,
206    /// Total size of the checkout directory.
207    ///
208    /// This can be None when the size is unknown. See [`RegistrySrc::size`]
209    /// for an explanation.
210    pub size: Option<u64>,
211}
212
213/// Filesystem paths in the global cache.
214///
215/// Accessing these assumes a lock has already been acquired.
216struct BasePaths {
217    /// Root path to the index caches.
218    index: PathBuf,
219    /// Root path to the git DBs.
220    git_db: PathBuf,
221    /// Root path to the git checkouts.
222    git_co: PathBuf,
223    /// Root path to the `.crate` files.
224    crate_dir: PathBuf,
225    /// Root path to the `src` directories.
226    src: PathBuf,
227}
228
229/// Migrations which initialize the database, and can be used to evolve it over time.
230///
231/// See [`Migration`] for more detail.
232///
233/// **Be sure to not change the order or entries here!**
234fn migrations() -> Vec<Migration> {
235    vec![
236        // registry_index tracks the overall usage of an index cache, and tracks a
237        // numeric ID to refer to that index that is used in other tables.
238        basic_migration(
239            "CREATE TABLE registry_index (
240                id INTEGER PRIMARY KEY AUTOINCREMENT,
241                name TEXT UNIQUE NOT NULL,
242                timestamp INTEGER NOT NULL
243            )",
244        ),
245        // .crate files
246        basic_migration(
247            "CREATE TABLE registry_crate (
248                registry_id INTEGER NOT NULL,
249                name TEXT NOT NULL,
250                size INTEGER NOT NULL,
251                timestamp INTEGER NOT NULL,
252                PRIMARY KEY (registry_id, name),
253                FOREIGN KEY (registry_id) REFERENCES registry_index (id) ON DELETE CASCADE
254             )",
255        ),
256        // Extracted src directories
257        //
258        // Note that `size` can be NULL. This will happen when marking a src
259        // directory as used that was created by an older version of cargo
260        // that didn't do size tracking.
261        basic_migration(
262            "CREATE TABLE registry_src (
263                registry_id INTEGER NOT NULL,
264                name TEXT NOT NULL,
265                size INTEGER,
266                timestamp INTEGER NOT NULL,
267                PRIMARY KEY (registry_id, name),
268                FOREIGN KEY (registry_id) REFERENCES registry_index (id) ON DELETE CASCADE
269             )",
270        ),
271        // Git db directories
272        basic_migration(
273            "CREATE TABLE git_db (
274                id INTEGER PRIMARY KEY AUTOINCREMENT,
275                name TEXT UNIQUE NOT NULL,
276                timestamp INTEGER NOT NULL
277             )",
278        ),
279        // Git checkout directories
280        basic_migration(
281            "CREATE TABLE git_checkout (
282                git_id INTEGER NOT NULL,
283                name TEXT UNIQUE NOT NULL,
284                size INTEGER,
285                timestamp INTEGER NOT NULL,
286                PRIMARY KEY (git_id, name),
287                FOREIGN KEY (git_id) REFERENCES git_db (id) ON DELETE CASCADE
288             )",
289        ),
290        // This is a general-purpose single-row table that can store arbitrary
291        // data. Feel free to add columns (with ALTER TABLE) if necessary.
292        basic_migration(
293            "CREATE TABLE global_data (
294                last_auto_gc INTEGER NOT NULL
295            )",
296        ),
297        // last_auto_gc tracks the last time auto-gc was run (so that it only
298        // runs roughly once a day for performance reasons). Prime it with the
299        // current time to establish a baseline.
300        Box::new(|conn| {
301            conn.execute(
302                "INSERT INTO global_data (last_auto_gc) VALUES (?1)",
303                [now()],
304            )?;
305            Ok(())
306        }),
307    ]
308}
309
310/// Type for SQL columns that refer to the primary key of their parent table.
311///
312/// For example, `registry_crate.registry_id` refers to its parent `registry_index.id`.
313#[derive(Copy, Clone, Debug, PartialEq)]
314struct ParentId(i64);
315
316impl rusqlite::types::FromSql for ParentId {
317    fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
318        let i = i64::column_result(value)?;
319        Ok(ParentId(i))
320    }
321}
322
323impl rusqlite::types::ToSql for ParentId {
324    fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
325        Ok(rusqlite::types::ToSqlOutput::from(self.0))
326    }
327}
328
329/// Tracking for the global shared cache (registry files, etc.).
330///
331/// This is the interface to the global cache database, used for tracking and
332/// cleaning. See the [`crate::workspace::global_cache_tracker`] module docs for
333/// details.
334#[derive(Debug)]
335pub struct GlobalCacheTracker {
336    /// Connection to the SQLite database.
337    conn: Connection,
338    /// This is an optimization used to make sure cargo only checks if gc
339    /// needs to run once per session. This starts as `false`, and then the
340    /// first time it checks if automatic gc needs to run, it will be set to
341    /// `true`.
342    auto_gc_checked_this_session: bool,
343}
344
345impl GlobalCacheTracker {
346    /// Creates a new [`GlobalCacheTracker`].
347    ///
348    /// The caller is responsible for locking the package cache with
349    /// [`CacheLockMode::DownloadExclusive`] before calling this.
350    pub fn new(gctx: &GlobalContext) -> CargoResult<GlobalCacheTracker> {
351        let db_path = Self::db_path(gctx);
352        // A package cache lock is required to ensure only one cargo is
353        // accessing at the same time. If there is concurrent access, we
354        // want to rely on cargo's own "Blocking" system (which can
355        // provide user feedback) rather than blocking inside sqlite
356        // (which by default has a short timeout).
357        let db_path = gctx.assert_package_cache_locked(CacheLockMode::DownloadExclusive, &db_path);
358        let mut conn = Connection::open(db_path)?;
359        conn.pragma_update(None, "foreign_keys", true)?;
360        sqlite::migrate(&mut conn, &migrations())?;
361        Ok(GlobalCacheTracker {
362            conn,
363            auto_gc_checked_this_session: false,
364        })
365    }
366
367    /// The path to the database.
368    pub fn db_path(gctx: &GlobalContext) -> Filesystem {
369        gctx.home().join(GLOBAL_CACHE_FILENAME)
370    }
371
372    /// Given an encoded registry name, returns its ID.
373    ///
374    /// Returns None if the given name isn't in the database.
375    fn id_from_name(
376        conn: &Connection,
377        table_name: &str,
378        encoded_name: &str,
379    ) -> CargoResult<Option<ParentId>> {
380        let mut stmt =
381            conn.prepare_cached(&format!("SELECT id FROM {table_name} WHERE name = ?"))?;
382        match stmt.query_row([encoded_name], |row| row.get(0)) {
383            Ok(id) => Ok(Some(id)),
384            Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
385            Err(e) => Err(e.into()),
386        }
387    }
388
389    /// Returns a map of ID to path for the given ids in the given table.
390    ///
391    /// For example, given `registry_index` IDs, it returns filenames of the
392    /// form "index.crates.io-6f17d22bba15001f".
393    fn get_id_map(
394        conn: &Connection,
395        table_name: &str,
396        ids: &[i64],
397    ) -> CargoResult<HashMap<i64, PathBuf>> {
398        let mut stmt =
399            conn.prepare_cached(&format!("SELECT name FROM {table_name} WHERE id = ?1"))?;
400        ids.iter()
401            .map(|id| {
402                let name = stmt.query_row(params![id], |row| {
403                    Ok(PathBuf::from(row.get::<_, String>(0)?))
404                })?;
405                Ok((*id, name))
406            })
407            .collect()
408    }
409
410    /// Returns all index cache timestamps.
411    pub fn registry_index_all(&self) -> CargoResult<Vec<(RegistryIndex, Timestamp)>> {
412        let mut stmt = self
413            .conn
414            .prepare_cached("SELECT name, timestamp FROM registry_index")?;
415        let rows = stmt
416            .query_map([], |row| {
417                let encoded_registry_name = row.get_unwrap(0);
418                let timestamp = row.get_unwrap(1);
419                let kind = RegistryIndex {
420                    encoded_registry_name,
421                };
422                Ok((kind, timestamp))
423            })?
424            .collect::<Result<Vec<_>, _>>()?;
425        Ok(rows)
426    }
427
428    /// Returns all registry crate cache timestamps.
429    pub fn registry_crate_all(&self) -> CargoResult<Vec<(RegistryCrate, Timestamp)>> {
430        let mut stmt = self.conn.prepare_cached(
431            "SELECT registry_index.name, registry_crate.name, registry_crate.size, registry_crate.timestamp
432             FROM registry_index, registry_crate
433             WHERE registry_crate.registry_id = registry_index.id",
434        )?;
435        let rows = stmt
436            .query_map([], |row| {
437                let encoded_registry_name = row.get_unwrap(0);
438                let crate_filename = row.get_unwrap(1);
439                let size = row.get_unwrap(2);
440                let timestamp = row.get_unwrap(3);
441                let kind = RegistryCrate {
442                    encoded_registry_name,
443                    crate_filename,
444                    size,
445                };
446                Ok((kind, timestamp))
447            })?
448            .collect::<Result<Vec<_>, _>>()?;
449        Ok(rows)
450    }
451
452    /// Returns all registry source cache timestamps.
453    pub fn registry_src_all(&self) -> CargoResult<Vec<(RegistrySrc, Timestamp)>> {
454        let mut stmt = self.conn.prepare_cached(
455            "SELECT registry_index.name, registry_src.name, registry_src.size, registry_src.timestamp
456             FROM registry_index, registry_src
457             WHERE registry_src.registry_id = registry_index.id",
458        )?;
459        let rows = stmt
460            .query_map([], |row| {
461                let encoded_registry_name = row.get_unwrap(0);
462                let package_dir = row.get_unwrap(1);
463                let size = row.get_unwrap(2);
464                let timestamp = row.get_unwrap(3);
465                let kind = RegistrySrc {
466                    encoded_registry_name,
467                    package_dir,
468                    size,
469                };
470                Ok((kind, timestamp))
471            })?
472            .collect::<Result<Vec<_>, _>>()?;
473        Ok(rows)
474    }
475
476    /// Returns all git db timestamps.
477    pub fn git_db_all(&self) -> CargoResult<Vec<(GitDb, Timestamp)>> {
478        let mut stmt = self
479            .conn
480            .prepare_cached("SELECT name, timestamp FROM git_db")?;
481        let rows = stmt
482            .query_map([], |row| {
483                let encoded_git_name = row.get_unwrap(0);
484                let timestamp = row.get_unwrap(1);
485                let kind = GitDb { encoded_git_name };
486                Ok((kind, timestamp))
487            })?
488            .collect::<Result<Vec<_>, _>>()?;
489        Ok(rows)
490    }
491
492    /// Returns all git checkout timestamps.
493    pub fn git_checkout_all(&self) -> CargoResult<Vec<(GitCheckout, Timestamp)>> {
494        let mut stmt = self.conn.prepare_cached(
495            "SELECT git_db.name, git_checkout.name, git_checkout.size, git_checkout.timestamp
496             FROM git_db, git_checkout
497             WHERE git_checkout.git_id = git_db.id",
498        )?;
499        let rows = stmt
500            .query_map([], |row| {
501                let encoded_git_name = row.get_unwrap(0);
502                let short_name = row.get_unwrap(1);
503                let size = row.get_unwrap(2);
504                let timestamp = row.get_unwrap(3);
505                let kind = GitCheckout {
506                    encoded_git_name,
507                    short_name,
508                    size,
509                };
510                Ok((kind, timestamp))
511            })?
512            .collect::<Result<Vec<_>, _>>()?;
513        Ok(rows)
514    }
515
516    /// Returns whether or not an auto GC should be performed, compared to the
517    /// last time it was recorded in the database.
518    pub fn should_run_auto_gc(&mut self, frequency: Duration) -> CargoResult<bool> {
519        trace!(target: "gc", "should_run_auto_gc");
520        if self.auto_gc_checked_this_session {
521            return Ok(false);
522        }
523        let last_auto_gc: Timestamp =
524            self.conn
525                .query_row("SELECT last_auto_gc FROM global_data", [], |row| row.get(0))?;
526        let should_run = last_auto_gc + frequency.as_secs() < now();
527        trace!(target: "gc",
528            "last auto gc was {}, {}",
529            last_auto_gc,
530            if should_run { "running" } else { "skipping" }
531        );
532        self.auto_gc_checked_this_session = true;
533        Ok(should_run)
534    }
535
536    /// Writes to the database to indicate that an automatic GC has just been
537    /// completed.
538    pub fn set_last_auto_gc(&self) -> CargoResult<()> {
539        self.conn
540            .execute("UPDATE global_data SET last_auto_gc = ?1", [now()])?;
541        Ok(())
542    }
543
544    /// Deletes files from the global cache based on the given options.
545    pub fn clean(&mut self, clean_ctx: &mut CleanContext<'_>, gc_opts: &GcOpts) -> CargoResult<()> {
546        self.clean_inner(clean_ctx, gc_opts)
547            .context("failed to clean entries from the global cache")
548    }
549
550    #[tracing::instrument(skip_all)]
551    fn clean_inner(
552        &mut self,
553        clean_ctx: &mut CleanContext<'_>,
554        gc_opts: &GcOpts,
555    ) -> CargoResult<()> {
556        let gctx = clean_ctx.gctx;
557        let base = BasePaths {
558            index: gctx.registry_index_path().into_path_unlocked(),
559            git_db: gctx.git_db_path().into_path_unlocked(),
560            git_co: gctx.git_checkouts_path().into_path_unlocked(),
561            crate_dir: gctx.registry_cache_path().into_path_unlocked(),
562            src: gctx.registry_source_path().into_path_unlocked(),
563        };
564        let now = now();
565        trace!(target: "gc", "cleaning {gc_opts:?}");
566        let tx = self.conn.transaction()?;
567        let mut delete_paths = Vec::new();
568        // This can be an expensive operation, so only perform it if necessary.
569        if gc_opts.is_download_cache_opt_set() {
570            // TODO: Investigate how slow this might be.
571            Self::sync_db_with_files(
572                &tx,
573                now,
574                gctx,
575                &base,
576                gc_opts.is_download_cache_size_set(),
577                &mut delete_paths,
578            )
579            .context("failed to sync tracking database")?
580        }
581        if let Some(max_age) = gc_opts.max_index_age {
582            let max_age = now - max_age.as_secs();
583            Self::get_registry_index_to_clean(&tx, max_age, &base, &mut delete_paths)?;
584        }
585        if let Some(max_age) = gc_opts.max_src_age {
586            let max_age = now - max_age.as_secs();
587            Self::get_registry_items_to_clean_age(
588                &tx,
589                max_age,
590                REGISTRY_SRC_TABLE,
591                &base.src,
592                &mut delete_paths,
593            )?;
594        }
595        if let Some(max_age) = gc_opts.max_crate_age {
596            let max_age = now - max_age.as_secs();
597            Self::get_registry_items_to_clean_age(
598                &tx,
599                max_age,
600                REGISTRY_CRATE_TABLE,
601                &base.crate_dir,
602                &mut delete_paths,
603            )?;
604        }
605        if let Some(max_age) = gc_opts.max_git_db_age {
606            let max_age = now - max_age.as_secs();
607            Self::get_git_db_items_to_clean(&tx, max_age, &base, &mut delete_paths)?;
608        }
609        if let Some(max_age) = gc_opts.max_git_co_age {
610            let max_age = now - max_age.as_secs();
611            Self::get_git_co_items_to_clean(&tx, max_age, &base.git_co, &mut delete_paths)?;
612        }
613        // Size collection must happen after date collection so that dates
614        // have precedence, since size constraints are a more blunt
615        // instrument.
616        //
617        // These are also complicated by the `--max-download-size` option
618        // overlapping with `--max-crate-size` and `--max-src-size`, which
619        // requires some coordination between those options which isn't
620        // necessary with the age-based options. An item's age is either older
621        // or it isn't, but contrast that with size which is based on the sum
622        // of all tracked items. Also, `--max-download-size` is summed against
623        // both the crate and src tracking, which requires combining them to
624        // compute the size, and then separating them to calculate the correct
625        // paths.
626        if let Some(max_size) = gc_opts.max_crate_size {
627            Self::get_registry_items_to_clean_size(
628                &tx,
629                max_size,
630                REGISTRY_CRATE_TABLE,
631                &base.crate_dir,
632                &mut delete_paths,
633            )?;
634        }
635        if let Some(max_size) = gc_opts.max_src_size {
636            Self::get_registry_items_to_clean_size(
637                &tx,
638                max_size,
639                REGISTRY_SRC_TABLE,
640                &base.src,
641                &mut delete_paths,
642            )?;
643        }
644        if let Some(max_size) = gc_opts.max_git_size {
645            Self::get_git_items_to_clean_size(&tx, max_size, &base, &mut delete_paths)?;
646        }
647        if let Some(max_size) = gc_opts.max_download_size {
648            Self::get_registry_items_to_clean_size_both(&tx, max_size, &base, &mut delete_paths)?;
649        }
650
651        clean_ctx.remove_paths(&delete_paths)?;
652
653        if clean_ctx.dry_run {
654            tx.rollback()?;
655        } else {
656            tx.commit()?;
657        }
658        Ok(())
659    }
660
661    /// Returns a list of directory entries in the given path that are
662    /// themselves directories.
663    fn list_dir_names(path: &Path) -> CargoResult<Vec<String>> {
664        Self::read_dir_with_filter(path, &|entry| {
665            entry.file_type().map_or(false, |ty| ty.is_dir())
666        })
667    }
668
669    /// Returns a list of names in a directory, filtered by the given callback.
670    fn read_dir_with_filter(
671        path: &Path,
672        filter: &dyn Fn(&std::fs::DirEntry) -> bool,
673    ) -> CargoResult<Vec<String>> {
674        let entries = match path.read_dir() {
675            Ok(e) => e,
676            Err(e) => {
677                if e.kind() == std::io::ErrorKind::NotFound {
678                    return Ok(Vec::new());
679                } else {
680                    return Err(
681                        anyhow::Error::new(e).context(format!("failed to read path `{path:?}`"))
682                    );
683                }
684            }
685        };
686        let names = entries
687            .filter_map(|entry| entry.ok())
688            .filter(|entry| filter(entry))
689            .filter_map(|entry| entry.file_name().into_string().ok())
690            .collect();
691        Ok(names)
692    }
693
694    /// Synchronizes the database to match the files on disk.
695    ///
696    /// This performs the following cleanups:
697    ///
698    /// 1. Remove entries from the database that are missing on disk.
699    /// 2. Adds missing entries to the database that are on disk (such as when
700    ///    files are added by older versions of cargo).
701    /// 3. Fills in the `size` column where it is NULL (such as when something
702    ///    is added to disk by an older version of cargo, and one of the mark
703    ///    functions marked it without knowing the size).
704    ///
705    ///    Size computations are only done if `sync_size` is set since it can
706    ///    be a very expensive operation. This should only be set if the user
707    ///    requested to clean based on the cache size.
708    /// 4. Checks for orphaned files. For example, if there are `.crate` files
709    ///    associated with an index that does not exist.
710    ///
711    ///    These orphaned files will be added to `delete_paths` so that the
712    ///    caller can delete them.
713    #[tracing::instrument(skip(conn, gctx, base, delete_paths))]
714    fn sync_db_with_files(
715        conn: &Connection,
716        now: Timestamp,
717        gctx: &GlobalContext,
718        base: &BasePaths,
719        sync_size: bool,
720        delete_paths: &mut Vec<PathBuf>,
721    ) -> CargoResult<()> {
722        debug!(target: "gc", "starting db sync");
723        // For registry_index and git_db, add anything that is missing in the db.
724        Self::update_parent_for_missing_from_db(conn, now, REGISTRY_INDEX_TABLE, &base.index)?;
725        Self::update_parent_for_missing_from_db(conn, now, GIT_DB_TABLE, &base.git_db)?;
726
727        // For registry_crate, registry_src, and git_checkout, remove anything
728        // from the db that isn't on disk.
729        Self::update_db_for_removed(
730            conn,
731            REGISTRY_INDEX_TABLE,
732            "registry_id",
733            REGISTRY_CRATE_TABLE,
734            &base.crate_dir,
735        )?;
736        Self::update_db_for_removed(
737            conn,
738            REGISTRY_INDEX_TABLE,
739            "registry_id",
740            REGISTRY_SRC_TABLE,
741            &base.src,
742        )?;
743        Self::update_db_for_removed(conn, GIT_DB_TABLE, "git_id", GIT_CO_TABLE, &base.git_co)?;
744
745        // For registry_index and git_db, remove anything from the db that
746        // isn't on disk.
747        //
748        // This also collects paths for any child files that don't have their
749        // respective parent on disk.
750        Self::update_db_parent_for_removed_from_disk(
751            conn,
752            REGISTRY_INDEX_TABLE,
753            &base.index,
754            &[&base.crate_dir, &base.src],
755            delete_paths,
756        )?;
757        Self::update_db_parent_for_removed_from_disk(
758            conn,
759            GIT_DB_TABLE,
760            &base.git_db,
761            &[&base.git_co],
762            delete_paths,
763        )?;
764
765        // For registry_crate, registry_src, and git_checkout, add anything
766        // that is missing in the db.
767        Self::populate_untracked_crate(conn, now, &base.crate_dir)?;
768        Self::populate_untracked(
769            conn,
770            now,
771            gctx,
772            REGISTRY_INDEX_TABLE,
773            "registry_id",
774            REGISTRY_SRC_TABLE,
775            &base.src,
776            sync_size,
777        )?;
778        Self::populate_untracked(
779            conn,
780            now,
781            gctx,
782            GIT_DB_TABLE,
783            "git_id",
784            GIT_CO_TABLE,
785            &base.git_co,
786            sync_size,
787        )?;
788
789        // Update any NULL sizes if needed.
790        if sync_size {
791            Self::update_null_sizes(
792                conn,
793                gctx,
794                REGISTRY_INDEX_TABLE,
795                "registry_id",
796                REGISTRY_SRC_TABLE,
797                &base.src,
798            )?;
799            Self::update_null_sizes(
800                conn,
801                gctx,
802                GIT_DB_TABLE,
803                "git_id",
804                GIT_CO_TABLE,
805                &base.git_co,
806            )?;
807        }
808        Ok(())
809    }
810
811    /// For parent tables, add any entries that are on disk but aren't tracked in the db.
812    #[tracing::instrument(skip(conn, now, base_path))]
813    fn update_parent_for_missing_from_db(
814        conn: &Connection,
815        now: Timestamp,
816        parent_table_name: &str,
817        base_path: &Path,
818    ) -> CargoResult<()> {
819        trace!(target: "gc", "checking for untracked parent to add to {parent_table_name}");
820        let names = Self::list_dir_names(base_path)?;
821
822        let mut stmt = conn.prepare_cached(&format!(
823            "INSERT INTO {parent_table_name} (name, timestamp)
824                VALUES (?1, ?2)
825                ON CONFLICT DO NOTHING",
826        ))?;
827        for name in names {
828            stmt.execute(params![name, now])?;
829        }
830        Ok(())
831    }
832
833    /// Removes database entries for any files that are not on disk for the child tables.
834    ///
835    /// This could happen for example if the user manually deleted the file or
836    /// any such scenario where the filesystem and db are out of sync.
837    #[tracing::instrument(skip(conn, base_path))]
838    fn update_db_for_removed(
839        conn: &Connection,
840        parent_table_name: &str,
841        id_column_name: &str,
842        table_name: &str,
843        base_path: &Path,
844    ) -> CargoResult<()> {
845        trace!(target: "gc", "checking for db entries to remove from {table_name}");
846        let mut select_stmt = conn.prepare_cached(&format!(
847            "SELECT {table_name}.rowid, {parent_table_name}.name, {table_name}.name
848             FROM {parent_table_name}, {table_name}
849             WHERE {table_name}.{id_column_name} = {parent_table_name}.id",
850        ))?;
851        let mut delete_stmt =
852            conn.prepare_cached(&format!("DELETE FROM {table_name} WHERE rowid = ?1"))?;
853        let mut rows = select_stmt.query([])?;
854        while let Some(row) = rows.next()? {
855            let rowid: i64 = row.get_unwrap(0);
856            let id_name: String = row.get_unwrap(1);
857            let name: String = row.get_unwrap(2);
858            if !base_path.join(id_name).join(name).exists() {
859                delete_stmt.execute([rowid])?;
860            }
861        }
862        Ok(())
863    }
864
865    /// Removes database entries for any files that are not on disk for the parent tables.
866    #[tracing::instrument(skip(conn, base_path, child_base_paths, delete_paths))]
867    fn update_db_parent_for_removed_from_disk(
868        conn: &Connection,
869        parent_table_name: &str,
870        base_path: &Path,
871        child_base_paths: &[&Path],
872        delete_paths: &mut Vec<PathBuf>,
873    ) -> CargoResult<()> {
874        trace!(target: "gc", "checking for db entries to remove from {parent_table_name}");
875        let mut select_stmt =
876            conn.prepare_cached(&format!("SELECT rowid, name FROM {parent_table_name}"))?;
877        let mut delete_stmt =
878            conn.prepare_cached(&format!("DELETE FROM {parent_table_name} WHERE rowid = ?1"))?;
879        let mut rows = select_stmt.query([])?;
880        while let Some(row) = rows.next()? {
881            let rowid: i64 = row.get_unwrap(0);
882            let id_name: String = row.get_unwrap(1);
883            if !base_path.join(&id_name).exists() {
884                delete_stmt.execute([rowid])?;
885                // Make sure any child data is also cleaned up.
886                for child_base in child_base_paths {
887                    let child_path = child_base.join(&id_name);
888                    if child_path.exists() {
889                        debug!(target: "gc", "removing orphaned path {child_path:?}");
890                        delete_paths.push(child_path);
891                    }
892                }
893            }
894        }
895        Ok(())
896    }
897
898    /// Updates the database to add any `.crate` files that are currently
899    /// not tracked (such as when they are downloaded by an older version of
900    /// cargo).
901    #[tracing::instrument(skip(conn, now, base_path))]
902    fn populate_untracked_crate(
903        conn: &Connection,
904        now: Timestamp,
905        base_path: &Path,
906    ) -> CargoResult<()> {
907        trace!(target: "gc", "populating untracked crate files");
908        let mut insert_stmt = conn.prepare_cached(
909            "INSERT INTO registry_crate (registry_id, name, size, timestamp)
910             VALUES (?1, ?2, ?3, ?4)
911             ON CONFLICT DO NOTHING",
912        )?;
913        let index_names = Self::list_dir_names(&base_path)?;
914        for index_name in index_names {
915            let Some(id) = Self::id_from_name(conn, REGISTRY_INDEX_TABLE, &index_name)? else {
916                // The id is missing from the database. This should be resolved
917                // via update_db_parent_for_removed_from_disk.
918                continue;
919            };
920            let index_path = base_path.join(index_name);
921            let crates = Self::read_dir_with_filter(&index_path, &|entry| {
922                entry.file_type().map_or(false, |ty| ty.is_file())
923                    && entry
924                        .file_name()
925                        .to_str()
926                        .map_or(false, |name| name.ends_with(".crate"))
927            })?;
928            for crate_name in crates {
929                // Missing files should have already been taken care of by
930                // update_db_for_removed.
931                let size = paths::metadata(index_path.join(&crate_name))?.len();
932                insert_stmt.execute(params![id, crate_name, size, now])?;
933            }
934        }
935        Ok(())
936    }
937
938    /// Updates the database to add any files that are currently not tracked
939    /// (such as when they are downloaded by an older version of cargo).
940    #[tracing::instrument(skip(conn, now, gctx, base_path, populate_size))]
941    fn populate_untracked(
942        conn: &Connection,
943        now: Timestamp,
944        gctx: &GlobalContext,
945        id_table_name: &str,
946        id_column_name: &str,
947        table_name: &str,
948        base_path: &Path,
949        populate_size: bool,
950    ) -> CargoResult<()> {
951        trace!(target: "gc", "populating untracked files for {table_name}");
952        // Gather names (and make sure they are in the database).
953        let id_names = Self::list_dir_names(&base_path)?;
954
955        // This SELECT is used to determine if the directory is already
956        // tracked. We don't want to do the expensive size computation unless
957        // necessary.
958        let mut select_stmt = conn.prepare_cached(&format!(
959            "SELECT 1 FROM {table_name}
960             WHERE {id_column_name} = ?1 AND name = ?2",
961        ))?;
962        let mut insert_stmt = conn.prepare_cached(&format!(
963            "INSERT INTO {table_name} ({id_column_name}, name, size, timestamp)
964             VALUES (?1, ?2, ?3, ?4)
965             ON CONFLICT DO NOTHING",
966        ))?;
967        let mut progress = Progress::with_style("Scanning", ProgressStyle::Ratio, gctx);
968        // Compute the size of any directory not in the database.
969        for id_name in id_names {
970            let Some(id) = Self::id_from_name(conn, id_table_name, &id_name)? else {
971                // The id is missing from the database. This should be resolved
972                // via update_db_parent_for_removed_from_disk.
973                continue;
974            };
975            let index_path = base_path.join(id_name);
976            let names = Self::list_dir_names(&index_path)?;
977            let max = names.len();
978            for (i, name) in names.iter().enumerate() {
979                if select_stmt.exists(params![id, name])? {
980                    continue;
981                }
982                let dir_path = index_path.join(name);
983                if !dir_path.is_dir() {
984                    continue;
985                }
986                progress.tick(i, max, "")?;
987                let size = if populate_size {
988                    Some(du(&dir_path, table_name)?)
989                } else {
990                    None
991                };
992                insert_stmt.execute(params![id, name, size, now])?;
993            }
994        }
995        Ok(())
996    }
997
998    /// Fills in the `size` column where it is NULL.
999    ///
1000    /// This can happen when something is added to disk by an older version of
1001    /// cargo, and one of the mark functions marked it without knowing the
1002    /// size.
1003    ///
1004    /// `update_db_for_removed` should be called before this is called.
1005    #[tracing::instrument(skip(conn, gctx, base_path))]
1006    fn update_null_sizes(
1007        conn: &Connection,
1008        gctx: &GlobalContext,
1009        parent_table_name: &str,
1010        id_column_name: &str,
1011        table_name: &str,
1012        base_path: &Path,
1013    ) -> CargoResult<()> {
1014        trace!(target: "gc", "updating NULL size information in {table_name}");
1015        let mut null_stmt = conn.prepare_cached(&format!(
1016            "SELECT {table_name}.rowid, {table_name}.name, {parent_table_name}.name
1017             FROM {table_name}, {parent_table_name}
1018             WHERE {table_name}.size IS NULL AND {table_name}.{id_column_name} = {parent_table_name}.id",
1019        ))?;
1020        let mut update_stmt = conn.prepare_cached(&format!(
1021            "UPDATE {table_name} SET size = ?1 WHERE rowid = ?2"
1022        ))?;
1023        let mut progress = Progress::with_style("Scanning", ProgressStyle::Ratio, gctx);
1024        let rows: Vec<_> = null_stmt
1025            .query_map([], |row| {
1026                Ok((row.get_unwrap(0), row.get_unwrap(1), row.get_unwrap(2)))
1027            })?
1028            .collect();
1029        let max = rows.len();
1030        for (i, row) in rows.into_iter().enumerate() {
1031            let (rowid, name, id_name): (i64, String, String) = row?;
1032            let path = base_path.join(id_name).join(name);
1033            progress.tick(i, max, "")?;
1034            // Missing files should have already been taken care of by
1035            // update_db_for_removed.
1036            let size = du(&path, table_name)?;
1037            update_stmt.execute(params![size, rowid])?;
1038        }
1039        Ok(())
1040    }
1041
1042    /// Adds paths to delete from either `registry_crate` or `registry_src` whose
1043    /// last use is older than the given timestamp.
1044    fn get_registry_items_to_clean_age(
1045        conn: &Connection,
1046        max_age: Timestamp,
1047        table_name: &str,
1048        base_path: &Path,
1049        delete_paths: &mut Vec<PathBuf>,
1050    ) -> CargoResult<()> {
1051        debug!(target: "gc", "cleaning {table_name} since {max_age:?}");
1052        let mut stmt = conn.prepare_cached(&format!(
1053            "DELETE FROM {table_name} WHERE timestamp < ?1
1054                RETURNING registry_id, name"
1055        ))?;
1056        let rows = stmt
1057            .query_map(params![max_age], |row| {
1058                let registry_id = row.get_unwrap(0);
1059                let name: String = row.get_unwrap(1);
1060                Ok((registry_id, name))
1061            })?
1062            .collect::<Result<Vec<_>, _>>()?;
1063        let ids: Vec<_> = rows.iter().map(|r| r.0).collect();
1064        let id_map = Self::get_id_map(conn, REGISTRY_INDEX_TABLE, &ids)?;
1065        for (id, name) in rows {
1066            let encoded_registry_name = &id_map[&id];
1067            delete_paths.push(base_path.join(encoded_registry_name).join(name));
1068        }
1069        Ok(())
1070    }
1071
1072    /// Adds paths to delete from either `registry_crate` or `registry_src` in
1073    /// order to keep the total size under the given max size.
1074    fn get_registry_items_to_clean_size(
1075        conn: &Connection,
1076        max_size: u64,
1077        table_name: &str,
1078        base_path: &Path,
1079        delete_paths: &mut Vec<PathBuf>,
1080    ) -> CargoResult<()> {
1081        debug!(target: "gc", "cleaning {table_name} till under {max_size:?}");
1082        let total_size: u64 = conn.query_row(
1083            &format!("SELECT coalesce(SUM(size), 0) FROM {table_name}"),
1084            [],
1085            |row| row.get(0),
1086        )?;
1087        if total_size <= max_size {
1088            return Ok(());
1089        }
1090        // This SQL statement selects all of the rows ordered by timestamp,
1091        // and then uses a window function to keep a running total of the
1092        // size. It selects all rows until the running total exceeds the
1093        // threshold of the total number of bytes that we want to delete.
1094        //
1095        // The window function essentially computes an aggregate over all
1096        // previous rows as it goes along. As long as the running size is
1097        // below the total amount that we need to delete, it keeps picking
1098        // more rows.
1099        //
1100        // The ORDER BY includes `name` mainly for test purposes so that
1101        // entries with the same timestamp have deterministic behavior.
1102        //
1103        // The coalesce helps convert NULL to 0.
1104        let mut stmt = conn.prepare(&format!(
1105            "DELETE FROM {table_name} WHERE rowid IN \
1106                (SELECT x.rowid FROM \
1107                    (SELECT rowid, size, SUM(size) OVER \
1108                        (ORDER BY timestamp, name ROWS UNBOUNDED PRECEDING) AS running_amount \
1109                        FROM {table_name}) x \
1110                    WHERE coalesce(x.running_amount, 0) - x.size < ?1) \
1111                RETURNING registry_id, name;"
1112        ))?;
1113        let rows = stmt
1114            .query_map(params![total_size - max_size], |row| {
1115                let id = row.get_unwrap(0);
1116                let name: String = row.get_unwrap(1);
1117                Ok((id, name))
1118            })?
1119            .collect::<Result<Vec<_>, _>>()?;
1120        // Convert registry_id to the encoded registry name, and join those.
1121        let ids: Vec<_> = rows.iter().map(|r| r.0).collect();
1122        let id_map = Self::get_id_map(conn, REGISTRY_INDEX_TABLE, &ids)?;
1123        for (id, name) in rows {
1124            let encoded_name = &id_map[&id];
1125            delete_paths.push(base_path.join(encoded_name).join(name));
1126        }
1127        Ok(())
1128    }
1129
1130    /// Adds paths to delete from both `registry_crate` and `registry_src` in
1131    /// order to keep the total size under the given max size.
1132    fn get_registry_items_to_clean_size_both(
1133        conn: &Connection,
1134        max_size: u64,
1135        base: &BasePaths,
1136        delete_paths: &mut Vec<PathBuf>,
1137    ) -> CargoResult<()> {
1138        debug!(target: "gc", "cleaning download till under {max_size:?}");
1139
1140        // This SQL statement selects from both registry_src and
1141        // registry_crate so that sorting of timestamps incorporates both of
1142        // them at the same time. It uses a const value of 1 or 2 as the first
1143        // column so that the code below can determine which table the value
1144        // came from.
1145        let mut stmt = conn.prepare_cached(
1146            "SELECT 1, registry_src.rowid, registry_src.name AS name, registry_index.name,
1147                    registry_src.size, registry_src.timestamp AS timestamp
1148             FROM registry_src, registry_index
1149             WHERE registry_src.registry_id = registry_index.id AND registry_src.size NOT NULL
1150
1151             UNION
1152
1153             SELECT 2, registry_crate.rowid, registry_crate.name AS name, registry_index.name,
1154                    registry_crate.size, registry_crate.timestamp AS timestamp
1155             FROM registry_crate, registry_index
1156             WHERE registry_crate.registry_id = registry_index.id
1157
1158             ORDER BY timestamp, name",
1159        )?;
1160        let mut delete_src_stmt =
1161            conn.prepare_cached("DELETE FROM registry_src WHERE rowid = ?1")?;
1162        let mut delete_crate_stmt =
1163            conn.prepare_cached("DELETE FROM registry_crate WHERE rowid = ?1")?;
1164        let rows = stmt
1165            .query_map([], |row| {
1166                Ok((
1167                    row.get_unwrap(0),
1168                    row.get_unwrap(1),
1169                    row.get_unwrap(2),
1170                    row.get_unwrap(3),
1171                    row.get_unwrap(4),
1172                ))
1173            })?
1174            .collect::<Result<Vec<(i64, i64, String, String, u64)>, _>>()?;
1175        let mut total_size: u64 = rows.iter().map(|r| r.4).sum();
1176        debug!(target: "gc", "total download cache size appears to be {total_size}");
1177        for (table, rowid, name, index_name, size) in rows {
1178            if total_size <= max_size {
1179                break;
1180            }
1181            if table == 1 {
1182                delete_paths.push(base.src.join(index_name).join(name));
1183                delete_src_stmt.execute([rowid])?;
1184            } else {
1185                delete_paths.push(base.crate_dir.join(index_name).join(name));
1186                delete_crate_stmt.execute([rowid])?;
1187            }
1188            // TODO: If delete crate, ensure src is also deleted.
1189            total_size -= size;
1190        }
1191        Ok(())
1192    }
1193
1194    /// Adds paths to delete from the git cache, keeping the total size under
1195    /// the give value.
1196    ///
1197    /// Paths are relative to the `git` directory in the cache directory.
1198    fn get_git_items_to_clean_size(
1199        conn: &Connection,
1200        max_size: u64,
1201        base: &BasePaths,
1202        delete_paths: &mut Vec<PathBuf>,
1203    ) -> CargoResult<()> {
1204        debug!(target: "gc", "cleaning git till under {max_size:?}");
1205
1206        // Collect all the sizes from git_db and git_checkouts, and then sort them by timestamp.
1207        let mut stmt = conn.prepare_cached("SELECT rowid, name, timestamp FROM git_db")?;
1208        let mut git_info = stmt
1209            .query_map([], |row| {
1210                let rowid: i64 = row.get_unwrap(0);
1211                let name: String = row.get_unwrap(1);
1212                let timestamp: Timestamp = row.get_unwrap(2);
1213                // Size is added below so that the error doesn't need to be
1214                // converted to a rusqlite error.
1215                Ok((timestamp, rowid, None, name, 0))
1216            })?
1217            .collect::<Result<Vec<_>, _>>()?;
1218        for info in &mut git_info {
1219            let size = cargo_util::du(&base.git_db.join(&info.3), &[])?;
1220            info.4 = size;
1221        }
1222
1223        let mut stmt = conn.prepare_cached(
1224            "SELECT git_checkout.rowid, git_db.name, git_checkout.name,
1225                git_checkout.size, git_checkout.timestamp
1226                FROM git_checkout, git_db
1227                WHERE git_checkout.git_id = git_db.id AND git_checkout.size NOT NULL",
1228        )?;
1229        let git_co_rows = stmt
1230            .query_map([], |row| {
1231                let rowid = row.get_unwrap(0);
1232                let db_name: String = row.get_unwrap(1);
1233                let name = row.get_unwrap(2);
1234                let size = row.get_unwrap(3);
1235                let timestamp = row.get_unwrap(4);
1236                Ok((timestamp, rowid, Some(db_name), name, size))
1237            })?
1238            .collect::<Result<Vec<_>, _>>()?;
1239        git_info.extend(git_co_rows);
1240
1241        // Sort by timestamp, and name. The name is included mostly for test
1242        // purposes so that entries with the same timestamp have deterministic
1243        // behavior.
1244        git_info.sort_by(|a, b| (b.0, &b.3).cmp(&(a.0, &a.3)));
1245
1246        // Collect paths to delete.
1247        let mut delete_db_stmt = conn.prepare_cached("DELETE FROM git_db WHERE rowid = ?1")?;
1248        let mut delete_co_stmt =
1249            conn.prepare_cached("DELETE FROM git_checkout WHERE rowid = ?1")?;
1250        let mut total_size: u64 = git_info.iter().map(|r| r.4).sum();
1251        debug!(target: "gc", "total git cache size appears to be {total_size}");
1252        while let Some((_timestamp, rowid, db_name, name, size)) = git_info.pop() {
1253            if total_size <= max_size {
1254                break;
1255            }
1256            if let Some(db_name) = db_name {
1257                delete_paths.push(base.git_co.join(db_name).join(name));
1258                delete_co_stmt.execute([rowid])?;
1259                total_size -= size;
1260            } else {
1261                total_size -= size;
1262                delete_paths.push(base.git_db.join(&name));
1263                delete_db_stmt.execute([rowid])?;
1264                // If the db is deleted, then all the checkouts must be deleted.
1265                let mut i = 0;
1266                while i < git_info.len() {
1267                    if git_info[i].2.as_deref() == Some(name.as_ref()) {
1268                        let (_, rowid, db_name, name, size) = git_info.remove(i);
1269                        delete_paths.push(base.git_co.join(db_name.unwrap()).join(name));
1270                        delete_co_stmt.execute([rowid])?;
1271                        total_size -= size;
1272                    } else {
1273                        i += 1;
1274                    }
1275                }
1276            }
1277        }
1278        Ok(())
1279    }
1280
1281    /// Adds paths to delete from `registry_index` whose last use is older
1282    /// than the given timestamp.
1283    fn get_registry_index_to_clean(
1284        conn: &Connection,
1285        max_age: Timestamp,
1286        base: &BasePaths,
1287        delete_paths: &mut Vec<PathBuf>,
1288    ) -> CargoResult<()> {
1289        debug!(target: "gc", "cleaning index since {max_age:?}");
1290        let mut stmt = conn.prepare_cached(
1291            "DELETE FROM registry_index WHERE timestamp < ?1
1292                RETURNING name",
1293        )?;
1294        let mut rows = stmt.query([max_age])?;
1295        while let Some(row) = rows.next()? {
1296            let name: String = row.get_unwrap(0);
1297            delete_paths.push(base.index.join(&name));
1298            // Also delete .crate and src directories, since by definition
1299            // they cannot be used without their index.
1300            delete_paths.push(base.src.join(&name));
1301            delete_paths.push(base.crate_dir.join(&name));
1302        }
1303        Ok(())
1304    }
1305
1306    /// Adds paths to delete from `git_checkout` whose last use is
1307    /// older than the given timestamp.
1308    fn get_git_co_items_to_clean(
1309        conn: &Connection,
1310        max_age: Timestamp,
1311        base_path: &Path,
1312        delete_paths: &mut Vec<PathBuf>,
1313    ) -> CargoResult<()> {
1314        debug!(target: "gc", "cleaning git co since {max_age:?}");
1315        let mut stmt = conn.prepare_cached(
1316            "DELETE FROM git_checkout WHERE timestamp < ?1
1317                RETURNING git_id, name",
1318        )?;
1319        let rows = stmt
1320            .query_map(params![max_age], |row| {
1321                let git_id = row.get_unwrap(0);
1322                let name: String = row.get_unwrap(1);
1323                Ok((git_id, name))
1324            })?
1325            .collect::<Result<Vec<_>, _>>()?;
1326        let ids: Vec<_> = rows.iter().map(|r| r.0).collect();
1327        let id_map = Self::get_id_map(conn, GIT_DB_TABLE, &ids)?;
1328        for (id, name) in rows {
1329            let encoded_git_name = &id_map[&id];
1330            delete_paths.push(base_path.join(encoded_git_name).join(name));
1331        }
1332        Ok(())
1333    }
1334
1335    /// Adds paths to delete from `git_db` in order to keep the total size
1336    /// under the given max size.
1337    fn get_git_db_items_to_clean(
1338        conn: &Connection,
1339        max_age: Timestamp,
1340        base: &BasePaths,
1341        delete_paths: &mut Vec<PathBuf>,
1342    ) -> CargoResult<()> {
1343        debug!(target: "gc", "cleaning git db since {max_age:?}");
1344        let mut stmt = conn.prepare_cached(
1345            "DELETE FROM git_db WHERE timestamp < ?1
1346                RETURNING name",
1347        )?;
1348        let mut rows = stmt.query([max_age])?;
1349        while let Some(row) = rows.next()? {
1350            let name: String = row.get_unwrap(0);
1351            delete_paths.push(base.git_db.join(&name));
1352            // Also delete checkout directories, since by definition they
1353            // cannot be used without their db.
1354            delete_paths.push(base.git_co.join(&name));
1355        }
1356        Ok(())
1357    }
1358}
1359
1360/// Helper to generate the upsert for the parent tables.
1361///
1362/// This handles checking if the row already exists, and only updates the
1363/// timestamp it if it hasn't been updated recently. This also handles keeping
1364/// a cached map of the `id` value.
1365///
1366/// Unfortunately it is a bit tricky to share this code without a macro.
1367macro_rules! insert_or_update_parent {
1368    ($self:expr, $conn:expr, $table_name:expr, $timestamps_field:ident, $keys_field:ident, $encoded_name:ident) => {
1369        let mut select_stmt = $conn.prepare_cached(concat!(
1370            "SELECT id, timestamp FROM ",
1371            $table_name,
1372            " WHERE name = ?1"
1373        ))?;
1374        let mut insert_stmt = $conn.prepare_cached(concat!(
1375            "INSERT INTO ",
1376            $table_name,
1377            " (name, timestamp)
1378                VALUES (?1, ?2)
1379                ON CONFLICT DO UPDATE SET timestamp=excluded.timestamp
1380                RETURNING id",
1381        ))?;
1382        let mut update_stmt = $conn.prepare_cached(concat!(
1383            "UPDATE ",
1384            $table_name,
1385            " SET timestamp = ?1 WHERE id = ?2"
1386        ))?;
1387        for (parent, new_timestamp) in std::mem::take(&mut $self.$timestamps_field) {
1388            trace!(target: "gc",
1389                concat!("insert ", $table_name, " {:?} {}"),
1390                parent,
1391                new_timestamp
1392            );
1393            let mut rows = select_stmt.query([parent.$encoded_name])?;
1394            let id = if let Some(row) = rows.next()? {
1395                let id: ParentId = row.get_unwrap(0);
1396                let timestamp: Timestamp = row.get_unwrap(1);
1397                if timestamp < new_timestamp - UPDATE_RESOLUTION {
1398                    update_stmt.execute(params![new_timestamp, id])?;
1399                }
1400                id
1401            } else {
1402                insert_stmt.query_row(params![parent.$encoded_name, new_timestamp], |row| {
1403                    row.get(0)
1404                })?
1405            };
1406            match $self.$keys_field.entry(parent.$encoded_name) {
1407                hash_map::Entry::Occupied(o) => {
1408                    assert_eq!(*o.get(), id);
1409                }
1410                hash_map::Entry::Vacant(v) => {
1411                    v.insert(id);
1412                }
1413            }
1414        }
1415        return Ok(());
1416    };
1417}
1418
1419/// This is a cache of modifications that will be saved to disk all at once
1420/// via the [`DeferredGlobalLastUse::save`] method.
1421///
1422/// This is here to improve performance.
1423#[derive(Debug)]
1424pub struct DeferredGlobalLastUse {
1425    /// Cache of registry keys, used for faster fetching.
1426    ///
1427    /// The key is the registry name (which is its directory name) and the
1428    /// value is the `id` in the `registry_index` table.
1429    registry_keys: HashMap<InternedString, ParentId>,
1430    /// Cache of git keys, used for faster fetching.
1431    ///
1432    /// The key is the git db name (which is its directory name) and the value
1433    /// is the `id` in the `git_db` table.
1434    git_keys: HashMap<InternedString, ParentId>,
1435
1436    /// New registry index entries to insert.
1437    registry_index_timestamps: HashMap<RegistryIndex, Timestamp>,
1438    /// New registry `.crate` entries to insert.
1439    registry_crate_timestamps: HashMap<RegistryCrate, Timestamp>,
1440    /// New registry src directory entries to insert.
1441    registry_src_timestamps: HashMap<RegistrySrc, Timestamp>,
1442    /// New git db entries to insert.
1443    git_db_timestamps: HashMap<GitDb, Timestamp>,
1444    /// New git checkout entries to insert.
1445    git_checkout_timestamps: HashMap<GitCheckout, Timestamp>,
1446    /// This is used so that a warning about failing to update the database is
1447    /// only displayed once.
1448    save_err_has_warned: bool,
1449    /// The current time, used to improve performance to avoid accessing the
1450    /// clock hundreds of times.
1451    now: Timestamp,
1452}
1453
1454impl DeferredGlobalLastUse {
1455    pub fn new() -> DeferredGlobalLastUse {
1456        DeferredGlobalLastUse {
1457            registry_keys: HashMap::default(),
1458            git_keys: HashMap::default(),
1459            registry_index_timestamps: HashMap::default(),
1460            registry_crate_timestamps: HashMap::default(),
1461            registry_src_timestamps: HashMap::default(),
1462            git_db_timestamps: HashMap::default(),
1463            git_checkout_timestamps: HashMap::default(),
1464            save_err_has_warned: false,
1465            now: now(),
1466        }
1467    }
1468
1469    pub fn is_empty(&self) -> bool {
1470        self.registry_index_timestamps.is_empty()
1471            && self.registry_crate_timestamps.is_empty()
1472            && self.registry_src_timestamps.is_empty()
1473            && self.git_db_timestamps.is_empty()
1474            && self.git_checkout_timestamps.is_empty()
1475    }
1476
1477    fn clear(&mut self) {
1478        self.registry_index_timestamps.clear();
1479        self.registry_crate_timestamps.clear();
1480        self.registry_src_timestamps.clear();
1481        self.git_db_timestamps.clear();
1482        self.git_checkout_timestamps.clear();
1483    }
1484
1485    /// Indicates the given [`RegistryIndex`] has been used right now.
1486    pub fn mark_registry_index_used(&mut self, registry_index: RegistryIndex) {
1487        self.mark_registry_index_used_stamp(registry_index, None);
1488    }
1489
1490    /// Indicates the given [`RegistryCrate`] has been used right now.
1491    ///
1492    /// Also implicitly marks the index used, too.
1493    pub fn mark_registry_crate_used(&mut self, registry_crate: RegistryCrate) {
1494        self.mark_registry_crate_used_stamp(registry_crate, None);
1495    }
1496
1497    /// Indicates the given [`RegistrySrc`] has been used right now.
1498    ///
1499    /// Also implicitly marks the index used, too.
1500    pub fn mark_registry_src_used(&mut self, registry_src: RegistrySrc) {
1501        self.mark_registry_src_used_stamp(registry_src, None);
1502    }
1503
1504    /// Indicates the given [`GitCheckout`] has been used right now.
1505    ///
1506    /// Also implicitly marks the git db used, too.
1507    pub fn mark_git_checkout_used(&mut self, git_checkout: GitCheckout) {
1508        self.mark_git_checkout_used_stamp(git_checkout, None);
1509    }
1510
1511    /// Indicates the given [`RegistryIndex`] has been used with the given
1512    /// time (or "now" if `None`).
1513    pub fn mark_registry_index_used_stamp(
1514        &mut self,
1515        registry_index: RegistryIndex,
1516        timestamp: Option<&SystemTime>,
1517    ) {
1518        let timestamp = timestamp.map_or(self.now, to_timestamp);
1519        self.registry_index_timestamps
1520            .insert(registry_index, timestamp);
1521    }
1522
1523    /// Indicates the given [`RegistryCrate`] has been used with the given
1524    /// time (or "now" if `None`).
1525    ///
1526    /// Also implicitly marks the index used, too.
1527    pub fn mark_registry_crate_used_stamp(
1528        &mut self,
1529        registry_crate: RegistryCrate,
1530        timestamp: Option<&SystemTime>,
1531    ) {
1532        let timestamp = timestamp.map_or(self.now, to_timestamp);
1533        let index = RegistryIndex {
1534            encoded_registry_name: registry_crate.encoded_registry_name,
1535        };
1536        self.registry_index_timestamps.insert(index, timestamp);
1537        self.registry_crate_timestamps
1538            .insert(registry_crate, timestamp);
1539    }
1540
1541    /// Indicates the given [`RegistrySrc`] has been used with the given
1542    /// time (or "now" if `None`).
1543    ///
1544    /// Also implicitly marks the index used, too.
1545    pub fn mark_registry_src_used_stamp(
1546        &mut self,
1547        registry_src: RegistrySrc,
1548        timestamp: Option<&SystemTime>,
1549    ) {
1550        let timestamp = timestamp.map_or(self.now, to_timestamp);
1551        let index = RegistryIndex {
1552            encoded_registry_name: registry_src.encoded_registry_name,
1553        };
1554        self.registry_index_timestamps.insert(index, timestamp);
1555        self.registry_src_timestamps.insert(registry_src, timestamp);
1556    }
1557
1558    /// Indicates the given [`GitCheckout`] has been used with the given
1559    /// time (or "now" if `None`).
1560    ///
1561    /// Also implicitly marks the git db used, too.
1562    pub fn mark_git_checkout_used_stamp(
1563        &mut self,
1564        git_checkout: GitCheckout,
1565        timestamp: Option<&SystemTime>,
1566    ) {
1567        let timestamp = timestamp.map_or(self.now, to_timestamp);
1568        let db = GitDb {
1569            encoded_git_name: git_checkout.encoded_git_name,
1570        };
1571        self.git_db_timestamps.insert(db, timestamp);
1572        self.git_checkout_timestamps.insert(git_checkout, timestamp);
1573    }
1574
1575    /// Saves all of the deferred information to the database.
1576    ///
1577    /// This will also clear the state of `self`.
1578    #[tracing::instrument(skip_all)]
1579    pub fn save(&mut self, tracker: &mut GlobalCacheTracker) -> CargoResult<()> {
1580        trace!(target: "gc", "saving last-use data");
1581        if self.is_empty() {
1582            return Ok(());
1583        }
1584        let tx = tracker.conn.transaction()?;
1585        // These must run before the ones that refer to their IDs.
1586        self.insert_registry_index_from_cache(&tx)?;
1587        self.insert_git_db_from_cache(&tx)?;
1588        self.insert_registry_crate_from_cache(&tx)?;
1589        self.insert_registry_src_from_cache(&tx)?;
1590        self.insert_git_checkout_from_cache(&tx)?;
1591        tx.commit()?;
1592        trace!(target: "gc", "last-use save complete");
1593        Ok(())
1594    }
1595
1596    /// Variant of [`DeferredGlobalLastUse::save`] that does not return an
1597    /// error.
1598    ///
1599    /// This will log or display a warning to the user.
1600    pub fn save_no_error(&mut self, gctx: &GlobalContext) {
1601        if let Err(e) = self.save_with_gctx(gctx) {
1602            // Because there is an assertion in auto-gc that checks if this is
1603            // empty, be sure to clear it so that assertion doesn't fail.
1604            self.clear();
1605            if !self.save_err_has_warned {
1606                if is_silent_error(&e) && gctx.shell().verbosity() != Verbosity::Verbose {
1607                    tracing::warn!("failed to save last-use data: {e:?}");
1608                } else {
1609                    crate::display_warning_with_error(
1610                        "failed to save last-use data\n\
1611                        This may prevent cargo from accurately tracking what is being \
1612                        used in its global cache. This information is used for \
1613                        automatically removing unused data in the cache.",
1614                        &e,
1615                        &mut gctx.shell(),
1616                    );
1617                    self.save_err_has_warned = true;
1618                }
1619            }
1620        }
1621    }
1622
1623    fn save_with_gctx(&mut self, gctx: &GlobalContext) -> CargoResult<()> {
1624        let mut tracker = gctx.global_cache_tracker()?;
1625        self.save(&mut tracker)
1626    }
1627
1628    /// Flushes all of the `registry_index_timestamps` to the database,
1629    /// clearing `registry_index_timestamps`.
1630    fn insert_registry_index_from_cache(&mut self, conn: &Connection) -> CargoResult<()> {
1631        insert_or_update_parent!(
1632            self,
1633            conn,
1634            "registry_index",
1635            registry_index_timestamps,
1636            registry_keys,
1637            encoded_registry_name
1638        );
1639    }
1640
1641    /// Flushes all of the `git_db_timestamps` to the database,
1642    /// clearing `registry_index_timestamps`.
1643    fn insert_git_db_from_cache(&mut self, conn: &Connection) -> CargoResult<()> {
1644        insert_or_update_parent!(
1645            self,
1646            conn,
1647            "git_db",
1648            git_db_timestamps,
1649            git_keys,
1650            encoded_git_name
1651        );
1652    }
1653
1654    /// Flushes all of the `registry_crate_timestamps` to the database,
1655    /// clearing `registry_index_timestamps`.
1656    fn insert_registry_crate_from_cache(&mut self, conn: &Connection) -> CargoResult<()> {
1657        let registry_crate_timestamps = std::mem::take(&mut self.registry_crate_timestamps);
1658        for (registry_crate, timestamp) in registry_crate_timestamps {
1659            trace!(target: "gc", "insert registry crate {registry_crate:?} {timestamp}");
1660            let registry_id = self.registry_id(conn, registry_crate.encoded_registry_name)?;
1661            let mut stmt = conn.prepare_cached(
1662                "INSERT INTO registry_crate (registry_id, name, size, timestamp)
1663                 VALUES (?1, ?2, ?3, ?4)
1664                 ON CONFLICT DO UPDATE SET timestamp=excluded.timestamp
1665                    WHERE timestamp < ?5
1666                 ",
1667            )?;
1668            stmt.execute(params![
1669                registry_id,
1670                registry_crate.crate_filename,
1671                registry_crate.size,
1672                timestamp,
1673                timestamp - UPDATE_RESOLUTION
1674            ])?;
1675        }
1676        Ok(())
1677    }
1678
1679    /// Flushes all of the `registry_src_timestamps` to the database,
1680    /// clearing `registry_index_timestamps`.
1681    fn insert_registry_src_from_cache(&mut self, conn: &Connection) -> CargoResult<()> {
1682        let registry_src_timestamps = std::mem::take(&mut self.registry_src_timestamps);
1683        for (registry_src, timestamp) in registry_src_timestamps {
1684            trace!(target: "gc", "insert registry src {registry_src:?} {timestamp}");
1685            let registry_id = self.registry_id(conn, registry_src.encoded_registry_name)?;
1686            let mut stmt = conn.prepare_cached(
1687                "INSERT INTO registry_src (registry_id, name, size, timestamp)
1688                 VALUES (?1, ?2, ?3, ?4)
1689                 ON CONFLICT DO UPDATE SET timestamp=excluded.timestamp
1690                    WHERE timestamp < ?5
1691                 ",
1692            )?;
1693            stmt.execute(params![
1694                registry_id,
1695                registry_src.package_dir,
1696                registry_src.size,
1697                timestamp,
1698                timestamp - UPDATE_RESOLUTION
1699            ])?;
1700        }
1701
1702        Ok(())
1703    }
1704
1705    /// Flushes all of the `git_checkout_timestamps` to the database,
1706    /// clearing `registry_index_timestamps`.
1707    fn insert_git_checkout_from_cache(&mut self, conn: &Connection) -> CargoResult<()> {
1708        let git_checkout_timestamps = std::mem::take(&mut self.git_checkout_timestamps);
1709        for (git_checkout, timestamp) in git_checkout_timestamps {
1710            let git_id = self.git_id(conn, git_checkout.encoded_git_name)?;
1711            let mut stmt = conn.prepare_cached(
1712                "INSERT INTO git_checkout (git_id, name, size, timestamp)
1713                 VALUES (?1, ?2, ?3, ?4)
1714                 ON CONFLICT DO UPDATE SET timestamp=excluded.timestamp
1715                    WHERE timestamp < ?5",
1716            )?;
1717            stmt.execute(params![
1718                git_id,
1719                git_checkout.short_name,
1720                git_checkout.size,
1721                timestamp,
1722                timestamp - UPDATE_RESOLUTION
1723            ])?;
1724        }
1725
1726        Ok(())
1727    }
1728
1729    /// Returns the numeric ID of the registry, either fetching from the local
1730    /// cache, or getting it from the database.
1731    ///
1732    /// It is an error if the registry does not exist.
1733    fn registry_id(
1734        &mut self,
1735        conn: &Connection,
1736        encoded_registry_name: InternedString,
1737    ) -> CargoResult<ParentId> {
1738        match self.registry_keys.get(&encoded_registry_name) {
1739            Some(i) => Ok(*i),
1740            None => {
1741                let Some(id) = GlobalCacheTracker::id_from_name(
1742                    conn,
1743                    REGISTRY_INDEX_TABLE,
1744                    &encoded_registry_name,
1745                )?
1746                else {
1747                    bail!(
1748                        "expected registry_index {encoded_registry_name} to exist, but wasn't found"
1749                    );
1750                };
1751                self.registry_keys.insert(encoded_registry_name, id);
1752                Ok(id)
1753            }
1754        }
1755    }
1756
1757    /// Returns the numeric ID of the git db, either fetching from the local
1758    /// cache, or getting it from the database.
1759    ///
1760    /// It is an error if the git db does not exist.
1761    fn git_id(
1762        &mut self,
1763        conn: &Connection,
1764        encoded_git_name: InternedString,
1765    ) -> CargoResult<ParentId> {
1766        match self.git_keys.get(&encoded_git_name) {
1767            Some(i) => Ok(*i),
1768            None => {
1769                let Some(id) =
1770                    GlobalCacheTracker::id_from_name(conn, GIT_DB_TABLE, &encoded_git_name)?
1771                else {
1772                    bail!("expected git_db {encoded_git_name} to exist, but wasn't found")
1773                };
1774                self.git_keys.insert(encoded_git_name, id);
1775                Ok(id)
1776            }
1777        }
1778    }
1779}
1780
1781/// Converts a [`SystemTime`] to a [`Timestamp`] which can be stored in the database.
1782fn to_timestamp(t: &SystemTime) -> Timestamp {
1783    t.duration_since(SystemTime::UNIX_EPOCH)
1784        .expect("invalid clock")
1785        .as_secs()
1786}
1787
1788/// Returns the current time.
1789///
1790/// This supports pretending that the time is different for testing using an
1791/// environment variable.
1792///
1793/// If possible, try to avoid calling this too often since accessing clocks
1794/// can be a little slow on some systems.
1795#[expect(
1796    clippy::disallowed_methods,
1797    reason = "testing only, no reason for config support"
1798)]
1799fn now() -> Timestamp {
1800    match std::env::var("__CARGO_TEST_LAST_USE_NOW") {
1801        Ok(now) => now.parse().unwrap(),
1802        Err(_) => to_timestamp(&SystemTime::now()),
1803    }
1804}
1805
1806/// Returns whether or not the given error should cause a warning to be
1807/// displayed to the user.
1808///
1809/// In some situations, like a read-only global cache, we don't want to spam
1810/// the user with a warning. I think once cargo has controllable lints, I
1811/// think we should consider changing this to always warn, but give the user
1812/// an option to silence the warning.
1813pub fn is_silent_error(e: &anyhow::Error) -> bool {
1814    if let Some(e) = e.downcast_ref::<rusqlite::Error>() {
1815        if matches!(
1816            e.sqlite_error_code(),
1817            Some(ErrorCode::CannotOpen | ErrorCode::ReadOnly)
1818        ) {
1819            return true;
1820        }
1821    }
1822    false
1823}
1824
1825/// Returns the disk usage for a git checkout directory.
1826#[tracing::instrument]
1827fn du_git_checkout(path: &Path) -> CargoResult<u64> {
1828    // !.git is used because clones typically use hardlinks for the git
1829    // contents. TODO: Verify behavior on Windows.
1830    // TODO: Or even better, switch to worktrees, and remove this.
1831    cargo_util::du(&path, &["!.git"])
1832}
1833
1834fn du(path: &Path, table_name: &str) -> CargoResult<u64> {
1835    if table_name == GIT_CO_TABLE {
1836        du_git_checkout(path)
1837    } else {
1838        cargo_util::du(&path, &[])
1839    }
1840}