cachelib/rust/readonly/readonly.rs (165 lines of code) (raw):

/* * Copyright (c) Facebook, Inc. and its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ use std::os::unix::ffi::OsStrExt; use std::path::Path; use cxx::let_cxx_string; use thiserror::Error; #[derive(Debug, Error)] #[error("Failed to attach ReadOnlySharedCacheView: {cxx_exn}")] pub struct FailedToAttachError { #[from] cxx_exn: cxx::Exception, } #[derive(Debug, Error)] #[error("Invalid remote item handle: {cxx_exn}")] pub struct InvalidHandleError { #[from] cxx_exn: cxx::Exception, } #[cxx::bridge(namespace = "facebook::rust::cachelib")] mod ffi { unsafe extern "C++" { include!("cachelib/rust/readonly/readonly.h"); #[namespace = "facebook::cachelib"] type ReadOnlySharedCacheView; fn ro_cache_view_attach( cache_dir: &CxxString, ) -> Result<UniquePtr<ReadOnlySharedCacheView>>; fn ro_cache_view_attach_at_address( cache_dir: &CxxString, addr: usize, ) -> Result<UniquePtr<ReadOnlySharedCacheView>>; fn ro_cache_view_get_shm_mapping_address(cache: &ReadOnlySharedCacheView) -> usize; fn ro_cache_view_get_item_ptr_from_offset( cache: &ReadOnlySharedCacheView, offset: usize, ) -> Result<*const u8>; } } pub struct ReadOnlySharedCacheView { cache_view: cxx::UniquePtr<ffi::ReadOnlySharedCacheView>, } impl ReadOnlySharedCacheView { pub fn new(cache_dir: impl AsRef<Path>) -> Result<Self, FailedToAttachError> { let_cxx_string!(cache_dir = cache_dir.as_ref().as_os_str().as_bytes()); let cache_view = ffi::ro_cache_view_attach(&cache_dir)?; Ok(Self { cache_view }) } pub fn new_at_address( cache_dir: impl AsRef<Path>, addr: *mut std::ffi::c_void, ) -> Result<Self, FailedToAttachError> { let_cxx_string!(cache_dir = cache_dir.as_ref().as_os_str().as_bytes()); let cache_view = ffi::ro_cache_view_attach_at_address(&cache_dir, addr as usize)?; Ok(Self { cache_view }) } /// Return a byte slice from a (offset, len) pair within the cache. /// (offset, len) must be retrieved using [get_remote_handle] from an LruCacheHandle. pub fn get_bytes_from_offset<'a>( &'a self, offset: usize, len: usize, ) -> Result<&'a [u8], InvalidHandleError> { let item_ptr = ffi::ro_cache_view_get_item_ptr_from_offset(&*self.cache_view, offset)?; Ok(unsafe { std::slice::from_raw_parts(item_ptr, len) }) } pub fn shm_mapping_address(&self) -> usize { let addr = ffi::ro_cache_view_get_shm_mapping_address(&*self.cache_view); if addr == 0 { // This shouldn't happen--the whole point of ReadOnlySharedCacheView // is that it's a view into a cache's shared memory, and // getShmMappingAddress should return nullptr only when the cache is // not using shared memory. panic!("ReadOnlySharedCacheView returned null shm_mapping_address") } else { addr } } } #[cfg(test)] mod test { use super::*; use std::path::PathBuf; use std::time::Duration; use anyhow::Result; use bytes::Bytes; use cachelib::*; use fbinit::FacebookInit; use tempdir::TempDir; fn create_temp_dir(dir_prefix: &str) -> TempDir { TempDir::new(dir_prefix).expect("failed to create temp dir") } fn create_shared_cache(fb: FacebookInit, cache_directory: PathBuf) { let config = LruCacheConfig::new(128 * 1024 * 1024) .set_shrinker(ShrinkMonitor { shrinker_type: ShrinkMonitorType::ResidentSize { max_process_size_gib: 16, min_process_size_gib: 1, }, interval: Duration::new(1, 0), max_resize_per_iteration_percent: 10, max_removed_percent: 90, strategy: RebalanceStrategy::LruTailAge { age_difference_ratio: 0.1, min_retained_slabs: 1, }, }) .set_pool_resizer(PoolResizeConfig { interval: Duration::new(1, 0), slabs_per_iteration: 100, strategy: RebalanceStrategy::LruTailAge { age_difference_ratio: 0.1, min_retained_slabs: 1, }, }) .set_cache_dir(cache_directory) .set_pool_rebalance(PoolRebalanceConfig { interval: Duration::new(1, 0), strategy: RebalanceStrategy::LruTailAge { age_difference_ratio: 0.1, min_retained_slabs: 1, }, }); if let Err(e) = init_cache(fb, config) { panic!("{}", e); } } #[fbinit::test] fn test_readonly_shared_cache(fb: FacebookInit) -> Result<()> { let temp_dir = create_temp_dir("test_shared_cache"); create_shared_cache(fb, temp_dir.path().into()); // Set value in original cache let pool = get_or_create_pool("find_pool_by_name", 4 * 1024 * 1024)?; let value = b"I am a fish"; pool.set(b"test", Bytes::from(value.as_ref()))?; let test_handle = pool.get_handle(b"test")?.unwrap(); let remote_handle = test_handle.get_remote_handle()?; // Get value from read-only cache let ro_cache_view = ReadOnlySharedCacheView::new(&temp_dir.path())?; let slice = ro_cache_view .get_bytes_from_offset(remote_handle.get_offset(), remote_handle.get_length())?; let reader_bytes = Bytes::copy_from_slice(slice); // Verify that value is the same assert_eq!( reader_bytes, Bytes::from(b"I am a fish".as_ref()), "Data does not match!" ); Ok(()) } #[test] fn test_non_existent_cache_dir() { let temp_dir = create_temp_dir("test_non_existent_cache_dir"); let mut path = temp_dir.path().to_owned(); path.push("this_dir_does_not_exist"); match ReadOnlySharedCacheView::new(&path) { Ok(_) => panic!("ReadOnlySharedCacheView::new returned Ok for non-existent dir"), Err(FailedToAttachError { .. }) => {} } } }