use crate::ops::CleanContext;
use crate::util::cache_lock::CacheLockMode;
use crate::util::data_structures::HashMap;
use crate::util::interning::InternedString;
use crate::util::sqlite::{self, Migration, basic_migration};
use crate::util::{Filesystem, Progress, ProgressStyle};
use crate::workspace::gc::GcOpts;
use crate::{CargoResult, GlobalContext};
use anyhow::{Context as _, bail};
use cargo_util::paths;
use cargo_util_terminal::Verbosity;
use rusqlite::{Connection, ErrorCode, params};
use std::collections::hash_map;
use std::path::{Path, PathBuf};
use std::time::{Duration, SystemTime};
use tracing::{debug, trace};
const GLOBAL_CACHE_FILENAME: &str = ".global-cache";
const REGISTRY_INDEX_TABLE: &str = "registry_index";
const REGISTRY_CRATE_TABLE: &str = "registry_crate";
const REGISTRY_SRC_TABLE: &str = "registry_src";
const GIT_DB_TABLE: &str = "git_db";
const GIT_CO_TABLE: &str = "git_checkout";
const UPDATE_RESOLUTION: u64 = 60 * 5;
type Timestamp = u64;
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct RegistryIndex {
pub encoded_registry_name: InternedString,
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct RegistryCrate {
pub encoded_registry_name: InternedString,
pub crate_filename: InternedString,
pub size: u64,
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct RegistrySrc {
pub encoded_registry_name: InternedString,
pub package_dir: InternedString,
pub size: Option<u64>,
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct GitDb {
pub encoded_git_name: InternedString,
}
#[derive(Clone, Debug, Hash, Eq, PartialEq)]
pub struct GitCheckout {
pub encoded_git_name: InternedString,
pub short_name: InternedString,
pub size: Option<u64>,
}
struct BasePaths {
index: PathBuf,
git_db: PathBuf,
git_co: PathBuf,
crate_dir: PathBuf,
src: PathBuf,
}
fn migrations() -> Vec<Migration> {
vec![
basic_migration(
"CREATE TABLE registry_index (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
timestamp INTEGER NOT NULL
)",
),
basic_migration(
"CREATE TABLE registry_crate (
registry_id INTEGER NOT NULL,
name TEXT NOT NULL,
size INTEGER NOT NULL,
timestamp INTEGER NOT NULL,
PRIMARY KEY (registry_id, name),
FOREIGN KEY (registry_id) REFERENCES registry_index (id) ON DELETE CASCADE
)",
),
basic_migration(
"CREATE TABLE registry_src (
registry_id INTEGER NOT NULL,
name TEXT NOT NULL,
size INTEGER,
timestamp INTEGER NOT NULL,
PRIMARY KEY (registry_id, name),
FOREIGN KEY (registry_id) REFERENCES registry_index (id) ON DELETE CASCADE
)",
),
basic_migration(
"CREATE TABLE git_db (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL,
timestamp INTEGER NOT NULL
)",
),
basic_migration(
"CREATE TABLE git_checkout (
git_id INTEGER NOT NULL,
name TEXT UNIQUE NOT NULL,
size INTEGER,
timestamp INTEGER NOT NULL,
PRIMARY KEY (git_id, name),
FOREIGN KEY (git_id) REFERENCES git_db (id) ON DELETE CASCADE
)",
),
basic_migration(
"CREATE TABLE global_data (
last_auto_gc INTEGER NOT NULL
)",
),
Box::new(|conn| {
conn.execute(
"INSERT INTO global_data (last_auto_gc) VALUES (?1)",
[now()],
)?;
Ok(())
}),
]
}
#[derive(Copy, Clone, Debug, PartialEq)]
struct ParentId(i64);
impl rusqlite::types::FromSql for ParentId {
fn column_result(value: rusqlite::types::ValueRef<'_>) -> rusqlite::types::FromSqlResult<Self> {
let i = i64::column_result(value)?;
Ok(ParentId(i))
}
}
impl rusqlite::types::ToSql for ParentId {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::from(self.0))
}
}
#[derive(Debug)]
pub struct GlobalCacheTracker {
conn: Connection,
auto_gc_checked_this_session: bool,
}
impl GlobalCacheTracker {
pub fn new(gctx: &GlobalContext) -> CargoResult<GlobalCacheTracker> {
let db_path = Self::db_path(gctx);
let db_path = gctx.assert_package_cache_locked(CacheLockMode::DownloadExclusive, &db_path);
let mut conn = Connection::open(db_path)?;
conn.pragma_update(None, "foreign_keys", true)?;
sqlite::migrate(&mut conn, &migrations())?;
Ok(GlobalCacheTracker {
conn,
auto_gc_checked_this_session: false,
})
}
pub fn db_path(gctx: &GlobalContext) -> Filesystem {
gctx.home().join(GLOBAL_CACHE_FILENAME)
}
fn id_from_name(
conn: &Connection,
table_name: &str,
encoded_name: &str,
) -> CargoResult<Option<ParentId>> {
let mut stmt =
conn.prepare_cached(&format!("SELECT id FROM {table_name} WHERE name = ?"))?;
match stmt.query_row([encoded_name], |row| row.get(0)) {
Ok(id) => Ok(Some(id)),
Err(rusqlite::Error::QueryReturnedNoRows) => Ok(None),
Err(e) => Err(e.into()),
}
}
fn get_id_map(
conn: &Connection,
table_name: &str,
ids: &[i64],
) -> CargoResult<HashMap<i64, PathBuf>> {
let mut stmt =
conn.prepare_cached(&format!("SELECT name FROM {table_name} WHERE id = ?1"))?;
ids.iter()
.map(|id| {
let name = stmt.query_row(params![id], |row| {
Ok(PathBuf::from(row.get::<_, String>(0)?))
})?;
Ok((*id, name))
})
.collect()
}
pub fn registry_index_all(&self) -> CargoResult<Vec<(RegistryIndex, Timestamp)>> {
let mut stmt = self
.conn
.prepare_cached("SELECT name, timestamp FROM registry_index")?;
let rows = stmt
.query_map([], |row| {
let encoded_registry_name = row.get_unwrap(0);
let timestamp = row.get_unwrap(1);
let kind = RegistryIndex {
encoded_registry_name,
};
Ok((kind, timestamp))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn registry_crate_all(&self) -> CargoResult<Vec<(RegistryCrate, Timestamp)>> {
let mut stmt = self.conn.prepare_cached(
"SELECT registry_index.name, registry_crate.name, registry_crate.size, registry_crate.timestamp
FROM registry_index, registry_crate
WHERE registry_crate.registry_id = registry_index.id",
)?;
let rows = stmt
.query_map([], |row| {
let encoded_registry_name = row.get_unwrap(0);
let crate_filename = row.get_unwrap(1);
let size = row.get_unwrap(2);
let timestamp = row.get_unwrap(3);
let kind = RegistryCrate {
encoded_registry_name,
crate_filename,
size,
};
Ok((kind, timestamp))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn registry_src_all(&self) -> CargoResult<Vec<(RegistrySrc, Timestamp)>> {
let mut stmt = self.conn.prepare_cached(
"SELECT registry_index.name, registry_src.name, registry_src.size, registry_src.timestamp
FROM registry_index, registry_src
WHERE registry_src.registry_id = registry_index.id",
)?;
let rows = stmt
.query_map([], |row| {
let encoded_registry_name = row.get_unwrap(0);
let package_dir = row.get_unwrap(1);
let size = row.get_unwrap(2);
let timestamp = row.get_unwrap(3);
let kind = RegistrySrc {
encoded_registry_name,
package_dir,
size,
};
Ok((kind, timestamp))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn git_db_all(&self) -> CargoResult<Vec<(GitDb, Timestamp)>> {
let mut stmt = self
.conn
.prepare_cached("SELECT name, timestamp FROM git_db")?;
let rows = stmt
.query_map([], |row| {
let encoded_git_name = row.get_unwrap(0);
let timestamp = row.get_unwrap(1);
let kind = GitDb { encoded_git_name };
Ok((kind, timestamp))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn git_checkout_all(&self) -> CargoResult<Vec<(GitCheckout, Timestamp)>> {
let mut stmt = self.conn.prepare_cached(
"SELECT git_db.name, git_checkout.name, git_checkout.size, git_checkout.timestamp
FROM git_db, git_checkout
WHERE git_checkout.git_id = git_db.id",
)?;
let rows = stmt
.query_map([], |row| {
let encoded_git_name = row.get_unwrap(0);
let short_name = row.get_unwrap(1);
let size = row.get_unwrap(2);
let timestamp = row.get_unwrap(3);
let kind = GitCheckout {
encoded_git_name,
short_name,
size,
};
Ok((kind, timestamp))
})?
.collect::<Result<Vec<_>, _>>()?;
Ok(rows)
}
pub fn should_run_auto_gc(&mut self, frequency: Duration) -> CargoResult<bool> {
trace!(target: "gc", "should_run_auto_gc");
if self.auto_gc_checked_this_session {
return Ok(false);
}
let last_auto_gc: Timestamp =
self.conn
.query_row("SELECT last_auto_gc FROM global_data", [], |row| row.get(0))?;
let should_run = last_auto_gc + frequency.as_secs() < now();
trace!(target: "gc",
"last auto gc was {}, {}",
last_auto_gc,
if should_run { "running" } else { "skipping" }
);
self.auto_gc_checked_this_session = true;
Ok(should_run)
}
pub fn set_last_auto_gc(&self) -> CargoResult<()> {
self.conn
.execute("UPDATE global_data SET last_auto_gc = ?1", [now()])?;
Ok(())
}
pub fn clean(&mut self, clean_ctx: &mut CleanContext<'_>, gc_opts: &GcOpts) -> CargoResult<()> {
self.clean_inner(clean_ctx, gc_opts)
.context("failed to clean entries from the global cache")
}
#[tracing::instrument(skip_all)]
fn clean_inner(
&mut self,
clean_ctx: &mut CleanContext<'_>,
gc_opts: &GcOpts,
) -> CargoResult<()> {
let gctx = clean_ctx.gctx;
let base = BasePaths {
index: gctx.registry_index_path().into_path_unlocked(),
git_db: gctx.git_db_path().into_path_unlocked(),
git_co: gctx.git_checkouts_path().into_path_unlocked(),
crate_dir: gctx.registry_cache_path().into_path_unlocked(),
src: gctx.registry_source_path().into_path_unlocked(),
};
let now = now();
trace!(target: "gc", "cleaning {gc_opts:?}");
let tx = self.conn.transaction()?;
let mut delete_paths = Vec::new();
if gc_opts.is_download_cache_opt_set() {
Self::sync_db_with_files(
&tx,
now,
gctx,
&base,
gc_opts.is_download_cache_size_set(),
&mut delete_paths,
)
.context("failed to sync tracking database")?
}
if let Some(max_age) = gc_opts.max_index_age {
let max_age = now - max_age.as_secs();
Self::get_registry_index_to_clean(&tx, max_age, &base, &mut delete_paths)?;
}
if let Some(max_age) = gc_opts.max_src_age {
let max_age = now - max_age.as_secs();
Self::get_registry_items_to_clean_age(
&tx,
max_age,
REGISTRY_SRC_TABLE,
&base.src,
&mut delete_paths,
)?;
}
if let Some(max_age) = gc_opts.max_crate_age {
let max_age = now - max_age.as_secs();
Self::get_registry_items_to_clean_age(
&tx,
max_age,
REGISTRY_CRATE_TABLE,
&base.crate_dir,
&mut delete_paths,
)?;
}
if let Some(max_age) = gc_opts.max_git_db_age {
let max_age = now - max_age.as_secs();
Self::get_git_db_items_to_clean(&tx, max_age, &base, &mut delete_paths)?;
}
if let Some(max_age) = gc_opts.max_git_co_age {
let max_age = now - max_age.as_secs();
Self::get_git_co_items_to_clean(&tx, max_age, &base.git_co, &mut delete_paths)?;
}
if let Some(max_size) = gc_opts.max_crate_size {
Self::get_registry_items_to_clean_size(
&tx,
max_size,
REGISTRY_CRATE_TABLE,
&base.crate_dir,
&mut delete_paths,
)?;
}
if let Some(max_size) = gc_opts.max_src_size {
Self::get_registry_items_to_clean_size(
&tx,
max_size,
REGISTRY_SRC_TABLE,
&base.src,
&mut delete_paths,
)?;
}
if let Some(max_size) = gc_opts.max_git_size {
Self::get_git_items_to_clean_size(&tx, max_size, &base, &mut delete_paths)?;
}
if let Some(max_size) = gc_opts.max_download_size {
Self::get_registry_items_to_clean_size_both(&tx, max_size, &base, &mut delete_paths)?;
}
clean_ctx.remove_paths(&delete_paths)?;
if clean_ctx.dry_run {
tx.rollback()?;
} else {
tx.commit()?;
}
Ok(())
}
fn list_dir_names(path: &Path) -> CargoResult<Vec<String>> {
Self::read_dir_with_filter(path, &|entry| {
entry.file_type().map_or(false, |ty| ty.is_dir())
})
}
fn read_dir_with_filter(
path: &Path,
filter: &dyn Fn(&std::fs::DirEntry) -> bool,
) -> CargoResult<Vec<String>> {
let entries = match path.read_dir() {
Ok(e) => e,
Err(e) => {
if e.kind() == std::io::ErrorKind::NotFound {
return Ok(Vec::new());
} else {
return Err(
anyhow::Error::new(e).context(format!("failed to read path `{path:?}`"))
);
}
}
};
let names = entries
.filter_map(|entry| entry.ok())
.filter(|entry| filter(entry))
.filter_map(|entry| entry.file_name().into_string().ok())
.collect();
Ok(names)
}
#[tracing::instrument(skip(conn, gctx, base, delete_paths))]
fn sync_db_with_files(
conn: &Connection,
now: Timestamp,
gctx: &GlobalContext,
base: &BasePaths,
sync_size: bool,
delete_paths: &mut Vec<PathBuf>,
) -> CargoResult<()> {
debug!(target: "gc", "starting db sync");
Self::update_parent_for_missing_from_db(conn, now, REGISTRY_INDEX_TABLE, &base.index)?;
Self::update_parent_for_missing_from_db(conn, now, GIT_DB_TABLE, &base.git_db)?;
Self::update_db_for_removed(
conn,
REGISTRY_INDEX_TABLE,
"registry_id",
REGISTRY_CRATE_TABLE,
&base.crate_dir,
)?;
Self::update_db_for_removed(
conn,
REGISTRY_INDEX_TABLE,
"registry_id",
REGISTRY_SRC_TABLE,
&base.src,
)?;
Self::update_db_for_removed(conn, GIT_DB_TABLE, "git_id", GIT_CO_TABLE, &base.git_co)?;
Self::update_db_parent_for_removed_from_disk(
conn,
REGISTRY_INDEX_TABLE,
&base.index,
&[&base.crate_dir, &base.src],
delete_paths,
)?;
Self::update_db_parent_for_removed_from_disk(
conn,
GIT_DB_TABLE,
&base.git_db,
&[&base.git_co],
delete_paths,
)?;
Self::populate_untracked_crate(conn, now, &base.crate_dir)?;
Self::populate_untracked(
conn,
now,
gctx,
REGISTRY_INDEX_TABLE,
"registry_id",
REGISTRY_SRC_TABLE,
&base.src,
sync_size,
)?;
Self::populate_untracked(
conn,
now,
gctx,
GIT_DB_TABLE,
"git_id",
GIT_CO_TABLE,
&base.git_co,
sync_size,
)?;
if sync_size {
Self::update_null_sizes(
conn,
gctx,
REGISTRY_INDEX_TABLE,
"registry_id",
REGISTRY_SRC_TABLE,
&base.src,
)?;
Self::update_null_sizes(
conn,
gctx,
GIT_DB_TABLE,
"git_id",
GIT_CO_TABLE,
&base.git_co,
)?;
}
Ok(())
}
#[tracing::instrument(skip(conn, now, base_path))]
fn update_parent_for_missing_from_db(
conn: &Connection,
now: Timestamp,
parent_table_name: &str,
base_path: &Path,
) -> CargoResult<()> {
trace!(target: "gc", "checking for untracked parent to add to {parent_table_name}");
let names = Self::list_dir_names(base_path)?;
let mut stmt = conn.prepare_cached(&format!(
"INSERT INTO {parent_table_name} (name, timestamp)
VALUES (?1, ?2)
ON CONFLICT DO NOTHING",
))?;
for name in names {
stmt.execute(params![name, now])?;
}
Ok(())
}
#[tracing::instrument(skip(conn, base_path))]
fn update_db_for_removed(
conn: &Connection,
parent_table_name: &str,
id_column_name: &str,
table_name: &str,
base_path: &Path,
) -> CargoResult<()> {
trace!(target: "gc", "checking for db entries to remove from {table_name}");
let mut select_stmt = conn.prepare_cached(&format!(
"SELECT {table_name}.rowid, {parent_table_name}.name, {table_name}.name
FROM {parent_table_name}, {table_name}
WHERE {table_name}.{id_column_name} = {parent_table_name}.id",
))?;
let mut delete_stmt =
conn.prepare_cached(&format!("DELETE FROM {table_name} WHERE rowid = ?1"))?;
let mut rows = select_stmt.query([])?;
while let Some(row) = rows.next()? {
let rowid: i64 = row.get_unwrap(0);
let id_name: String = row.get_unwrap(1);
let name: String = row.get_unwrap(2);
if !base_path.join(id_name).join(name).exists() {
delete_stmt.execute([rowid])?;
}
}
Ok(())
}
#[tracing::instrument(skip(conn, base_path, child_base_paths, delete_paths))]
fn update_db_parent_for_removed_from_disk(
conn: &Connection,
parent_table_name: &str,
base_path: &Path,
child_base_paths: &[&Path],
delete_paths: &mut Vec<PathBuf>,
) -> CargoResult<()> {
trace!(target: "gc", "checking for db entries to remove from {parent_table_name}");
let mut select_stmt =
conn.prepare_cached(&format!("SELECT rowid, name FROM {parent_table_name}"))?;
let mut delete_stmt =
conn.prepare_cached(&format!("DELETE FROM {parent_table_name} WHERE rowid = ?1"))?;
let mut rows = select_stmt.query([])?;
while let Some(row) = rows.next()? {
let rowid: i64 = row.get_unwrap(0);
let id_name: String = row.get_unwrap(1);
if !base_path.join(&id_name).exists() {
delete_stmt.execute([rowid])?;
for child_base in child_base_paths {
let child_path = child_base.join(&id_name);
if child_path.exists() {
debug!(target: "gc", "removing orphaned path {child_path:?}");
delete_paths.push(child_path);
}
}
}
}
Ok(())
}
#[tracing::instrument(skip(conn, now, base_path))]
fn populate_untracked_crate(
conn: &Connection,
now: Timestamp,
base_path: &Path,
) -> CargoResult<()> {
trace!(target: "gc", "populating untracked crate files");
let mut insert_stmt = conn.prepare_cached(
"INSERT INTO registry_crate (registry_id, name, size, timestamp)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT DO NOTHING",
)?;
let index_names = Self::list_dir_names(&base_path)?;
for index_name in index_names {
let Some(id) = Self::id_from_name(conn, REGISTRY_INDEX_TABLE, &index_name)? else {
continue;
};
let index_path = base_path.join(index_name);
let crates = Self::read_dir_with_filter(&index_path, &|entry| {
entry.file_type().map_or(false, |ty| ty.is_file())
&& entry
.file_name()
.to_str()
.map_or(false, |name| name.ends_with(".crate"))
})?;
for crate_name in crates {
let size = paths::metadata(index_path.join(&crate_name))?.len();
insert_stmt.execute(params![id, crate_name, size, now])?;
}
}
Ok(())
}
#[tracing::instrument(skip(conn, now, gctx, base_path, populate_size))]
fn populate_untracked(
conn: &Connection,
now: Timestamp,
gctx: &GlobalContext,
id_table_name: &str,
id_column_name: &str,
table_name: &str,
base_path: &Path,
populate_size: bool,
) -> CargoResult<()> {
trace!(target: "gc", "populating untracked files for {table_name}");
let id_names = Self::list_dir_names(&base_path)?;
let mut select_stmt = conn.prepare_cached(&format!(
"SELECT 1 FROM {table_name}
WHERE {id_column_name} = ?1 AND name = ?2",
))?;
let mut insert_stmt = conn.prepare_cached(&format!(
"INSERT INTO {table_name} ({id_column_name}, name, size, timestamp)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT DO NOTHING",
))?;
let mut progress = Progress::with_style("Scanning", ProgressStyle::Ratio, gctx);
for id_name in id_names {
let Some(id) = Self::id_from_name(conn, id_table_name, &id_name)? else {
continue;
};
let index_path = base_path.join(id_name);
let names = Self::list_dir_names(&index_path)?;
let max = names.len();
for (i, name) in names.iter().enumerate() {
if select_stmt.exists(params![id, name])? {
continue;
}
let dir_path = index_path.join(name);
if !dir_path.is_dir() {
continue;
}
progress.tick(i, max, "")?;
let size = if populate_size {
Some(du(&dir_path, table_name)?)
} else {
None
};
insert_stmt.execute(params![id, name, size, now])?;
}
}
Ok(())
}
#[tracing::instrument(skip(conn, gctx, base_path))]
fn update_null_sizes(
conn: &Connection,
gctx: &GlobalContext,
parent_table_name: &str,
id_column_name: &str,
table_name: &str,
base_path: &Path,
) -> CargoResult<()> {
trace!(target: "gc", "updating NULL size information in {table_name}");
let mut null_stmt = conn.prepare_cached(&format!(
"SELECT {table_name}.rowid, {table_name}.name, {parent_table_name}.name
FROM {table_name}, {parent_table_name}
WHERE {table_name}.size IS NULL AND {table_name}.{id_column_name} = {parent_table_name}.id",
))?;
let mut update_stmt = conn.prepare_cached(&format!(
"UPDATE {table_name} SET size = ?1 WHERE rowid = ?2"
))?;
let mut progress = Progress::with_style("Scanning", ProgressStyle::Ratio, gctx);
let rows: Vec<_> = null_stmt
.query_map([], |row| {
Ok((row.get_unwrap(0), row.get_unwrap(1), row.get_unwrap(2)))
})?
.collect();
let max = rows.len();
for (i, row) in rows.into_iter().enumerate() {
let (rowid, name, id_name): (i64, String, String) = row?;
let path = base_path.join(id_name).join(name);
progress.tick(i, max, "")?;
let size = du(&path, table_name)?;
update_stmt.execute(params![size, rowid])?;
}
Ok(())
}
fn get_registry_items_to_clean_age(
conn: &Connection,
max_age: Timestamp,
table_name: &str,
base_path: &Path,
delete_paths: &mut Vec<PathBuf>,
) -> CargoResult<()> {
debug!(target: "gc", "cleaning {table_name} since {max_age:?}");
let mut stmt = conn.prepare_cached(&format!(
"DELETE FROM {table_name} WHERE timestamp < ?1
RETURNING registry_id, name"
))?;
let rows = stmt
.query_map(params![max_age], |row| {
let registry_id = row.get_unwrap(0);
let name: String = row.get_unwrap(1);
Ok((registry_id, name))
})?
.collect::<Result<Vec<_>, _>>()?;
let ids: Vec<_> = rows.iter().map(|r| r.0).collect();
let id_map = Self::get_id_map(conn, REGISTRY_INDEX_TABLE, &ids)?;
for (id, name) in rows {
let encoded_registry_name = &id_map[&id];
delete_paths.push(base_path.join(encoded_registry_name).join(name));
}
Ok(())
}
fn get_registry_items_to_clean_size(
conn: &Connection,
max_size: u64,
table_name: &str,
base_path: &Path,
delete_paths: &mut Vec<PathBuf>,
) -> CargoResult<()> {
debug!(target: "gc", "cleaning {table_name} till under {max_size:?}");
let total_size: u64 = conn.query_row(
&format!("SELECT coalesce(SUM(size), 0) FROM {table_name}"),
[],
|row| row.get(0),
)?;
if total_size <= max_size {
return Ok(());
}
let mut stmt = conn.prepare(&format!(
"DELETE FROM {table_name} WHERE rowid IN \
(SELECT x.rowid FROM \
(SELECT rowid, size, SUM(size) OVER \
(ORDER BY timestamp, name ROWS UNBOUNDED PRECEDING) AS running_amount \
FROM {table_name}) x \
WHERE coalesce(x.running_amount, 0) - x.size < ?1) \
RETURNING registry_id, name;"
))?;
let rows = stmt
.query_map(params![total_size - max_size], |row| {
let id = row.get_unwrap(0);
let name: String = row.get_unwrap(1);
Ok((id, name))
})?
.collect::<Result<Vec<_>, _>>()?;
let ids: Vec<_> = rows.iter().map(|r| r.0).collect();
let id_map = Self::get_id_map(conn, REGISTRY_INDEX_TABLE, &ids)?;
for (id, name) in rows {
let encoded_name = &id_map[&id];
delete_paths.push(base_path.join(encoded_name).join(name));
}
Ok(())
}
fn get_registry_items_to_clean_size_both(
conn: &Connection,
max_size: u64,
base: &BasePaths,
delete_paths: &mut Vec<PathBuf>,
) -> CargoResult<()> {
debug!(target: "gc", "cleaning download till under {max_size:?}");
let mut stmt = conn.prepare_cached(
"SELECT 1, registry_src.rowid, registry_src.name AS name, registry_index.name,
registry_src.size, registry_src.timestamp AS timestamp
FROM registry_src, registry_index
WHERE registry_src.registry_id = registry_index.id AND registry_src.size NOT NULL
UNION
SELECT 2, registry_crate.rowid, registry_crate.name AS name, registry_index.name,
registry_crate.size, registry_crate.timestamp AS timestamp
FROM registry_crate, registry_index
WHERE registry_crate.registry_id = registry_index.id
ORDER BY timestamp, name",
)?;
let mut delete_src_stmt =
conn.prepare_cached("DELETE FROM registry_src WHERE rowid = ?1")?;
let mut delete_crate_stmt =
conn.prepare_cached("DELETE FROM registry_crate WHERE rowid = ?1")?;
let rows = stmt
.query_map([], |row| {
Ok((
row.get_unwrap(0),
row.get_unwrap(1),
row.get_unwrap(2),
row.get_unwrap(3),
row.get_unwrap(4),
))
})?
.collect::<Result<Vec<(i64, i64, String, String, u64)>, _>>()?;
let mut total_size: u64 = rows.iter().map(|r| r.4).sum();
debug!(target: "gc", "total download cache size appears to be {total_size}");
for (table, rowid, name, index_name, size) in rows {
if total_size <= max_size {
break;
}
if table == 1 {
delete_paths.push(base.src.join(index_name).join(name));
delete_src_stmt.execute([rowid])?;
} else {
delete_paths.push(base.crate_dir.join(index_name).join(name));
delete_crate_stmt.execute([rowid])?;
}
total_size -= size;
}
Ok(())
}
fn get_git_items_to_clean_size(
conn: &Connection,
max_size: u64,
base: &BasePaths,
delete_paths: &mut Vec<PathBuf>,
) -> CargoResult<()> {
debug!(target: "gc", "cleaning git till under {max_size:?}");
let mut stmt = conn.prepare_cached("SELECT rowid, name, timestamp FROM git_db")?;
let mut git_info = stmt
.query_map([], |row| {
let rowid: i64 = row.get_unwrap(0);
let name: String = row.get_unwrap(1);
let timestamp: Timestamp = row.get_unwrap(2);
Ok((timestamp, rowid, None, name, 0))
})?
.collect::<Result<Vec<_>, _>>()?;
for info in &mut git_info {
let size = cargo_util::du(&base.git_db.join(&info.3), &[])?;
info.4 = size;
}
let mut stmt = conn.prepare_cached(
"SELECT git_checkout.rowid, git_db.name, git_checkout.name,
git_checkout.size, git_checkout.timestamp
FROM git_checkout, git_db
WHERE git_checkout.git_id = git_db.id AND git_checkout.size NOT NULL",
)?;
let git_co_rows = stmt
.query_map([], |row| {
let rowid = row.get_unwrap(0);
let db_name: String = row.get_unwrap(1);
let name = row.get_unwrap(2);
let size = row.get_unwrap(3);
let timestamp = row.get_unwrap(4);
Ok((timestamp, rowid, Some(db_name), name, size))
})?
.collect::<Result<Vec<_>, _>>()?;
git_info.extend(git_co_rows);
git_info.sort_by(|a, b| (b.0, &b.3).cmp(&(a.0, &a.3)));
let mut delete_db_stmt = conn.prepare_cached("DELETE FROM git_db WHERE rowid = ?1")?;
let mut delete_co_stmt =
conn.prepare_cached("DELETE FROM git_checkout WHERE rowid = ?1")?;
let mut total_size: u64 = git_info.iter().map(|r| r.4).sum();
debug!(target: "gc", "total git cache size appears to be {total_size}");
while let Some((_timestamp, rowid, db_name, name, size)) = git_info.pop() {
if total_size <= max_size {
break;
}
if let Some(db_name) = db_name {
delete_paths.push(base.git_co.join(db_name).join(name));
delete_co_stmt.execute([rowid])?;
total_size -= size;
} else {
total_size -= size;
delete_paths.push(base.git_db.join(&name));
delete_db_stmt.execute([rowid])?;
let mut i = 0;
while i < git_info.len() {
if git_info[i].2.as_deref() == Some(name.as_ref()) {
let (_, rowid, db_name, name, size) = git_info.remove(i);
delete_paths.push(base.git_co.join(db_name.unwrap()).join(name));
delete_co_stmt.execute([rowid])?;
total_size -= size;
} else {
i += 1;
}
}
}
}
Ok(())
}
fn get_registry_index_to_clean(
conn: &Connection,
max_age: Timestamp,
base: &BasePaths,
delete_paths: &mut Vec<PathBuf>,
) -> CargoResult<()> {
debug!(target: "gc", "cleaning index since {max_age:?}");
let mut stmt = conn.prepare_cached(
"DELETE FROM registry_index WHERE timestamp < ?1
RETURNING name",
)?;
let mut rows = stmt.query([max_age])?;
while let Some(row) = rows.next()? {
let name: String = row.get_unwrap(0);
delete_paths.push(base.index.join(&name));
delete_paths.push(base.src.join(&name));
delete_paths.push(base.crate_dir.join(&name));
}
Ok(())
}
fn get_git_co_items_to_clean(
conn: &Connection,
max_age: Timestamp,
base_path: &Path,
delete_paths: &mut Vec<PathBuf>,
) -> CargoResult<()> {
debug!(target: "gc", "cleaning git co since {max_age:?}");
let mut stmt = conn.prepare_cached(
"DELETE FROM git_checkout WHERE timestamp < ?1
RETURNING git_id, name",
)?;
let rows = stmt
.query_map(params![max_age], |row| {
let git_id = row.get_unwrap(0);
let name: String = row.get_unwrap(1);
Ok((git_id, name))
})?
.collect::<Result<Vec<_>, _>>()?;
let ids: Vec<_> = rows.iter().map(|r| r.0).collect();
let id_map = Self::get_id_map(conn, GIT_DB_TABLE, &ids)?;
for (id, name) in rows {
let encoded_git_name = &id_map[&id];
delete_paths.push(base_path.join(encoded_git_name).join(name));
}
Ok(())
}
fn get_git_db_items_to_clean(
conn: &Connection,
max_age: Timestamp,
base: &BasePaths,
delete_paths: &mut Vec<PathBuf>,
) -> CargoResult<()> {
debug!(target: "gc", "cleaning git db since {max_age:?}");
let mut stmt = conn.prepare_cached(
"DELETE FROM git_db WHERE timestamp < ?1
RETURNING name",
)?;
let mut rows = stmt.query([max_age])?;
while let Some(row) = rows.next()? {
let name: String = row.get_unwrap(0);
delete_paths.push(base.git_db.join(&name));
delete_paths.push(base.git_co.join(&name));
}
Ok(())
}
}
macro_rules! insert_or_update_parent {
($self:expr, $conn:expr, $table_name:expr, $timestamps_field:ident, $keys_field:ident, $encoded_name:ident) => {
let mut select_stmt = $conn.prepare_cached(concat!(
"SELECT id, timestamp FROM ",
$table_name,
" WHERE name = ?1"
))?;
let mut insert_stmt = $conn.prepare_cached(concat!(
"INSERT INTO ",
$table_name,
" (name, timestamp)
VALUES (?1, ?2)
ON CONFLICT DO UPDATE SET timestamp=excluded.timestamp
RETURNING id",
))?;
let mut update_stmt = $conn.prepare_cached(concat!(
"UPDATE ",
$table_name,
" SET timestamp = ?1 WHERE id = ?2"
))?;
for (parent, new_timestamp) in std::mem::take(&mut $self.$timestamps_field) {
trace!(target: "gc",
concat!("insert ", $table_name, " {:?} {}"),
parent,
new_timestamp
);
let mut rows = select_stmt.query([parent.$encoded_name])?;
let id = if let Some(row) = rows.next()? {
let id: ParentId = row.get_unwrap(0);
let timestamp: Timestamp = row.get_unwrap(1);
if timestamp < new_timestamp - UPDATE_RESOLUTION {
update_stmt.execute(params![new_timestamp, id])?;
}
id
} else {
insert_stmt.query_row(params![parent.$encoded_name, new_timestamp], |row| {
row.get(0)
})?
};
match $self.$keys_field.entry(parent.$encoded_name) {
hash_map::Entry::Occupied(o) => {
assert_eq!(*o.get(), id);
}
hash_map::Entry::Vacant(v) => {
v.insert(id);
}
}
}
return Ok(());
};
}
#[derive(Debug)]
pub struct DeferredGlobalLastUse {
registry_keys: HashMap<InternedString, ParentId>,
git_keys: HashMap<InternedString, ParentId>,
registry_index_timestamps: HashMap<RegistryIndex, Timestamp>,
registry_crate_timestamps: HashMap<RegistryCrate, Timestamp>,
registry_src_timestamps: HashMap<RegistrySrc, Timestamp>,
git_db_timestamps: HashMap<GitDb, Timestamp>,
git_checkout_timestamps: HashMap<GitCheckout, Timestamp>,
save_err_has_warned: bool,
now: Timestamp,
}
impl DeferredGlobalLastUse {
pub fn new() -> DeferredGlobalLastUse {
DeferredGlobalLastUse {
registry_keys: HashMap::default(),
git_keys: HashMap::default(),
registry_index_timestamps: HashMap::default(),
registry_crate_timestamps: HashMap::default(),
registry_src_timestamps: HashMap::default(),
git_db_timestamps: HashMap::default(),
git_checkout_timestamps: HashMap::default(),
save_err_has_warned: false,
now: now(),
}
}
pub fn is_empty(&self) -> bool {
self.registry_index_timestamps.is_empty()
&& self.registry_crate_timestamps.is_empty()
&& self.registry_src_timestamps.is_empty()
&& self.git_db_timestamps.is_empty()
&& self.git_checkout_timestamps.is_empty()
}
fn clear(&mut self) {
self.registry_index_timestamps.clear();
self.registry_crate_timestamps.clear();
self.registry_src_timestamps.clear();
self.git_db_timestamps.clear();
self.git_checkout_timestamps.clear();
}
pub fn mark_registry_index_used(&mut self, registry_index: RegistryIndex) {
self.mark_registry_index_used_stamp(registry_index, None);
}
pub fn mark_registry_crate_used(&mut self, registry_crate: RegistryCrate) {
self.mark_registry_crate_used_stamp(registry_crate, None);
}
pub fn mark_registry_src_used(&mut self, registry_src: RegistrySrc) {
self.mark_registry_src_used_stamp(registry_src, None);
}
pub fn mark_git_checkout_used(&mut self, git_checkout: GitCheckout) {
self.mark_git_checkout_used_stamp(git_checkout, None);
}
pub fn mark_registry_index_used_stamp(
&mut self,
registry_index: RegistryIndex,
timestamp: Option<&SystemTime>,
) {
let timestamp = timestamp.map_or(self.now, to_timestamp);
self.registry_index_timestamps
.insert(registry_index, timestamp);
}
pub fn mark_registry_crate_used_stamp(
&mut self,
registry_crate: RegistryCrate,
timestamp: Option<&SystemTime>,
) {
let timestamp = timestamp.map_or(self.now, to_timestamp);
let index = RegistryIndex {
encoded_registry_name: registry_crate.encoded_registry_name,
};
self.registry_index_timestamps.insert(index, timestamp);
self.registry_crate_timestamps
.insert(registry_crate, timestamp);
}
pub fn mark_registry_src_used_stamp(
&mut self,
registry_src: RegistrySrc,
timestamp: Option<&SystemTime>,
) {
let timestamp = timestamp.map_or(self.now, to_timestamp);
let index = RegistryIndex {
encoded_registry_name: registry_src.encoded_registry_name,
};
self.registry_index_timestamps.insert(index, timestamp);
self.registry_src_timestamps.insert(registry_src, timestamp);
}
pub fn mark_git_checkout_used_stamp(
&mut self,
git_checkout: GitCheckout,
timestamp: Option<&SystemTime>,
) {
let timestamp = timestamp.map_or(self.now, to_timestamp);
let db = GitDb {
encoded_git_name: git_checkout.encoded_git_name,
};
self.git_db_timestamps.insert(db, timestamp);
self.git_checkout_timestamps.insert(git_checkout, timestamp);
}
#[tracing::instrument(skip_all)]
pub fn save(&mut self, tracker: &mut GlobalCacheTracker) -> CargoResult<()> {
trace!(target: "gc", "saving last-use data");
if self.is_empty() {
return Ok(());
}
let tx = tracker.conn.transaction()?;
self.insert_registry_index_from_cache(&tx)?;
self.insert_git_db_from_cache(&tx)?;
self.insert_registry_crate_from_cache(&tx)?;
self.insert_registry_src_from_cache(&tx)?;
self.insert_git_checkout_from_cache(&tx)?;
tx.commit()?;
trace!(target: "gc", "last-use save complete");
Ok(())
}
pub fn save_no_error(&mut self, gctx: &GlobalContext) {
if let Err(e) = self.save_with_gctx(gctx) {
self.clear();
if !self.save_err_has_warned {
if is_silent_error(&e) && gctx.shell().verbosity() != Verbosity::Verbose {
tracing::warn!("failed to save last-use data: {e:?}");
} else {
crate::display_warning_with_error(
"failed to save last-use data\n\
This may prevent cargo from accurately tracking what is being \
used in its global cache. This information is used for \
automatically removing unused data in the cache.",
&e,
&mut gctx.shell(),
);
self.save_err_has_warned = true;
}
}
}
}
fn save_with_gctx(&mut self, gctx: &GlobalContext) -> CargoResult<()> {
let mut tracker = gctx.global_cache_tracker()?;
self.save(&mut tracker)
}
fn insert_registry_index_from_cache(&mut self, conn: &Connection) -> CargoResult<()> {
insert_or_update_parent!(
self,
conn,
"registry_index",
registry_index_timestamps,
registry_keys,
encoded_registry_name
);
}
fn insert_git_db_from_cache(&mut self, conn: &Connection) -> CargoResult<()> {
insert_or_update_parent!(
self,
conn,
"git_db",
git_db_timestamps,
git_keys,
encoded_git_name
);
}
fn insert_registry_crate_from_cache(&mut self, conn: &Connection) -> CargoResult<()> {
let registry_crate_timestamps = std::mem::take(&mut self.registry_crate_timestamps);
for (registry_crate, timestamp) in registry_crate_timestamps {
trace!(target: "gc", "insert registry crate {registry_crate:?} {timestamp}");
let registry_id = self.registry_id(conn, registry_crate.encoded_registry_name)?;
let mut stmt = conn.prepare_cached(
"INSERT INTO registry_crate (registry_id, name, size, timestamp)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT DO UPDATE SET timestamp=excluded.timestamp
WHERE timestamp < ?5
",
)?;
stmt.execute(params![
registry_id,
registry_crate.crate_filename,
registry_crate.size,
timestamp,
timestamp - UPDATE_RESOLUTION
])?;
}
Ok(())
}
fn insert_registry_src_from_cache(&mut self, conn: &Connection) -> CargoResult<()> {
let registry_src_timestamps = std::mem::take(&mut self.registry_src_timestamps);
for (registry_src, timestamp) in registry_src_timestamps {
trace!(target: "gc", "insert registry src {registry_src:?} {timestamp}");
let registry_id = self.registry_id(conn, registry_src.encoded_registry_name)?;
let mut stmt = conn.prepare_cached(
"INSERT INTO registry_src (registry_id, name, size, timestamp)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT DO UPDATE SET timestamp=excluded.timestamp
WHERE timestamp < ?5
",
)?;
stmt.execute(params![
registry_id,
registry_src.package_dir,
registry_src.size,
timestamp,
timestamp - UPDATE_RESOLUTION
])?;
}
Ok(())
}
fn insert_git_checkout_from_cache(&mut self, conn: &Connection) -> CargoResult<()> {
let git_checkout_timestamps = std::mem::take(&mut self.git_checkout_timestamps);
for (git_checkout, timestamp) in git_checkout_timestamps {
let git_id = self.git_id(conn, git_checkout.encoded_git_name)?;
let mut stmt = conn.prepare_cached(
"INSERT INTO git_checkout (git_id, name, size, timestamp)
VALUES (?1, ?2, ?3, ?4)
ON CONFLICT DO UPDATE SET timestamp=excluded.timestamp
WHERE timestamp < ?5",
)?;
stmt.execute(params![
git_id,
git_checkout.short_name,
git_checkout.size,
timestamp,
timestamp - UPDATE_RESOLUTION
])?;
}
Ok(())
}
fn registry_id(
&mut self,
conn: &Connection,
encoded_registry_name: InternedString,
) -> CargoResult<ParentId> {
match self.registry_keys.get(&encoded_registry_name) {
Some(i) => Ok(*i),
None => {
let Some(id) = GlobalCacheTracker::id_from_name(
conn,
REGISTRY_INDEX_TABLE,
&encoded_registry_name,
)?
else {
bail!(
"expected registry_index {encoded_registry_name} to exist, but wasn't found"
);
};
self.registry_keys.insert(encoded_registry_name, id);
Ok(id)
}
}
}
fn git_id(
&mut self,
conn: &Connection,
encoded_git_name: InternedString,
) -> CargoResult<ParentId> {
match self.git_keys.get(&encoded_git_name) {
Some(i) => Ok(*i),
None => {
let Some(id) =
GlobalCacheTracker::id_from_name(conn, GIT_DB_TABLE, &encoded_git_name)?
else {
bail!("expected git_db {encoded_git_name} to exist, but wasn't found")
};
self.git_keys.insert(encoded_git_name, id);
Ok(id)
}
}
}
}
fn to_timestamp(t: &SystemTime) -> Timestamp {
t.duration_since(SystemTime::UNIX_EPOCH)
.expect("invalid clock")
.as_secs()
}
#[expect(
clippy::disallowed_methods,
reason = "testing only, no reason for config support"
)]
fn now() -> Timestamp {
match std::env::var("__CARGO_TEST_LAST_USE_NOW") {
Ok(now) => now.parse().unwrap(),
Err(_) => to_timestamp(&SystemTime::now()),
}
}
pub fn is_silent_error(e: &anyhow::Error) -> bool {
if let Some(e) = e.downcast_ref::<rusqlite::Error>() {
if matches!(
e.sqlite_error_code(),
Some(ErrorCode::CannotOpen | ErrorCode::ReadOnly)
) {
return true;
}
}
false
}
#[tracing::instrument]
fn du_git_checkout(path: &Path) -> CargoResult<u64> {
cargo_util::du(&path, &["!.git"])
}
fn du(path: &Path, table_name: &str) -> CargoResult<u64> {
if table_name == GIT_CO_TABLE {
du_git_checkout(path)
} else {
cargo_util::du(&path, &[])
}
}