ci: bound the sccache Azure container with a lifecycle retention rule
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 40
- Forks
- 9
- Avg merge
- 4d 20h
- Merged PRs (30d)
- 59
Description
Problem
#125 puts the rustc compile cache in an Azure blob container. Nothing in that design ever removes an entry, so the container grows without bound.
This is not a tunable that was left unset. SCCACHE_CACHE_SIZE is parsed once, at src/config.rs:1159, and its only consumer is DiskCacheConfig.size (:1182), which reaches DiskCache::new and nothing else. AzureCacheConfig has no size field at all, and the cloud storage impl hardcodes both size queries to None:
async fn current_size(&self) -> Result<Option<u64>> { Ok(None) } // cache/cache.rs:320
async fn max_size(&self) -> Result<Option<u64>> { Ok(None) } // cache/cache.rs:324
There is no TTL, no LRU, and no size accounting for azblob anywhere in sccache v0.17.0. The only expiry-related settings are SCCACHE_REDIS_EXPIRATION and SCCACHE_MEMCACHED_EXPIRATION, both specific to backends that implement TTL server-side. Upstream's own position is explicit — docs/MultiLevel.md describes the cloud tier as "Unlimited, cold storage". Retention is the container's responsibility.
Growth is driven by generations, not by build volume. Every toolchain bump, dependency bump, or edit to a widely-depended-on crate invalidates a set of keys and writes a fresh set; the superseded set is never read and never removed.
Why the obvious rule is the wrong one
The tempting rule is daysAfterModificationGreaterThan. It would be a bug here.
sccache never touches an object it reads. RemoteStorage::get is a single operator.read() (cache/cache.rs:230-242), which OpenDAL turns into a plain HTTP GET; the hit path in compiler/compiler.rs never calls put(). Combined with #125's write gating — only push and merge_group populate — an entry's last-modified time is fixed at the moment it was first written and never changes again, no matter how many runs read it daily.
So a modification-based rule deletes the hottest entries in the cache on a fixed schedule. It would evict exactly the artifacts that every PR depends on.
The correct condition is daysAfterLastAccessTimeGreaterThan, which requires last access time tracking on the storage account. With tracking on, Get Blob is an access operation and does refresh the timestamp, so an entry that CI keeps hitting stays alive and only genuinely unused entries age out. That is the LRU behaviour we want.
One trap worth stating plainly: if the rule is applied without enabling tracking first, Azure does not error. LastAccessTime is unset, so it substitutes the date tracking was enabled, and the rule silently degrades into the fixed global expiry described above. Enablement is not optional setup — it is what makes the rule correct.
Proposed change
Two actions on the storage account; no workflow change.
1. Enable last access time tracking.
az storage account blob-service-properties update \
--resource-group <resource-group> \
--account-name <storage-account> \
--enable-last-access-tracking true
2. Add a delete-only lifecycle rule scoped to this cache's prefix.
{
"rules": [
{
"name": "sccache-lru",
"enabled": true,
"type": "Lifecycle",
"definition": {
"filters": {
"blobTypes": ["blockBlob"],
"prefixMatch": ["cache/rocm-cli/"]
},
"actions": {
"baseBlob": {
"delete": { "daysAfterLastAccessTimeGreaterThan": 30 }
}
}
}
}
]
}
prefixMatch starts with the container name, so cache/rocm-cli/ is container cache plus the rocm-cli/ key prefix — it covers both the rocm-cli/build-and-test and rocm-cli/windows-build-and-test lanes while leaving anything else in the container alone. The trailing slash matters: without it the filter would also match sibling containers whose names merely begin with cache.
On the window
30 days is a starting value, not a measured one. The constraint is that PRs are READ_ONLY — a PR that misses cannot repopulate what it missed, so the window has to comfortably exceed the longest realistic gap between a main build and a PR that would benefit from it. Note that nightly.yml does not set RUSTC_WRAPPER, so nightly runs do not refresh access times; only ci.yml traffic keeps entries warm. 30 days absorbs a holiday-length quiet period with margin. It is cheap to tighten later once there is a real size curve to look at.
Worth measuring the live container before settling on a number:
az storage blob list --container-name cache --prefix rocm-cli/ \
--account-name <storage-account> --num-results '*' \
--query "length(@)"
Cost and caveats
- Delete operations under a lifecycle policy are free, and the policies themselves are free. The only recurring cost is last-access tracking, billed under "other operations" at most once per object per day.
- Tracking granularity is 24 hours — only the first read of a blob in a rolling 24-hour window updates its timestamp. Irrelevant at a day-granularity retention window.
- The policy engine runs once per day, and a new or edited policy can take up to 24 hours to take effect. A large account may need more than one run to process everything, so expect convergence to lag rather than being immediate.
- Immediately after tracking is enabled, every pre-existing blob reports the enablement date as its access time. Nothing is deleted for the first N days, so there is no mass-deletion risk at rollout.
- If soft delete is enabled on the account, policy deletes only move blobs into the soft-deleted state and they keep costing storage for the retention period. Worth checking before assuming the rule reclaims space.
- Deleting
.sccache_checkis harmless. sccache writes that sentinel un-normalized (cache/cache.rs:254), so it sits inside the prefix rather than under a hash shard and the rule will match it.check()treatsNotFoundon read as success (:260), and theREAD_WRITEpath recreates it;READ_ONLYreturns before the write probe (:277) and never needs it. No shard-enumeration workaround is required — which is just as well, since a rule accepts at most 10 prefixes and the hash space has 16 first-level shards. - Lifecycle management requires a general-purpose v2, premium block blob, or Blob Storage account. Delete is supported on all three.
Status
Last access time tracking is already enabled on the storage account, ahead of the rest. It is non-destructive on its own — it stamps LastAccessTime and deletes nothing — and enabling it early means that by the time the delete rule is applied, entries have real access history instead of all ageing from a single enablement instant.
Remaining steps, in order:
Enable last access time tracking— done.- Merge #125 (open; the cache is not yet populated by
maintraffic). - Let
mainaccumulate a few weeks of real traffic — the quantity being sized is generation turnover, which a single reading right after merge cannot show. - Measure the container, then pick the window from the size curve rather than from the placeholder above.
- Apply the delete rule.
Only step 5 is destructive, and it is the only one that needs a measured number.
Scope
Storage-account configuration only. Nothing here changes ci.yml, and the retention comment #125 carries at build-and-test stays accurate.
Follow-up to #125, which defers retention explicitly.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with follow-up #125, ci.yml, nightly.yml, and docs/MultiLevel.md to confirm the cache traffic and read-only behavior. Measure the cache prefix with the stated az storage blob list command, choose a retention window from observed usage, and apply the Azure last-access lifecycle rule only after tracking is enabled. Done means the rule is scoped to rocm-cli/ and removes only blobs idle beyond the selected window.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- azure, rust
- Domain
- cloud, infrastructure
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 48/100