Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions docs/commands/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ When invoked without arguments, kache prints `--help` and exits — it does not
| `kache list [<crate>] [--sort <field>] [--no-pager]` | List cache entries or show details for one crate |
| `kache why-miss <crate>` | Diagnose why a crate keeps missing the cache |
| `kache report [--format <fmt>] [--since <dur>] [--root <path>] [--output <path>] [--top <n>]` | Build report in text / json / trace / markdown / github format |
| `kache gc [--max-age <dur>]` | Evict entries by LRU or age |
| `kache gc [--max-age <dur> \| --stale-schema]` | Evict entries by LRU, age, or obsolete key schema |
| `kache purge [--crate-name <name>]` | Wipe entire cache or entries for one crate |
| `kache clean [-n \| --dry-run] [-y \| --yes]` | Find and remove `target/` directories (interactive; `-n` previews, `-y` removes all non-interactively) |
| `kache sync [flags]` | Sync local cache with a configured remote |
Expand Down Expand Up @@ -210,17 +210,20 @@ Saving a manifest is cheap; it is safe to run at the end of a CI build alongside
## `kache gc`

```sh
kache gc [--max-age <duration>]
kache gc [--max-age <duration> | --stale-schema]
```

Runs garbage collection. Without `--max-age`, applies the automatic policy: opt-in age retention from `KACHE_GC_MAX_AGE_HOURS` (default `0`, disabled) first, then duplicate and size-pressure eviction against the recomputed physical store size. Size pressure fires above `KACHE_MAX_SIZE` and evicts down to 90% of that cap. Duplicate cleanup is bounded by the same target and removes an entry only when backfilled metadata proves positive physical reclaim; unknown legacy entries are kept. The command reports each policy separately. With `--max-age`, runs only the requested age policy regardless of store size.
Runs garbage collection. Without an option, applies the automatic policy: opt-in age retention from `KACHE_GC_MAX_AGE_HOURS` (default `0`, disabled) first, then duplicate and size-pressure eviction against the recomputed physical store size. Size pressure fires above `KACHE_MAX_SIZE` and evicts down to 90% of that cap. Duplicate cleanup is bounded by the same target and removes an entry only when backfilled metadata proves positive physical reclaim; unknown legacy entries are kept. The command reports each policy separately. With `--max-age`, runs only the requested age policy regardless of store size.

During a rolling upgrade, both modes verify GC policy v2 before mutation and use a `gc_v2` command that older daemons reject without evicting. If the daemon cannot provide v2 semantics and per-policy reporting, the command reruns the requested policy locally under the same cross-process GC lock.
`--stale-schema` is an explicit key-upgrade cleanup. It retains entries created by the running cache-key recipe and removes entries from older recipes, including legacy entries whose recipe was not recorded. Ordinary upgrades and automatic GC do not remove those legacy entries. This mode runs locally under the cross-process GC lock and cannot be combined with `--max-age`.

During a rolling upgrade, the automatic and `--max-age` modes verify GC policy v2 before mutation and use a `gc_v2` command that older daemons reject without evicting. If the daemon cannot provide v2 semantics and per-policy reporting, the command reruns the requested policy locally under the same cross-process GC lock. The stale-schema mode always runs locally because older daemons do not know the running binary's key recipe.

```sh
kache gc # automatic configured policy (age first, then pressure)
kache gc --max-age 30d # remove anything unused for 30 days
kache gc --max-age 7d # aggressive cleanup before a release build
kache gc --stale-schema # reclaim entries orphaned by cache-key recipe changes
```

If `clean_incremental` is enabled (the default), GC removes tracked incremental compilation directories that previous wrapper invocations registered. Normal cached invocations also remove the current Cargo incremental dir eagerly. Adaptive and forced preserve-incremental compiles use separate state that kache never registers for GC; `cargo clean` still removes it with the target profile.
Expand Down
24 changes: 23 additions & 1 deletion src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2491,7 +2491,28 @@ pub fn run_gc_local(config: &Config, mode: GcMode) -> Result<()> {
}

/// Run garbage collection via the daemon.
pub fn gc(config: &Config, max_age_hours: Option<u64>) -> Result<()> {
pub fn gc(config: &Config, max_age_hours: Option<u64>, stale_schema: bool) -> Result<()> {
if stale_schema {
let store = Store::open(config)?;
let _gc_lock = match store.try_gc_lock()? {
Some(lock) => lock,
None => {
println!("Another GC is already running; skipping.");
return Ok(());
}
};
let stats = store.evict_stale_key_schemas(crate::cache_key::CACHE_KEY_VERSION)?;
println!(
"Stale-schema GC:{}\nCurrent key schema: {}.",
describe_eviction(&stats, false),
crate::cache_key::CACHE_KEY_VERSION,
);
let total_size = store.total_size()?;
let entry_count = store.entry_count()?;
println!("Store: {} ({} entries)", ByteSize(total_size), entry_count);
return Ok(());
}

let mode = GcMode::from_env();
if mode == GcMode::Background {
let sleep_secs = std::env::var("KACHE_AUTO_GC_RETRY_DELAY_SECS")
Expand Down Expand Up @@ -8194,6 +8215,7 @@ mod tests {
) -> crate::store::EntryMeta {
crate::store::EntryMeta {
cache_key: "k".to_string(),
key_schema: crate::cache_key::CACHE_KEY_VERSION,
crate_name: "c".to_string(),
crate_types: crate_types.iter().map(|v| (*v).to_string()).collect(),
files: Vec::new(),
Expand Down
15 changes: 11 additions & 4 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,11 +102,15 @@ enum Commands {
no_pager: bool,
},

/// Run garbage collection (LRU eviction)
/// Run garbage collection
Gc {
/// Evict entries older than this duration (e.g. 7d, 24h)
#[arg(long)]
#[arg(long, conflicts_with = "stale_schema")]
max_age: Option<String>,

/// Remove entries from old or unrecorded cache-key schemas
#[arg(long)]
stale_schema: bool,
},

/// Wipe entire cache or entries for a specific crate
Expand Down Expand Up @@ -499,7 +503,10 @@ fn main() -> Result<()> {
sort,
no_pager,
}) => cli::list(&config, crate_name.as_deref(), &sort, no_pager),
Some(Commands::Gc { max_age }) => {
Some(Commands::Gc {
max_age,
stale_schema,
}) => {
let hours = max_age
.as_deref()
.map(|value| {
Expand All @@ -510,7 +517,7 @@ fn main() -> Result<()> {
})
})
.transpose()?;
cli::gc(&config, hours)
cli::gc(&config, hours, stale_schema)
}
Some(Commands::Purge { crate_name }) => cli::purge(&config, crate_name.as_deref()),
Some(Commands::Clean { dry_run, yes }) => cli::clean(dry_run, yes),
Expand Down
Loading
Loading