diff --git a/Cargo.lock b/Cargo.lock index 8dbfc11d3..7bec2ae72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -510,7 +510,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-targets 0.52.0", + "windows-targets 0.52.5", ] [[package]] @@ -606,6 +606,13 @@ version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" +[[package]] +name = "clean-loom" +version = "0.40.0" +dependencies = [ + "loom", +] + [[package]] name = "close-err" version = "1.0.2" @@ -1366,6 +1373,20 @@ dependencies = [ "slab", ] +[[package]] +name = "generator" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186014d53bc231d0090ef8d6f03e0920c54d85a5ed22f4f2f74315ec56cf83fb" +dependencies = [ + "cc", + "cfg-if", + "libc", + "log", + "rustversion", + "windows 0.54.0", +] + [[package]] name = "generic-array" version = "0.14.7" @@ -1629,7 +1650,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.51.1", ] [[package]] @@ -1882,6 +1903,19 @@ version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "loom" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "419e0dc8046cb947daa77eb95ae174acfbddb7673b4151f56d1eed8e93fbfaca" +dependencies = [ + "cfg-if", + "generator", + "scoped-tls", + "tracing", + "tracing-subscriber", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -3034,6 +3068,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "scoped-tls" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" + [[package]] name = "scopeguard" version = "1.2.0" @@ -3501,7 +3541,7 @@ dependencies = [ "uuid 1.5.0", "walkdir", "whoami", - "windows", + "windows 0.51.1", ] [[package]] @@ -3617,7 +3657,7 @@ dependencies = [ "tracing", "unix_mode", "url", - "windows", + "windows 0.51.1", ] [[package]] @@ -3667,7 +3707,7 @@ dependencies = [ "tower", "tracing", "url", - "windows", + "windows 0.51.1", "winfsp", "winfsp-sys", ] @@ -3722,7 +3762,7 @@ dependencies = [ "tonic-build", "tracing", "url", - "windows", + "windows 0.51.1", "winfsp", "winfsp-sys", ] @@ -5543,10 +5583,20 @@ version = "0.51.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca229916c5ee38c2f2bc1e9d8f04df975b4bd93f9955dc69fabb5d91270045c9" dependencies = [ - "windows-core", + "windows-core 0.51.1", "windows-targets 0.48.5", ] +[[package]] +name = "windows" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9252e5725dbed82865af151df558e754e4a3c2c30818359eb17465f1346a1b49" +dependencies = [ + "windows-core 0.54.0", + "windows-targets 0.52.5", +] + [[package]] name = "windows-core" version = "0.51.1" @@ -5556,6 +5606,25 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "windows-core" +version = "0.54.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12661b9c89351d684a50a8a643ce5f608e20243b9fb84687800163429f161d65" +dependencies = [ + "windows-result", + "windows-targets 0.52.5", +] + +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.5", +] + [[package]] name = "windows-sys" version = "0.45.0" @@ -5606,17 +5675,18 @@ dependencies = [ [[package]] name = "windows-targets" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" dependencies = [ - "windows_aarch64_gnullvm 0.52.0", - "windows_aarch64_msvc 0.52.0", - "windows_i686_gnu 0.52.0", - "windows_i686_msvc 0.52.0", - "windows_x86_64_gnu 0.52.0", - "windows_x86_64_gnullvm 0.52.0", - "windows_x86_64_msvc 0.52.0", + "windows_aarch64_gnullvm 0.52.5", + "windows_aarch64_msvc 0.52.5", + "windows_i686_gnu 0.52.5", + "windows_i686_gnullvm", + "windows_i686_msvc 0.52.5", + "windows_x86_64_gnu 0.52.5", + "windows_x86_64_gnullvm 0.52.5", + "windows_x86_64_msvc 0.52.5", ] [[package]] @@ -5633,9 +5703,9 @@ checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] name = "windows_aarch64_gnullvm" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" +checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" [[package]] name = "windows_aarch64_msvc" @@ -5651,9 +5721,9 @@ checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] name = "windows_aarch64_msvc" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" +checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" [[package]] name = "windows_i686_gnu" @@ -5669,9 +5739,15 @@ checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] name = "windows_i686_gnu" -version = "0.52.0" +version = "0.52.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" +checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" [[package]] name = "windows_i686_msvc" @@ -5687,9 +5763,9 @@ checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" +checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" [[package]] name = "windows_x86_64_gnu" @@ -5705,9 +5781,9 @@ checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" +checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" [[package]] name = "windows_x86_64_gnullvm" @@ -5723,9 +5799,9 @@ checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" +checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" [[package]] name = "windows_x86_64_msvc" @@ -5741,9 +5817,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.52.0" +version = "0.52.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" +checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" [[package]] name = "winfsp" @@ -5756,7 +5832,7 @@ dependencies = [ "static_assertions", "thiserror", "widestring", - "windows", + "windows 0.51.1", "winfsp-sys", ] diff --git a/Cargo.toml b/Cargo.toml index 87eefc882..96f3275cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ members = [ "crates/spk-solve", "crates/spk-solve/crates/*", "crates/spk-storage", - "crates/spk", + "crates/spk", "clean-loom", ] resolver = "2" diff --git a/clean-loom/Cargo.toml b/clean-loom/Cargo.toml new file mode 100644 index 000000000..1c76538c9 --- /dev/null +++ b/clean-loom/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "clean-loom" +authors.workspace = true +edition.workspace = true +version.workspace = true + +[dependencies] +loom = "0.7" + +[lints] +workspace = true diff --git a/clean-loom/src/main.rs b/clean-loom/src/main.rs new file mode 100644 index 000000000..086e7d462 --- /dev/null +++ b/clean-loom/src/main.rs @@ -0,0 +1,189 @@ +use std::collections::BTreeSet; + +use loom::sync::{Arc, Mutex}; +use loom::thread; + +#[test] +fn simulate_existing_clean_behavior() { + loom::model(|| { + // Start out with `1` already as garbage. + let tags = Arc::new(Mutex::new(BTreeSet::::from_iter([2, 3]))); + let objects = Arc::new(Mutex::new(BTreeSet::::from_iter([1, 2, 3]))); + + let writer_tags = Arc::clone(&tags); + let writer_objects = Arc::clone(&objects); + let writer_thread = thread::spawn(move || { + // This simulates the current behavior of writing objects where + // the underlying objects are written first and the parent objects + // written second, e.g., blob first and manifest later. + let mut lock = writer_objects.lock().unwrap(); + lock.insert(4); + drop(lock); + let mut lock = writer_tags.lock().unwrap(); + lock.insert(4); + }); + + let cleaner_tags = Arc::clone(&tags); + let cleaner_objects = Arc::clone(&objects); + let cleaner_thread = thread::spawn(move || { + let lock = cleaner_objects.lock().unwrap(); + let objects_snapshot = (*lock).clone(); + drop(lock); + let lock = cleaner_tags.lock().unwrap(); + let tags_snapshot = (*lock).clone(); + drop(lock); + + let garbage_objects = objects_snapshot.difference(&tags_snapshot); + for obj in garbage_objects { + let mut lock = cleaner_objects.lock().unwrap(); + lock.remove(obj); + } + }); + + cleaner_thread.join().unwrap(); + writer_thread.join().unwrap(); + + let lock = objects.lock().unwrap(); + assert_eq!(*lock, BTreeSet::::from_iter([2, 3, 4])); + }); +} + +#[test] +fn simulate_proposed_clean_behavior() { + loom::model(|| { + // Start out with `1` already as garbage. + let tags = Arc::new(Mutex::new(BTreeSet::::from_iter([2, 3]))); + let objects = Arc::new(Mutex::new(BTreeSet::::from_iter([1, 2, 3]))); + let staged = Arc::new(Mutex::new(BTreeSet::::new())); + + let writer_tags = Arc::clone(&tags); + let writer_objects = Arc::clone(&objects); + let writer_staged = Arc::clone(&staged); + let writer_thread = thread::spawn(move || { + // Before writing any objects, a writer must create a hard reference + // to them by "staging them": + let mut lock = writer_staged.lock().unwrap(); + lock.insert(4); + drop(lock); + // Now it is safe to add to objects. + let mut lock = writer_objects.lock().unwrap(); + lock.insert(4); + drop(lock); + let mut lock = writer_tags.lock().unwrap(); + lock.insert(4); + drop(lock); + // With the tag written, it is now safe to unstage the object. + let mut lock = writer_staged.lock().unwrap(); + lock.remove(&4); + }); + + let cleaner_tags = Arc::clone(&tags); + let cleaner_objects = Arc::clone(&objects); + let cleaner_staged = Arc::clone(&staged); + let cleaner_thread = thread::spawn(move || { + let lock = cleaner_objects.lock().unwrap(); + let objects_snapshot = (*lock).clone(); + drop(lock); + // We hold the "staged" lock while deleting objects so anything + // trying to stage something has to wait. + let staged_lock = cleaner_staged.lock().unwrap(); + { + // We also hold the "staged" lock while reading the tags to + // prevent the interleaving where: + // - in thread 1: 4 hasn't been added to tags yet + // - in thread 2: tags snapshot is captured (without 4) + // - in thread 1: 4 is added to tags and removed from staged + // - in thread 2: staged is locked but is empty, allowing 4 + // to be deleted + let lock = cleaner_tags.lock().unwrap(); + let tags_snapshot = (*lock).clone(); + drop(lock); + let garbage_objects = objects_snapshot + .difference(&tags_snapshot) + .copied() + .collect::>(); + // We're not allowed to delete anything that is staged. + let to_delete = garbage_objects.difference(&staged_lock); + for obj in to_delete { + let mut lock = cleaner_objects.lock().unwrap(); + lock.remove(obj); + } + } + }); + + cleaner_thread.join().unwrap(); + writer_thread.join().unwrap(); + + let lock = objects.lock().unwrap(); + assert_eq!(*lock, BTreeSet::::from_iter([2, 3, 4])); + }); +} + +#[test] +fn simulate_proposed_clean_behavior_version_2() { + loom::model(|| { + // Start out with `1` already as garbage. + let tags = Arc::new(Mutex::new(BTreeSet::::from_iter([2, 3]))); + let objects = Arc::new(Mutex::new(BTreeSet::::from_iter([1, 2, 3]))); + let staged = Arc::new(Mutex::new(BTreeSet::::new())); + + let writer_tags = Arc::clone(&tags); + let writer_objects = Arc::clone(&objects); + let writer_staged = Arc::clone(&staged); + let writer_thread = thread::spawn(move || { + // Before writing any objects, a writer must create a hard reference + // to them by "staging them": + let mut lock = writer_staged.lock().unwrap(); + lock.insert(4); + drop(lock); + // Now it is safe to add to objects. + let mut lock = writer_objects.lock().unwrap(); + lock.insert(4); + drop(lock); + let mut lock = writer_tags.lock().unwrap(); + lock.insert(4); + // In this behavior, 4 is not unstaged here. In practice, it would + // be marked as "committed" and pruned later to give a reasonable + // amount of time to elapse to avoid races. Any new writers of the + // same object would add a new entry to staged to keep it alive + // longer. + }); + + let cleaner_tags = Arc::clone(&tags); + let cleaner_objects = Arc::clone(&objects); + let cleaner_staged = Arc::clone(&staged); + let cleaner_thread = thread::spawn(move || { + let lock = cleaner_objects.lock().unwrap(); + let objects_snapshot = (*lock).clone(); + drop(lock); + let lock = cleaner_tags.lock().unwrap(); + let tags_snapshot = (*lock).clone(); + drop(lock); + let garbage_objects = objects_snapshot + .difference(&tags_snapshot) + .copied() + .collect::>(); + // We hold the "staged" lock while deleting objects so anything + // trying to stage something has to wait. + let staged_lock = cleaner_staged.lock().unwrap(); + { + // We're not allowed to delete anything that is staged. + let to_delete = garbage_objects.difference(&staged_lock); + for obj in to_delete { + let mut lock = cleaner_objects.lock().unwrap(); + lock.remove(obj); + } + } + }); + + cleaner_thread.join().unwrap(); + writer_thread.join().unwrap(); + + let lock = objects.lock().unwrap(); + assert_eq!(*lock, BTreeSet::::from_iter([2, 3, 4])); + }); +} + +fn main() { + println!("Hello, world!"); +}