[pve-devel] [PATCH v2 proxmox 5/7] cache: add new crate 'proxmox-shared-cache'
Lukas Wagner
l.wagner at proxmox.com
Thu Sep 28 13:50:10 CEST 2023
This crate contains a file-backed cache with expiration logic.
The cache should be safe to be accessed from multiple processes at
once.
The cache stores values in a directory, based on the key.
E.g. key "foo" results in a file 'foo.json' in the given base
directory. If a new value is set, the file is atomically replaced.
The JSON file also contains some metadata, namely 'added_at' and
'expire_in' - they are used for cache expiration.
Note: This cache is not suited to applications that
- Might want to cache huge amounts of data, and/or access the cache
very frequently (due to the overhead of JSON de/serialization)
- Require arbitrary keys - right now, keys are limited by
SAFE_ID_REGEX
The cache was developed for the use in pvestatd, in order to cache
e.g. storage plugin status. There, these limitations do not really
play any role.
Signed-off-by: Lukas Wagner <l.wagner at proxmox.com>
---
Cargo.toml | 1 +
proxmox-shared-cache/Cargo.toml | 18 +
proxmox-shared-cache/debian/changelog | 5 +
proxmox-shared-cache/debian/control | 53 ++
proxmox-shared-cache/debian/copyright | 18 +
proxmox-shared-cache/debian/debcargo.toml | 7 +
proxmox-shared-cache/examples/performance.rs | 113 +++++
proxmox-shared-cache/src/lib.rs | 485 +++++++++++++++++++
8 files changed, 700 insertions(+)
create mode 100644 proxmox-shared-cache/Cargo.toml
create mode 100644 proxmox-shared-cache/debian/changelog
create mode 100644 proxmox-shared-cache/debian/control
create mode 100644 proxmox-shared-cache/debian/copyright
create mode 100644 proxmox-shared-cache/debian/debcargo.toml
create mode 100644 proxmox-shared-cache/examples/performance.rs
create mode 100644 proxmox-shared-cache/src/lib.rs
diff --git a/Cargo.toml b/Cargo.toml
index e334ac1..7b1e8e3 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -22,6 +22,7 @@ members = [
"proxmox-schema",
"proxmox-section-config",
"proxmox-serde",
+ "proxmox-shared-cache",
"proxmox-shared-memory",
"proxmox-sortable-macro",
"proxmox-subscription",
diff --git a/proxmox-shared-cache/Cargo.toml b/proxmox-shared-cache/Cargo.toml
new file mode 100644
index 0000000..ada1a12
--- /dev/null
+++ b/proxmox-shared-cache/Cargo.toml
@@ -0,0 +1,18 @@
+[package]
+name = "proxmox-shared-cache"
+version = "0.1.0"
+authors.workspace = true
+edition.workspace = true
+license.workspace = true
+repository.workspace = true
+exclude.workspace = true
+description = "A cache that can be used from multiple processes simultaneously"
+
+[dependencies]
+anyhow.workspace = true
+proxmox-sys = { workspace = true, features = ["timer"] }
+proxmox-time.workspace = true
+proxmox-schema = { workspace = true, features = ["api-types"]}
+serde_json = { workspace = true, features = ["raw_value"] }
+serde = { workspace = true, features = ["derive"]}
+nix.workspace = true
diff --git a/proxmox-shared-cache/debian/changelog b/proxmox-shared-cache/debian/changelog
new file mode 100644
index 0000000..54d39f5
--- /dev/null
+++ b/proxmox-shared-cache/debian/changelog
@@ -0,0 +1,5 @@
+rust-proxmox-shared-cache (0.1.0-1) unstable; urgency=medium
+
+ * initial Debian package
+
+ -- Proxmox Support Team <support at proxmox.com> Thu, 04 May 2023 08:40:38 +0200
diff --git a/proxmox-shared-cache/debian/control b/proxmox-shared-cache/debian/control
new file mode 100644
index 0000000..c8f0d8e
--- /dev/null
+++ b/proxmox-shared-cache/debian/control
@@ -0,0 +1,53 @@
+Source: rust-proxmox-shared-cache
+Section: rust
+Priority: optional
+Build-Depends: debhelper (>= 12),
+ dh-cargo (>= 25),
+ cargo:native <!nocheck>,
+ rustc:native <!nocheck>,
+ libstd-rust-dev <!nocheck>,
+ librust-anyhow-1+default-dev <!nocheck>,
+ librust-nix-0.26+default-dev (>= 0.26.1-~~) <!nocheck>,
+ librust-proxmox-schema-2+api-types-dev <!nocheck>,
+ librust-proxmox-schema-2+default-dev <!nocheck>,
+ librust-proxmox-sys-0.5+default-dev <!nocheck>,
+ librust-proxmox-sys-0.5+timer-dev <!nocheck>,
+ librust-proxmox-time-1+default-dev (>= 1.1.4-~~) <!nocheck>,
+ librust-serde-1+default-dev <!nocheck>,
+ librust-serde-1+derive-dev <!nocheck>,
+ librust-serde-json-1+default-dev <!nocheck>,
+ librust-serde-json-1+raw-value-dev <!nocheck>
+Maintainer: Proxmox Support Team <support at proxmox.com>
+Standards-Version: 4.6.1
+Vcs-Git: https://salsa.debian.org/rust-team/debcargo-conf.git [src/proxmox-shared-cache]
+Vcs-Browser: https://salsa.debian.org/rust-team/debcargo-conf/tree/master/src/proxmox-shared-cache
+X-Cargo-Crate: proxmox-shared-cache
+Rules-Requires-Root: no
+
+Package: librust-proxmox-shared-cache-dev
+Architecture: any
+Multi-Arch: same
+Depends:
+ ${misc:Depends},
+ librust-anyhow-1+default-dev,
+ librust-nix-0.26+default-dev (>= 0.26.1-~~),
+ librust-proxmox-schema-2+api-types-dev,
+ librust-proxmox-schema-2+default-dev,
+ librust-proxmox-sys-0.5+default-dev,
+ librust-proxmox-sys-0.5+timer-dev,
+ librust-proxmox-time-1+default-dev (>= 1.1.4-~~),
+ librust-serde-1+default-dev,
+ librust-serde-1+derive-dev,
+ librust-serde-json-1+default-dev,
+ librust-serde-json-1+raw-value-dev
+Provides:
+ librust-proxmox-shared-cache+default-dev (= ${binary:Version}),
+ librust-proxmox-shared-cache-0-dev (= ${binary:Version}),
+ librust-proxmox-shared-cache-0+default-dev (= ${binary:Version}),
+ librust-proxmox-shared-cache-0.1-dev (= ${binary:Version}),
+ librust-proxmox-shared-cache-0.1+default-dev (= ${binary:Version}),
+ librust-proxmox-shared-cache-0.1.0-dev (= ${binary:Version}),
+ librust-proxmox-shared-cache-0.1.0+default-dev (= ${binary:Version})
+Description: Cache implementations - Rust source code
+ This package contains the source for the Rust proxmox-shared-cache crate,
+ packaged by debcargo for use with cargo and dh-cargo.
diff --git a/proxmox-shared-cache/debian/copyright b/proxmox-shared-cache/debian/copyright
new file mode 100644
index 0000000..0d9eab3
--- /dev/null
+++ b/proxmox-shared-cache/debian/copyright
@@ -0,0 +1,18 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+
+Files:
+ *
+Copyright: 2019 - 2023 Proxmox Server Solutions GmbH <support at proxmox.com>
+License: AGPL-3.0-or-later
+ This program is free software: you can redistribute it and/or modify it under
+ the terms of the GNU Affero General Public License as published by the Free
+ Software Foundation, either version 3 of the License, or (at your option) any
+ later version.
+ .
+ This program is distributed in the hope that it will be useful, but WITHOUT
+ ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+ FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+ details.
+ .
+ You should have received a copy of the GNU Affero General Public License along
+ with this program. If not, see <https://www.gnu.org/licenses/>.
diff --git a/proxmox-shared-cache/debian/debcargo.toml b/proxmox-shared-cache/debian/debcargo.toml
new file mode 100644
index 0000000..14ad800
--- /dev/null
+++ b/proxmox-shared-cache/debian/debcargo.toml
@@ -0,0 +1,7 @@
+overlay = "."
+crate_src_path = ".."
+maintainer = "Proxmox Support Team <support at proxmox.com>"
+
+[source]
+#vcs_git = "git://git.proxmox.com/git/proxmox.git"
+#vcs_browser = "https://git.proxmox.com/?p=proxmox.git"
diff --git a/proxmox-shared-cache/examples/performance.rs b/proxmox-shared-cache/examples/performance.rs
new file mode 100644
index 0000000..54a9bf9
--- /dev/null
+++ b/proxmox-shared-cache/examples/performance.rs
@@ -0,0 +1,113 @@
+use proxmox_shared_cache::SharedCache;
+use proxmox_sys::fs::CreateOptions;
+use serde_json::Value;
+use std::time::{Duration, Instant};
+
+fn main() {
+ let options = CreateOptions::new()
+ .owner(nix::unistd::Uid::effective())
+ .group(nix::unistd::Gid::effective())
+ .perm(nix::sys::stat::Mode::from_bits_truncate(0o755));
+
+ let cache = SharedCache::new("/tmp/pmx-cache", options).unwrap();
+
+ let mut keys = Vec::new();
+
+ for i in 0..100000 {
+ keys.push(format!("key_{i}"));
+ }
+
+ let data = serde_json::json!({
+ "member1": "foo",
+ "member2": "foo",
+ "member3": "foo",
+ "member4": "foo",
+ "member5": "foo",
+ "member5": "foo",
+ "member6": "foo",
+ "member7": "foo",
+ "member8": "foo",
+ "array": [10, 20, 30, 40, 50],
+ "object": {
+ "member1": "foo",
+ "member2": "foo",
+ "member3": "foo",
+ "member4": "foo",
+ "member5": "foo",
+ "member5": "foo",
+ "member6": "foo",
+ "member7": "foo",
+ "member8": "foo",
+ }
+ });
+
+ // #####################################
+ let before = Instant::now();
+
+ for key in &keys {
+ cache.set(key, &data, None).expect("could not insert value");
+ }
+
+ let time = Instant::now() - before;
+ let time_per_op = time / keys.len() as u32;
+ println!(
+ "inserting {len} keys took {time:?} ({time_per_op:?} per key)",
+ len = keys.len(),
+ );
+
+ // #####################################
+ let before = Instant::now();
+ for key in &keys {
+ let _: Option<Value> = cache.get(key).expect("could not get value");
+ }
+
+ let time = Instant::now() - before;
+ let time_per_op = time / keys.len() as u32;
+ println!(
+ "getting {len} unexpired keys took {time:?} ({time_per_op:?} per key)",
+ len = keys.len(),
+ );
+
+ // #####################################
+ let before = Instant::now();
+ for key in &keys {
+ cache
+ .set(key, &data, Some(0))
+ .expect("could not insert value");
+ }
+
+ let time = Instant::now() - before;
+ let time_per_op = time / keys.len() as u32;
+ println!(
+ "updating {len} keys took {time:?} ({time_per_op:?} per key)",
+ len = keys.len(),
+ );
+
+ std::thread::sleep(Duration::from_secs(1));
+
+ // #####################################
+ let before = Instant::now();
+ for key in &keys {
+ let _: Option<Value> = cache.get(key).expect("could not get value");
+ }
+
+ let time = Instant::now() - before;
+ let time_per_op = time / keys.len() as u32;
+ println!(
+ "getting {len} expired keys took {time:?} ({time_per_op:?} per key)",
+ len = keys.len(),
+ );
+
+ // #####################################
+ let before = Instant::now();
+ for key in &keys {
+ cache.delete(key).expect("could not delete value");
+ }
+
+ let time = Instant::now() - before;
+ let time_per_op = time / keys.len() as u32;
+ println!(
+ "deleting {len} keys took {time:?} ({time_per_op:?} per key)",
+ len = keys.len(),
+ );
+}
diff --git a/proxmox-shared-cache/src/lib.rs b/proxmox-shared-cache/src/lib.rs
new file mode 100644
index 0000000..c63de91
--- /dev/null
+++ b/proxmox-shared-cache/src/lib.rs
@@ -0,0 +1,485 @@
+use std::fs::File;
+use std::os::fd::{FromRawFd, IntoRawFd, RawFd};
+use std::path::{Path, PathBuf};
+use std::time::Duration;
+
+use anyhow::{bail, Error};
+use serde::de::DeserializeOwned;
+use serde::{Deserialize, Serialize};
+use serde_json::value::RawValue;
+
+use proxmox_schema::api_types::SAFE_ID_FORMAT;
+use proxmox_sys::fs::CreateOptions;
+
+/// Lock guard for a locked cache entry.
+///
+/// The lock is dropped when the guard is dropped.
+pub struct CacheLockGuard(File);
+
+impl FromRawFd for CacheLockGuard {
+ unsafe fn from_raw_fd(fd: RawFd) -> Self {
+ CacheLockGuard(File::from_raw_fd(fd))
+ }
+}
+
+impl IntoRawFd for CacheLockGuard {
+ fn into_raw_fd(self) -> RawFd {
+ self.0.into_raw_fd()
+ }
+}
+
+/// A simple, file-backed cache that can be used from multiple processes concurrently.
+///
+/// Cache entries are stored as individual files inside a base directory. For instance,
+/// a cache entry with the key 'disk_stats' will result in a file 'disk_stats.json' inside
+/// the base directory. As the extension implies, the cached data will be stored as a JSON
+/// string.
+///
+/// For optimal performance, `SharedCache` should have its base directory in a `tmpfs`.
+///
+/// ## Key Space
+/// Due to the fact that cache keys are being directly used as filenames, they have to match the
+/// following regular expression: `[A-Za-z0-9_][A-Za-z0-9._\-]*`
+///
+/// ## Concurrency
+/// set/delete will use file locking to ensure that there are no race conditions.
+/// get does not require any locking, since all file operations used by set/delete should be
+/// atomic.
+///
+/// If multiple cache operations must be at the same locked context, `lock` can be used
+/// to manually lock a cache entry. The returned lock guard can then be passed to
+/// `{set,delete,get}_with_lock`.
+/// If multiple keys are locked at the same time, make sure that you always lock them
+/// in same order (e.g. by sorting the keys) - otherwise circular waits can occur.
+///
+/// ## Performance
+/// On a tmpfs:
+/// ```sh
+/// $ cargo run --release --example=performance
+/// inserting 100000 keys took 2.495008362s (24.95µs per key)
+/// getting 100000 unexpired keys took 1.557399535s (15.573µs per key)
+/// updating 100000 keys took 2.488894178s (24.888µs per key)
+/// getting 100000 expired keys took 1.324983239s (13.249µs per key)
+/// deleting 100000 keys took 1.533744028s (15.337µs per key)
+///
+/// Inserting/getting large objects might of course result in lower performance due to the cost
+/// of serialization.
+/// ```
+///
+/// # Limitations
+/// - At the moment, stale/expired keys are never cleaned - at the moment
+/// of creation this was simply not needed, since we only use the crate for
+/// caching data in pvestatd with a limited set of keys that are not changing
+/// - so there will not be any cache entries that need to be cleaned up.
+///
+pub struct SharedCache {
+ base_path: PathBuf,
+ create_options: CreateOptions,
+ #[cfg(test)]
+ time: std::cell::Cell<i64>,
+}
+
+impl SharedCache {
+ /// Instantiate a new cache instance for a given `base_path`.
+ ///
+ /// If `base_path` does not exist, it will be created - the access permissions
+ /// are determined by `options`.
+ /// If the base directory already contains cache entries, they will be available
+ /// via `get` for later retrieval. In other words, `SharedCache::new` never touches
+ /// existing cache entries.
+ pub fn new<P: AsRef<Path>>(base_path: P, options: CreateOptions) -> Result<Self, Error> {
+ proxmox_sys::fs::create_path(
+ base_path.as_ref(),
+ Some(options.clone()),
+ Some(options.clone()),
+ )?;
+
+ Ok(SharedCache {
+ base_path: base_path.as_ref().to_owned(),
+ create_options: options,
+ #[cfg(test)]
+ time: std::cell::Cell::new(0),
+ })
+ }
+
+ /// Set a cache entry, with optional value expiration.
+ ///
+ /// This method will attempt to lock the cache entry before deleting it.
+ ///
+ /// Keys have to match the following regular expression to be valid:
+ /// `[A-Za-z0-9_][A-Za-z0-9._\-]*`
+ ///
+ /// Returns an error if value serialization or storing the value failed.
+ pub fn set<S: AsRef<str>, V: Serialize>(
+ &self,
+ key: S,
+ value: &V,
+ expires_in: Option<i64>,
+ ) -> Result<(), Error> {
+ let lock = self.lock(key.as_ref(), true)?;
+ self.set_with_lock(key, value, expires_in, &lock)
+ }
+
+ /// Set a cache entry, with optional value expiration.
+ ///
+ /// This method assumes that the cache entry was locked before using `lock`.
+ ///
+ /// Keys have to match the following regular expression to be valid:
+ /// `[A-Za-z0-9_][A-Za-z0-9._\-]*`
+ ///
+ /// Returns an error if value serialization or storing the value failed.
+ pub fn set_with_lock<S: AsRef<str>, V: Serialize>(
+ &self,
+ key: S,
+ value: &V,
+ expires_in: Option<i64>,
+ _lock: &CacheLockGuard,
+ ) -> Result<(), Error> {
+ let path = self.get_entry_path(key.as_ref())?;
+ let added_at = self.get_time();
+
+ let item = CachedItem {
+ value,
+ added_at,
+ expires_in,
+ };
+
+ let serialized = serde_json::to_vec_pretty(&item)?;
+
+ // Atomically replace file
+ proxmox_sys::fs::replace_file(path, &serialized, self.create_options.clone(), true)?;
+ Ok(())
+ }
+
+ /// Delete a cache entry.
+ ///
+ /// This method will attempt to lock the cache entry before deleting it.
+ ///
+ /// Keys have to match the following regular expression to be valid:
+ /// `[A-Za-z0-9_][A-Za-z0-9._\-]*`
+ ///
+ /// Returns an error if the entry could not be deleted.
+ pub fn delete<S: AsRef<str>>(&self, key: S) -> Result<(), Error> {
+ let lock = self.lock(key.as_ref(), true)?;
+ self.delete_with_lock(key.as_ref(), &lock)?;
+
+ Ok(())
+ }
+
+ /// Delete a cache entry.
+ ///
+ /// This method assumes that the cache entry was locked before using `lock`.
+ ///
+ /// Keys have to match the following regular expression to be valid:
+ /// `[A-Za-z0-9_][A-Za-z0-9._\-]*`
+ ///
+ /// Returns an error if the entry could not be deleted.
+ pub fn delete_with_lock<S: AsRef<str>>(
+ &self,
+ key: S,
+ _lock: &CacheLockGuard,
+ ) -> Result<(), Error> {
+ let path = self.get_entry_path(key.as_ref())?;
+ std::fs::remove_file(path)?;
+
+ // Unlink the lock file's dir entry from the fs, but since we have
+ // an open file handle for the lock file, it continues to exist until
+ // the handle is closed
+ std::fs::remove_file(self.get_lockfile_path(key.as_ref())?)?;
+
+ Ok(())
+ }
+
+ /// Get a value from the cache.
+ ///
+ /// This method will attempt to lock the entry with a non-exclusive lock before reading it.
+ ///
+ /// Keys have to match the following regular expression to be valid:
+ /// `[A-Za-z0-9_][A-Za-z0-9._\-]*`
+ ///
+ /// Returns an error if the entry could not be retrieved.
+ pub fn get<S: AsRef<str>, V: DeserializeOwned>(&self, key: S) -> Result<Option<V>, Error> {
+ let lock = self.lock(key.as_ref(), false)?;
+ self.get_with_lock(key, &lock)
+ }
+
+ /// Get a value from the cache.
+ ///
+ /// If the key does not exist or the value has expired, `Ok(None)` is returned.
+ /// This method assumes that the cache entry was locked before using `lock`.
+ ///
+ /// Keys have to match the following regular expression to be valid:
+ /// `[A-Za-z0-9_][A-Za-z0-9._\-]*`
+ ///
+ /// Returns an error if the entry could not be retrieved.
+ pub fn get_with_lock<S: AsRef<str>, V: DeserializeOwned>(
+ &self,
+ key: S,
+ _lock: &CacheLockGuard,
+ ) -> Result<Option<V>, Error> {
+ let path = self.get_entry_path(key.as_ref())?;
+
+ if let Some(content) = proxmox_sys::fs::file_get_optional_contents(path)? {
+ // Use RawValue so that we can deserialize the actual payload after
+ // checking value expiry. This should improve performance for large payloads.
+ let value: CachedItem<&'_ RawValue> = serde_json::from_slice(&content)?;
+
+ let now = self.get_time();
+
+ if let Some(expires_in) = value.expires_in {
+ // Check if value is not expired yet. Also do not allow
+ // values from the future, in case we have clock jumps
+ if value.added_at + expires_in > now && value.added_at <= now {
+ Ok(Some(serde_json::from_str(value.value.get())?))
+ } else {
+ Ok(None)
+ }
+ } else {
+ Ok(Some(serde_json::from_str(value.value.get())?))
+ }
+ } else {
+ Ok(None)
+ }
+ }
+
+ /// Get value from the cache. If it does not exist/is expired, compute
+ /// the new value from a passed closure and insert it into the cache.
+ ///
+ /// Keys have to match the following regular expression to be valid:
+ /// `[A-Za-z0-9_][A-Za-z0-9._\-]*`
+ pub fn get_or_update<K, V, F>(
+ &self,
+ key: K,
+ value_func: &mut F,
+ expires_in: Option<i64>,
+ ) -> Result<V, Error>
+ where
+ K: AsRef<str>,
+ F: FnMut() -> Result<V, Error>,
+ V: Serialize + DeserializeOwned,
+ {
+ // Lookup value and return if it exists and has not expired yet
+ // get uses a non-exclusive lock, so we can have concurrent lookups
+ let val = self.get(key.as_ref())?;
+ if let Some(val) = val {
+ return Ok(val);
+ }
+
+ // If not, lock the entry ...
+ let lock = self.lock(key.as_ref(), true)?;
+
+ // ... and check again, maybe somebody else has set the value
+ // before we locked it.
+ let val = self.get_with_lock(key.as_ref(), &lock)?;
+ if let Some(val) = val {
+ return Ok(val);
+ }
+
+ // If the value is still not there, compute its new value and store it
+ let val = value_func()?;
+ self.set_with_lock(key.as_ref(), &val, expires_in, &lock)?;
+
+ Ok(val)
+ }
+
+ /// Locks a cache entry.
+ ///
+ /// Useful if you must perform multiple operations while being locked.
+ ///
+ /// This will create a lockfile `<base-path>/<key>.lck` which will be locked
+ /// with an advisory file lock via `flock`.
+ ///
+ /// On success, `CacheLockGuard` is returned. It serves as a handle to
+ /// the open lock file. If the handle is dropped, the lock is removed.
+ ///
+ /// Keys have to match the following regular expression to be valid:
+ /// `[A-Za-z0-9_][A-Za-z0-9._\-]*`
+ ///
+ /// Returns an error if the entry could not be locked.
+ pub fn lock<S: AsRef<str>>(&self, key: S, exclusive: bool) -> Result<CacheLockGuard, Error> {
+ let mut path = self.get_entry_path(key.as_ref())?;
+ path.set_extension("lck");
+
+ let options = proxmox_sys::fs::CreateOptions::new()
+ .perm(nix::sys::stat::Mode::from_bits_truncate(0o660));
+
+ let lockfile =
+ proxmox_sys::fs::open_file_locked(path, Duration::from_secs(5), exclusive, options)?;
+
+ Ok(CacheLockGuard(lockfile))
+ }
+
+ #[cfg(not(test))]
+ fn get_time(&self) -> i64 {
+ proxmox_time::epoch_i64()
+ }
+
+ #[cfg(test)]
+ fn get_time(&self) -> i64 {
+ self.time.get()
+ }
+
+ #[cfg(test)]
+ fn set_time(&self, time: i64) {
+ self.time.set(time);
+ }
+
+ fn enforce_safe_key(key: &str) -> Result<(), Error> {
+ let safe_id_regex = SAFE_ID_FORMAT.unwrap_pattern_format();
+ if safe_id_regex.is_match(key) {
+ Ok(())
+ } else {
+ bail!("invalid key format")
+ }
+ }
+
+ fn get_entry_path(&self, key: &str) -> Result<PathBuf, Error> {
+ Self::enforce_safe_key(key)?;
+ let mut path = self.base_path.join(key);
+ path.set_extension("json");
+ Ok(path)
+ }
+
+ fn get_lockfile_path(&self, key: &str) -> Result<PathBuf, Error> {
+ Self::enforce_safe_key(key)?;
+ let mut path = self.base_path.join(key);
+ path.set_extension("lck");
+ Ok(path)
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct CachedItem<V> {
+ value: V,
+ added_at: i64,
+ expires_in: Option<i64>,
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use serde_json::Value;
+
+ #[test]
+ fn test_basic_set_and_get() {
+ let cache = TestCache::new();
+ cache
+ .cache
+ .set("foo", &Value::String("bar".into()), None)
+ .unwrap();
+
+ assert_eq!(
+ cache.cache.get("foo").unwrap(),
+ Some(Value::String("bar".into()))
+ );
+ assert!(cache.cache.get::<_, Value>("notthere").unwrap().is_none());
+ }
+
+ struct TestCache {
+ cache: SharedCache,
+ }
+
+ impl TestCache {
+ fn new() -> Self {
+ let path = proxmox_sys::fs::make_tmp_dir("/tmp/", None).unwrap();
+
+ let options = CreateOptions::new()
+ .owner(nix::unistd::Uid::effective())
+ .group(nix::unistd::Gid::effective())
+ .perm(nix::sys::stat::Mode::from_bits_truncate(0o600));
+
+ let cache = SharedCache::new(&path, options).unwrap();
+ Self { cache }
+ }
+ }
+
+ impl Drop for TestCache {
+ fn drop(&mut self) {
+ let _ = std::fs::remove_dir_all(&self.cache.base_path);
+ }
+ }
+
+ #[test]
+ fn test_expiry() {
+ let wrapper = TestCache::new();
+
+ wrapper
+ .cache
+ .set("expiring", &Value::String("bar".into()), Some(10))
+ .unwrap();
+ assert!(wrapper.cache.get::<_, Value>("expiring").unwrap().is_some());
+
+ wrapper.cache.set_time(9);
+ assert!(wrapper.cache.get::<_, Value>("expiring").unwrap().is_some());
+ wrapper.cache.set_time(11);
+ assert!(wrapper.cache.get::<_, Value>("expiring").unwrap().is_none());
+ }
+
+ #[test]
+ fn test_backwards_time_jump() {
+ let wrapper = TestCache::new();
+
+ wrapper.cache.set_time(50);
+ wrapper
+ .cache
+ .set("future", &Value::String("bar".into()), Some(10))
+ .unwrap();
+ wrapper.cache.set_time(30);
+ assert!(wrapper.cache.get::<_, Value>("future").unwrap().is_none());
+ }
+
+ #[test]
+ fn test_invalid_keys() {
+ let wrapper = TestCache::new();
+
+ assert!(wrapper
+ .cache
+ .set("../escape_base", &Value::Null, None)
+ .is_err());
+ assert!(wrapper
+ .cache
+ .set("bjørnen drikker øl", &Value::Null, None)
+ .is_err());
+ assert!(wrapper.cache.set("test space", &Value::Null, None).is_err());
+ assert!(wrapper.cache.set("~/foo", &Value::Null, None).is_err());
+ }
+
+ #[test]
+ fn test_deletion() {
+ let wrapper = TestCache::new();
+
+ wrapper
+ .cache
+ .set("delete", &Value::String("bar".into()), Some(10))
+ .unwrap();
+
+ assert!(wrapper.cache.delete("delete").is_ok());
+ assert!(wrapper.cache.get::<_, Value>("delete").unwrap().is_none());
+ }
+
+ #[test]
+ fn test_get_or_update() {
+ let wrapper = TestCache::new();
+
+ let val = wrapper
+ .cache
+ .get_or_update("test", &mut || Ok(0), Some(5))
+ .unwrap();
+
+ assert_eq!(val, 0);
+
+ wrapper.cache.set_time(4);
+ let val = wrapper
+ .cache
+ .get_or_update("test", &mut || Ok(4), Some(5))
+ .unwrap();
+ assert_eq!(val, 0);
+
+ wrapper.cache.set_time(6);
+ let val = wrapper
+ .cache
+ .get_or_update("test", &mut || Ok(6), Some(5))
+ .unwrap();
+ assert_eq!(val, 6);
+ }
+}
--
2.39.2
More information about the pve-devel
mailing list