diff --git a/Cargo.lock b/Cargo.lock index 178038bbf5..7ab9378853 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4302,7 +4302,6 @@ dependencies = [ "reqwest", "rpds", "rustls 0.23.18", - "scc", "scopeguard", "send-future", "serde", @@ -6077,15 +6076,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "scc" -version = "2.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22b2d775fb28f245817589471dd49c5edf64237f4a19d10ce9a92ff4651a27f4" -dependencies = [ - "sdd", -] - [[package]] name = "schannel" version = "0.1.23" @@ -6126,12 +6116,6 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "621e3680f3e07db4c9c2c3fb07c6223ab2fab2e54bd3c04c3ae037990f428c32" -[[package]] -name = "sdd" -version = "3.0.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "584e070911c7017da6cb2eb0788d09f43d789029b5877d3e5ecc8acf86ceee21" - [[package]] name = "sec1" version = "0.3.0" diff --git a/Cargo.toml b/Cargo.toml index 6453ecd3c7..9d7904a787 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -160,7 +160,6 @@ rustc-hash = "1.1.0" rustls = { version = "0.23.16", default-features = false } rustls-pemfile = "2" rustls-pki-types = "1.11" -scc = "2.3.4" scopeguard = "1.1" sysinfo = "0.29.2" sd-notify = "0.4.1" diff --git a/pageserver/Cargo.toml b/pageserver/Cargo.toml index 0e29517afa..5c5bab0642 100644 --- a/pageserver/Cargo.toml +++ b/pageserver/Cargo.toml @@ -52,7 +52,6 @@ rand.workspace = true range-set-blaze = { version = "0.1.16", features = ["alloc"] } regex.workspace = true rustls.workspace = true -scc.workspace = true scopeguard.workspace = true send-future.workspace = true serde.workspace = true diff --git a/pageserver/src/page_cache.rs b/pageserver/src/page_cache.rs index d64f6d3c99..bac78faaae 100644 --- a/pageserver/src/page_cache.rs +++ b/pageserver/src/page_cache.rs @@ -67,8 +67,8 @@ //! mapping is automatically removed and the slot is marked free. //! -use scc::HashMap; -use scc::hash_map::Entry; +use std::collections::HashMap; +use std::collections::hash_map::Entry; use std::sync::atomic::{AtomicU8, AtomicU64, AtomicUsize, Ordering}; use std::sync::{Arc, Weak}; use std::time::Duration; @@ -194,7 +194,7 @@ impl SlotInner { } pub struct PageCache { - immutable_page_map: std::sync::Arc::>, + immutable_page_maps: [std::sync::RwLock>; 16], /// The actual buffers with their metadata. slots: Box<[Slot]>, @@ -204,7 +204,103 @@ pub struct PageCache { /// Index of the next candidate to evict, for the Clock replacement algorithm. /// This is interpreted modulo the page cache size. next_evict_slot: AtomicUsize, +} +impl PageCache { + /// Helper function to determine the shard index based on the low 4 bits of the u32 in the key tuple. + fn shard_index(_file_id: &FileId, blkno: u32) -> usize { + (blkno & 0xF) as usize + } + + /// Search for a page in the cache using the given search key. + /// + /// Returns the slot index, if any. + /// + /// NOTE: We don't hold any lock on the mapping on return, so the slot might + /// get recycled for an unrelated page immediately after this function + /// returns. The caller is responsible for re-checking that the slot still + /// contains the page with the same key before using it. + /// + fn search_mapping(&self, cache_key: &CacheKey) -> Option { + match cache_key { + CacheKey::ImmutableFilePage { file_id, blkno } => { + let shard_idx = Self::shard_index(file_id, *blkno); + let map = self.immutable_page_maps[shard_idx].read().unwrap(); + Some(*map.get(&(*file_id, *blkno))?) + } + } + } + + /// + /// Remove mapping for given key. + /// + fn remove_mapping(&self, old_key: &CacheKey) { + match old_key { + CacheKey::ImmutableFilePage { file_id, blkno } => { + let shard_idx = Self::shard_index(file_id, *blkno); + let mut map = self.immutable_page_maps[shard_idx].write().unwrap(); + map.remove(&(*file_id, *blkno)) + .expect("could not find old key in mapping"); + } + } + } + + /// + /// Insert mapping for given key. + /// + /// If a mapping already existed for the given key, returns the slot index + /// of the existing mapping and leaves it untouched. + fn try_insert_mapping(&self, new_key: &CacheKey, slot_idx: usize) -> Option { + match new_key { + CacheKey::ImmutableFilePage { file_id, blkno } => { + let shard_idx = Self::shard_index(file_id, *blkno); + let mut map = self.immutable_page_maps[shard_idx].write().unwrap(); + match map.entry((*file_id, *blkno)) { + Entry::Occupied(entry) => Some(*entry.get()), + Entry::Vacant(entry) => { + entry.insert(slot_idx); + None + } + } + } + } + } + + /// Initialize a new page cache + /// + /// This should be called only once at page server startup. + fn new(num_pages: usize) -> Self { + assert!(num_pages > 0, "page cache size must be > 0"); + + // We could use Vec::leak here, but that potentially also leaks + // uninitialized reserved capacity. With into_boxed_slice and Box::leak + // this is avoided. + let page_buffer = IoBufferMut::with_capacity_zeroed(num_pages * PAGE_SZ).leak(); + + let slots = page_buffer + .chunks_exact_mut(PAGE_SZ) + .map(|chunk| { + // SAFETY: Each chunk has `PAGE_SZ` (8192) bytes, greater than 512, still aligned. + let buf = unsafe { IoPageSlice::new_unchecked(chunk.try_into().unwrap()) }; + + Slot { + inner: tokio::sync::RwLock::new(SlotInner { + key: None, + buf, + permit: std::sync::Mutex::new(Weak::new()), + }), + usage_count: AtomicU8::new(0), + } + }) + .collect(); + + Self { + immutable_page_maps: Default::default(), + slots, + next_evict_slot: AtomicUsize::new(0), + pinned_slots: Arc::new(tokio::sync::Semaphore::new(num_pages)), + } + } } struct PinnedSlotsPermit { @@ -467,58 +563,6 @@ impl PageCache { } } - // - // Section 3: Mapping functions - // - - /// Search for a page in the cache using the given search key. - /// - /// Returns the slot index, if any. - /// - /// NOTE: We don't hold any lock on the mapping on return, so the slot might - /// get recycled for an unrelated page immediately after this function - /// returns. The caller is responsible for re-checking that the slot still - /// contains the page with the same key before using it. - /// - fn search_mapping(&self, cache_key: &CacheKey) -> Option { - match cache_key { - CacheKey::ImmutableFilePage { file_id, blkno } => { - Some(*self.immutable_page_map.get(&(*file_id, *blkno))?) - } - } - } - - /// - /// Remove mapping for given key. - /// - fn remove_mapping(&self, old_key: &CacheKey) { - match old_key { - CacheKey::ImmutableFilePage { file_id, blkno } => { - self.immutable_page_map.remove(&(*file_id, *blkno)) - .expect("could not find old key in mapping"); - } - } - } - - /// - /// Insert mapping for given key. - /// - /// If a mapping already existed for the given key, returns the slot index - /// of the existing mapping and leaves it untouched. - fn try_insert_mapping(&self, new_key: &CacheKey, slot_idx: usize) -> Option { - match new_key { - CacheKey::ImmutableFilePage { file_id, blkno } => { - match self.immutable_page_map.entry((*file_id, *blkno)) { - Entry::Occupied(entry) => Some(*entry.get()), - Entry::Vacant(entry) => { - entry.insert_entry(slot_idx); - None - } - } - } - } - } - // // Section 4: Misc internal helpers // @@ -590,40 +634,5 @@ impl PageCache { } } - /// Initialize a new page cache - /// - /// This should be called only once at page server startup. - fn new(num_pages: usize) -> Self { - assert!(num_pages > 0, "page cache size must be > 0"); - - // We could use Vec::leak here, but that potentially also leaks - // uninitialized reserved capacity. With into_boxed_slice and Box::leak - // this is avoided. - let page_buffer = IoBufferMut::with_capacity_zeroed(num_pages * PAGE_SZ).leak(); - - let slots = page_buffer - .chunks_exact_mut(PAGE_SZ) - .map(|chunk| { - // SAFETY: Each chunk has `PAGE_SZ` (8192) bytes, greater than 512, still aligned. - let buf = unsafe { IoPageSlice::new_unchecked(chunk.try_into().unwrap()) }; - - Slot { - inner: tokio::sync::RwLock::new(SlotInner { - key: None, - buf, - permit: std::sync::Mutex::new(Weak::new()), - }), - usage_count: AtomicU8::new(0), - } - }) - .collect(); - - Self { - immutable_page_map: Default::default(), - slots, - next_evict_slot: AtomicUsize::new(0), - pinned_slots: Arc::new(tokio::sync::Semaphore::new(num_pages)), - } - } }