From: Lukas Wagner <l.wagner@proxmox.com>
To: pdm-devel@lists.proxmox.com
Subject: [PATCH datacenter-manager v3 3/4] api: resources: subscriptions: switch over to api_cache
Date: Fri, 15 May 2026 16:49:26 +0200 [thread overview]
Message-ID: <20260515144927.427114-4-l.wagner@proxmox.com> (raw)
In-Reply-To: <20260515144927.427114-1-l.wagner@proxmox.com>
Instead of storing the subscription state in a module-level HashMap, use
the newly introduced api_cache module to cache the status.
Signed-off-by: Lukas Wagner <l.wagner@proxmox.com>
---
Notes:
Changes since the RFC:
- Use new async cache interface
Changes since v1:
- Make cache self-healing in case an entry could not be read or
deserialized. This ensure resiliency when the cached data type
changes unexpectedly. In this case, we just log an error and
return None
Changes since v2:
- don't update cache if somebody else has updated it in the meanwhile.
Additionally, return the more up-to-date data from the cache, so
that we always the freshest subscription data possible
server/src/api/resources.rs | 88 ++++++++++++++++---------------------
1 file changed, 39 insertions(+), 49 deletions(-)
diff --git a/server/src/api/resources.rs b/server/src/api/resources.rs
index 50315b11..8bcb8ce3 100644
--- a/server/src/api/resources.rs
+++ b/server/src/api/resources.rs
@@ -30,9 +30,10 @@ use proxmox_schema::{api, parse_boolean};
use proxmox_sortable_macro::sortable;
use proxmox_subscription::SubscriptionStatus;
use pve_api_types::{ClusterResource, ClusterResourceNetworkType, ClusterResourceType};
+use serde::{Deserialize, Serialize};
use crate::metric_collection::top_entities;
-use crate::{connection, views};
+use crate::{api_cache, connection, views};
pub const ROUTER: Router = Router::new()
.get(&list_subdirs_api_method!(SUBDIRS))
@@ -798,15 +799,11 @@ async fn get_top_entities(
Ok(res)
}
-#[derive(Clone)]
+#[derive(Clone, Serialize, Deserialize)]
struct CachedSubscriptionState {
node_info: HashMap<String, Option<NodeSubscriptionInfo>>,
- timestamp: i64,
}
-static SUBSCRIPTION_CACHE: LazyLock<RwLock<HashMap<String, CachedSubscriptionState>>> =
- LazyLock::new(|| RwLock::new(HashMap::new()));
-
/// Get the subscription state for a given remote.
///
/// If recent enough cached data is available, it is returned
@@ -815,66 +812,59 @@ pub async fn get_subscription_info_for_remote(
remote: &Remote,
max_age: u64,
) -> Result<HashMap<String, Option<NodeSubscriptionInfo>>, Error> {
- if let Some(cached_subscription) = get_cached_subscription_info(&remote.id, max_age) {
+ if let Some(cached_subscription) = get_cached_subscription_info(&remote.id, max_age).await? {
Ok(cached_subscription.node_info)
} else {
let node_info = fetch_remote_subscription_info(remote).await?;
let now = proxmox_time::epoch_i64();
- update_cached_subscription_info(&remote.id, &node_info, now);
+
+ if let Some(existing_state) =
+ update_cached_subscription_info(&remote.id, node_info.clone(), now).await?
+ {
+ // Somebody else updated the cache while we performed the API request,
+ // return the more recent data instead of the data we just fetched.
+ return Ok(existing_state.node_info);
+ }
Ok(node_info)
}
}
-fn get_cached_subscription_info(remote: &str, max_age: u64) -> Option<CachedSubscriptionState> {
- let cache = SUBSCRIPTION_CACHE
- .read()
- .expect("subscription mutex poisoned");
+const SUBSCRIPTION_STATE_CACHE_KEY: &str = "subscription-state";
- if max_age == 0 {
- return None;
- }
- if let Some(cached_subscription) = cache.get(remote) {
- let now = proxmox_time::epoch_i64();
- let diff = now - cached_subscription.timestamp;
+async fn get_cached_subscription_info(
+ remote: &str,
+ max_age: u64,
+) -> Result<Option<CachedSubscriptionState>, Error> {
+ let cache = api_cache::read_remote(remote).await?;
+ let subscription_state = cache
+ .get_with_max_age(SUBSCRIPTION_STATE_CACHE_KEY, max_age as i64)
+ .await
+ .inspect_err(|err| log::error!("could not read subscription-state from API cache: {err}"))
+ .ok()
+ .flatten();
- if diff >= max_age as i64 || diff < 0 {
- // value is too old or from the future
- None
- } else {
- Some(cached_subscription.clone())
- }
- } else {
- None
- }
+ Ok(subscription_state)
}
/// Update cached subscription data.
///
-/// If the cache already contains more recent data we don't insert the passed resources.
-fn update_cached_subscription_info(
+/// If the cache already contains more recent data, this function returns the already
+/// stored state as `Ok(Some(state))`. If the data that was passed in replaced the cache
+/// entry, `Ok(None)` is returned.
+async fn update_cached_subscription_info(
remote: &str,
- node_info: &HashMap<String, Option<NodeSubscriptionInfo>>,
+ node_info: HashMap<String, Option<NodeSubscriptionInfo>>,
now: i64,
-) {
- // there is no good way to recover from this, so panicking should be fine
- let mut cache = SUBSCRIPTION_CACHE
- .write()
- .expect("subscription mutex poisoned");
+) -> Result<Option<CachedSubscriptionState>, Error> {
+ let cache = api_cache::write_remote(remote).await?;
- if let Some(cached_resource) = cache.get(remote) {
- // skip updating if the data is new enough
- if cached_resource.timestamp >= now {
- return;
- }
- }
-
- cache.insert(
- remote.into(),
- CachedSubscriptionState {
- node_info: node_info.clone(),
- timestamp: now,
- },
- );
+ Ok(cache
+ .set_if_newer_with_timestamp(
+ SUBSCRIPTION_STATE_CACHE_KEY,
+ CachedSubscriptionState { node_info },
+ now,
+ )
+ .await?)
}
/// Maps a list of node subscription infos into a single [`RemoteSubscriptionState`]
--
2.47.3
next prev parent reply other threads:[~2026-05-15 14:49 UTC|newest]
Thread overview: 5+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-05-15 14:49 [PATCH datacenter-manager v3 0/4] add generic, per-remote (and global) cache for remote API responses Lukas Wagner
2026-05-15 14:49 ` [PATCH datacenter-manager v3 1/4] add persistent, generic, namespaced key-value cache implementation Lukas Wagner
2026-05-15 14:49 ` [PATCH datacenter-manager v3 2/4] add api_cache as a specialized wrapper around the namespaced cache Lukas Wagner
2026-05-15 14:49 ` Lukas Wagner [this message]
2026-05-15 14:49 ` [PATCH datacenter-manager v3 4/4] remote-updates: switch over to new api_cache Lukas Wagner
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20260515144927.427114-4-l.wagner@proxmox.com \
--to=l.wagner@proxmox.com \
--cc=pdm-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.