Skip to content

Commit 1e91ac5

Browse files
committed
multilevel: a chain with any writable level is writable
MultiLevelStorage::check() started from ReadWrite and demoted the composite to ReadOnly as soon as any level reported ReadOnly. The server then wraps the whole storage in ReadOnlyStorage, so every cache write is refused before any write-error policy is consulted, including writes to writable levels and preprocessor cache updates. That inverts the documented semantics: MultiLevel.md promises that read-only levels are "automatically skipped during writes, regardless of write policy", and put() already implements exactly that per-level skipping. The practical fallout is that the documented read-only fallback topology (fast writable local disk in front of a read-only shared remote, e.g. SCCACHE_S3_RW_MODE=READ_ONLY or an azure SAS token without write permission) silently stops populating the local disk cache entirely. Aggregate in the other direction: the composite is writable when any level is writable, and read-only only when every level is read-only. The existing single-level read-only test still passes; new tests cover mixed chains in both orders and the all-read-only chain. Fixes #2773 Signed-off-by: Steven Noonan <steven@edera.dev>
1 parent 7cd4f2c commit 1e91ac5

2 files changed

Lines changed: 74 additions & 2 deletions

File tree

src/cache/multilevel.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -877,14 +877,22 @@ impl Storage for MultiLevelStorage {
877877
}
878878

879879
async fn check(&self) -> Result<CacheMode> {
880-
let mut result = CacheMode::ReadWrite;
880+
// The composite is writable when any level is writable: put()
881+
// already skips read-only levels on writes, so a read-only level
882+
// must not demote the whole chain (e.g. a writable local disk in
883+
// front of a read-only shared remote). Only a chain in which
884+
// every level is read-only is itself read-only.
885+
let mut result = CacheMode::ReadOnly;
886+
if self.levels.is_empty() {
887+
return Ok(CacheMode::ReadWrite);
888+
}
881889
for (idx, level) in self.levels.iter().enumerate() {
882890
match level.check().await {
883891
Ok(CacheMode::ReadOnly) => {
884-
result = CacheMode::ReadOnly;
885892
debug!("Cache level {} is read-only", idx);
886893
}
887894
Ok(CacheMode::ReadWrite) => {
895+
result = CacheMode::ReadWrite;
888896
trace!("Cache level {} is read-write", idx);
889897
}
890898
Err(e) => {
@@ -893,6 +901,7 @@ impl Storage for MultiLevelStorage {
893901
}
894902
}
895903
}
904+
debug!("Multi-level cache mode: {:?}", result);
896905
Ok(result)
897906
}
898907

src/cache/multilevel_test.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -970,6 +970,69 @@ fn test_readonly_level_in_check() {
970970
});
971971
}
972972

973+
#[test]
974+
fn test_mixed_readonly_chain_is_readwrite_in_check() {
975+
// A chain that contains a writable level must report ReadWrite even
976+
// when other levels are read-only: put() skips read-only levels, so a
977+
// read-only remote behind a writable local disk (the documented
978+
// "read-only fallback" topology) must not demote the whole cache to
979+
// read-only. Regression test for #2773.
980+
let runtime = RuntimeBuilder::new_current_thread()
981+
.enable_all()
982+
.build()
983+
.unwrap();
984+
985+
// Writable L0, read-only L1
986+
let rw_l0 = Arc::new(InMemoryStorage::new());
987+
let ro_l1 = Arc::new(ReadOnlyStorage(Arc::new(InMemoryStorage::new())));
988+
let storage = MultiLevelStorage::new(vec![
989+
rw_l0 as Arc<dyn Storage>,
990+
ro_l1 as Arc<dyn Storage>,
991+
]);
992+
runtime.block_on(async {
993+
match storage.check().await.unwrap() {
994+
CacheMode::ReadWrite => {} // Expected
995+
_ => panic!("Writable L0 + read-only L1 should be ReadWrite"),
996+
}
997+
});
998+
999+
// Read-only L0, writable L1: still writable (put() skips L0)
1000+
let ro_l0 = Arc::new(ReadOnlyStorage(Arc::new(InMemoryStorage::new())));
1001+
let rw_l1 = Arc::new(InMemoryStorage::new());
1002+
let storage = MultiLevelStorage::new(vec![
1003+
ro_l0 as Arc<dyn Storage>,
1004+
rw_l1 as Arc<dyn Storage>,
1005+
]);
1006+
runtime.block_on(async {
1007+
match storage.check().await.unwrap() {
1008+
CacheMode::ReadWrite => {} // Expected
1009+
_ => panic!("Read-only L0 + writable L1 should be ReadWrite"),
1010+
}
1011+
});
1012+
}
1013+
1014+
#[test]
1015+
fn test_all_readonly_chain_is_readonly_in_check() {
1016+
// Only a chain in which EVERY level is read-only is itself read-only.
1017+
let runtime = RuntimeBuilder::new_current_thread()
1018+
.enable_all()
1019+
.build()
1020+
.unwrap();
1021+
1022+
let ro_l0 = Arc::new(ReadOnlyStorage(Arc::new(InMemoryStorage::new())));
1023+
let ro_l1 = Arc::new(ReadOnlyStorage(Arc::new(InMemoryStorage::new())));
1024+
let storage = MultiLevelStorage::new(vec![
1025+
ro_l0 as Arc<dyn Storage>,
1026+
ro_l1 as Arc<dyn Storage>,
1027+
]);
1028+
runtime.block_on(async {
1029+
match storage.check().await.unwrap() {
1030+
CacheMode::ReadOnly => {} // Expected
1031+
_ => panic!("All read-only levels should be ReadOnly"),
1032+
}
1033+
});
1034+
}
1035+
9731036
#[test]
9741037
fn test_sequential_read_order() {
9751038
// Test that reads happen sequentially (L0, L1, L2, ...), not in parallel

0 commit comments

Comments
 (0)