mirror of
https://forgejo.ellis.link/continuwuation/continuwuity.git
synced 2026-05-26 20:49:55 +00:00
fix: Clean up cache, prevent several race conditions
We use one map which is only ever held for a short time.
This commit is contained in:
@@ -299,6 +299,16 @@
|
|||||||
#
|
#
|
||||||
#max_concurrent_inbound_transactions = 150
|
#max_concurrent_inbound_transactions = 150
|
||||||
|
|
||||||
|
# Maximum age (in seconds) for cached federation transaction responses.
|
||||||
|
# Entries older than this will be removed during cleanup.
|
||||||
|
#
|
||||||
|
#transaction_id_cache_max_age_secs = 7200 (2 hours)
|
||||||
|
|
||||||
|
# Maximum number of cached federation transaction responses.
|
||||||
|
# When the cache exceeds this limit, older entries will be removed.
|
||||||
|
#
|
||||||
|
#transaction_id_cache_max_entries = 8192
|
||||||
|
|
||||||
# Default/base connection timeout (seconds). This is used only by URL
|
# Default/base connection timeout (seconds). This is used only by URL
|
||||||
# previews and update/news endpoint checks.
|
# previews and update/news endpoint checks.
|
||||||
#
|
#
|
||||||
|
|||||||
+28
-34
@@ -42,7 +42,7 @@ use ruma::{
|
|||||||
to_device::DeviceIdOrAllDevices,
|
to_device::DeviceIdOrAllDevices,
|
||||||
uint,
|
uint,
|
||||||
};
|
};
|
||||||
use service::transaction_ids::{TxnKey, WrappedTransactionResponse};
|
use service::transaction_ids::{FederationTxnState, TxnKey, WrappedTransactionResponse};
|
||||||
use tokio::sync::watch::{Receiver, Sender};
|
use tokio::sync::watch::{Receiver, Sender};
|
||||||
use tracing::instrument;
|
use tracing::instrument;
|
||||||
|
|
||||||
@@ -65,18 +65,6 @@ pub(crate) async fn send_transaction_message_route(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let txn_key = (body.origin().to_owned(), body.transaction_id.clone());
|
|
||||||
|
|
||||||
// Did we already process this transaction
|
|
||||||
if let Some(response) = services.transaction_ids.get_cached_txn(&txn_key) {
|
|
||||||
return Ok(response);
|
|
||||||
}
|
|
||||||
// Or are currently processing it
|
|
||||||
if let Some(receiver) = services.transaction_ids.get_active_federation_txn(&txn_key) {
|
|
||||||
// Wait up to 50 seconds for a result
|
|
||||||
return wait_for_result(receiver).await;
|
|
||||||
}
|
|
||||||
|
|
||||||
if body.pdus.len() > PDU_LIMIT {
|
if body.pdus.len() > PDU_LIMIT {
|
||||||
return Err!(Request(Forbidden(
|
return Err!(Request(Forbidden(
|
||||||
"Not allowed to send more than {PDU_LIMIT} PDUs in one transaction"
|
"Not allowed to send more than {PDU_LIMIT} PDUs in one transaction"
|
||||||
@@ -89,21 +77,31 @@ pub(crate) async fn send_transaction_message_route(
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
let sender = services
|
let txn_key = (body.origin().to_owned(), body.transaction_id.clone());
|
||||||
|
|
||||||
|
// Atomically check cache, join active, or start new transaction
|
||||||
|
match services
|
||||||
.transaction_ids
|
.transaction_ids
|
||||||
.start_federation_txn(txn_key.clone())?;
|
.get_or_start_federation_txn(txn_key.clone())?
|
||||||
services.server.runtime().spawn(process_inbound_transaction(
|
{
|
||||||
services,
|
| FederationTxnState::Cached(response) => {
|
||||||
body,
|
// Already responded
|
||||||
client,
|
Ok(response)
|
||||||
txn_key.clone(),
|
},
|
||||||
sender,
|
| FederationTxnState::Active(receiver) => {
|
||||||
));
|
// Another thread is processing
|
||||||
let receiver = services
|
wait_for_result(receiver).await
|
||||||
.transaction_ids
|
},
|
||||||
.get_active_federation_txn(&txn_key)
|
| FederationTxnState::Started { receiver, sender } => {
|
||||||
.expect("just-created transaction was missing");
|
// We're the first, spawn the processing task
|
||||||
wait_for_result(receiver).await
|
services
|
||||||
|
.server
|
||||||
|
.runtime()
|
||||||
|
.spawn(process_inbound_transaction(services, body, client, txn_key, sender));
|
||||||
|
// and wait for it
|
||||||
|
wait_for_result(receiver).await
|
||||||
|
},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn wait_for_result(
|
async fn wait_for_result(
|
||||||
@@ -161,7 +159,7 @@ async fn process_inbound_transaction(
|
|||||||
// think we processed it properly (and lose events), but we also can't return
|
// think we processed it properly (and lose events), but we also can't return
|
||||||
// an actual error.
|
// an actual error.
|
||||||
drop(sender);
|
drop(sender);
|
||||||
services.transaction_ids.finish_federation_txn(&txn_key);
|
services.transaction_ids.remove_federation_txn(&txn_key);
|
||||||
panic!("failed to handle incoming transaction");
|
panic!("failed to handle incoming transaction");
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -186,14 +184,10 @@ async fn process_inbound_transaction(
|
|||||||
.map(|(e, r)| (e, r.map_err(error::sanitized_message)))
|
.map(|(e, r)| (e, r.map_err(error::sanitized_message)))
|
||||||
.collect(),
|
.collect(),
|
||||||
};
|
};
|
||||||
|
|
||||||
services
|
services
|
||||||
.transaction_ids
|
.transaction_ids
|
||||||
.set_cached_txn(txn_key.clone(), response.clone());
|
.finish_federation_txn(txn_key, sender, response);
|
||||||
sender
|
|
||||||
.send(Some(response))
|
|
||||||
.expect("couldn't send response to channel");
|
|
||||||
services.transaction_ids.finish_federation_txn(&txn_key);
|
|
||||||
drop(sender);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle(
|
async fn handle(
|
||||||
|
|||||||
@@ -379,6 +379,20 @@ pub struct Config {
|
|||||||
#[serde(default = "default_max_concurrent_inbound_transactions")]
|
#[serde(default = "default_max_concurrent_inbound_transactions")]
|
||||||
pub max_concurrent_inbound_transactions: usize,
|
pub max_concurrent_inbound_transactions: usize,
|
||||||
|
|
||||||
|
/// Maximum age (in seconds) for cached federation transaction responses.
|
||||||
|
/// Entries older than this will be removed during cleanup.
|
||||||
|
///
|
||||||
|
/// default: 7200 (2 hours)
|
||||||
|
#[serde(default = "default_transaction_id_cache_max_age_secs")]
|
||||||
|
pub transaction_id_cache_max_age_secs: u64,
|
||||||
|
|
||||||
|
/// Maximum number of cached federation transaction responses.
|
||||||
|
/// When the cache exceeds this limit, older entries will be removed.
|
||||||
|
///
|
||||||
|
/// default: 8192
|
||||||
|
#[serde(default = "default_transaction_id_cache_max_entries")]
|
||||||
|
pub transaction_id_cache_max_entries: usize,
|
||||||
|
|
||||||
/// Default/base connection timeout (seconds). This is used only by URL
|
/// Default/base connection timeout (seconds). This is used only by URL
|
||||||
/// previews and update/news endpoint checks.
|
/// previews and update/news endpoint checks.
|
||||||
///
|
///
|
||||||
@@ -2553,6 +2567,10 @@ fn default_max_fetch_prev_events() -> u16 { 192_u16 }
|
|||||||
|
|
||||||
fn default_max_concurrent_inbound_transactions() -> usize { 150 }
|
fn default_max_concurrent_inbound_transactions() -> usize { 150 }
|
||||||
|
|
||||||
|
fn default_transaction_id_cache_max_age_secs() -> u64 { 60 * 60 * 2 }
|
||||||
|
|
||||||
|
fn default_transaction_id_cache_max_entries() -> usize { 8192 }
|
||||||
|
|
||||||
fn default_tracing_flame_filter() -> String {
|
fn default_tracing_flame_filter() -> String {
|
||||||
cfg!(debug_assertions)
|
cfg!(debug_assertions)
|
||||||
.then_some("trace,h2=off")
|
.then_some("trace,h2=off")
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
use std::{collections::HashMap, sync::Arc};
|
use std::{
|
||||||
|
collections::HashMap,
|
||||||
|
sync::{
|
||||||
|
Arc,
|
||||||
|
atomic::{AtomicU64, Ordering},
|
||||||
|
},
|
||||||
|
time::{Duration, SystemTime},
|
||||||
|
};
|
||||||
|
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use conduwuit::{Error, Result, SyncRwLock, debug, debug_warn, warn};
|
use conduwuit::{Error, Result, SyncRwLock, debug_warn, warn};
|
||||||
use database::{Handle, Map};
|
use database::{Handle, Map};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
DeviceId, OwnedServerName, OwnedTransactionId, TransactionId, UserId,
|
DeviceId, OwnedServerName, OwnedTransactionId, TransactionId, UserId,
|
||||||
@@ -12,16 +19,58 @@ use ruma::{
|
|||||||
};
|
};
|
||||||
use tokio::sync::watch::{Receiver, Sender};
|
use tokio::sync::watch::{Receiver, Sender};
|
||||||
|
|
||||||
|
use crate::{Dep, config};
|
||||||
|
|
||||||
pub type TxnKey = (OwnedServerName, OwnedTransactionId);
|
pub type TxnKey = (OwnedServerName, OwnedTransactionId);
|
||||||
pub type WrappedTransactionResponse = Option<send_transaction_message::v1::Response>;
|
pub type WrappedTransactionResponse = Option<send_transaction_message::v1::Response>;
|
||||||
pub type ActiveTransactionsMap = HashMap<TxnKey, Receiver<WrappedTransactionResponse>>;
|
|
||||||
|
/// Minimum interval between cache cleanup runs.
|
||||||
|
/// Exists to prevent thrashing when the cache is full of things that can't be
|
||||||
|
/// cleared
|
||||||
|
const CLEANUP_INTERVAL_SECS: u64 = 30;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct CachedTxnResponse {
|
||||||
|
pub response: send_transaction_message::v1::Response,
|
||||||
|
pub created: SystemTime,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Internal state for a federation transaction.
|
||||||
|
/// Either actively being processed or completed and cached.
|
||||||
|
#[derive(Clone)]
|
||||||
|
enum TxnState {
|
||||||
|
/// Transaction is currently being processed.
|
||||||
|
Active(Receiver<WrappedTransactionResponse>),
|
||||||
|
|
||||||
|
/// Transaction completed and response is cached.
|
||||||
|
Cached(CachedTxnResponse),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Result of atomically checking or starting a federation transaction.
|
||||||
|
pub enum FederationTxnState {
|
||||||
|
/// Transaction already completed and cached
|
||||||
|
Cached(send_transaction_message::v1::Response),
|
||||||
|
|
||||||
|
/// Transaction is currently being processed by another request.
|
||||||
|
/// Wait on this receiver for the result.
|
||||||
|
Active(Receiver<WrappedTransactionResponse>),
|
||||||
|
|
||||||
|
/// This caller should process the transaction (first to request it).
|
||||||
|
Started {
|
||||||
|
receiver: Receiver<WrappedTransactionResponse>,
|
||||||
|
sender: Sender<WrappedTransactionResponse>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Service {
|
pub struct Service {
|
||||||
|
services: Services,
|
||||||
db: Data,
|
db: Data,
|
||||||
servername_txnid_response_cache:
|
federation_txn_state: Arc<SyncRwLock<HashMap<TxnKey, TxnState>>>,
|
||||||
Arc<SyncRwLock<HashMap<TxnKey, send_transaction_message::v1::Response>>>,
|
last_cleanup: AtomicU64,
|
||||||
servername_txnid_active: Arc<SyncRwLock<ActiveTransactionsMap>>,
|
}
|
||||||
max_active_txns: usize,
|
|
||||||
|
struct Services {
|
||||||
|
config: Dep<config::Service>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Data {
|
struct Data {
|
||||||
@@ -32,30 +81,35 @@ struct Data {
|
|||||||
impl crate::Service for Service {
|
impl crate::Service for Service {
|
||||||
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||||
Ok(Arc::new(Self {
|
Ok(Arc::new(Self {
|
||||||
|
services: Services {
|
||||||
|
config: args.depend::<config::Service>("config"),
|
||||||
|
},
|
||||||
db: Data {
|
db: Data {
|
||||||
userdevicetxnid_response: args.db["userdevicetxnid_response"].clone(),
|
userdevicetxnid_response: args.db["userdevicetxnid_response"].clone(),
|
||||||
},
|
},
|
||||||
servername_txnid_response_cache: Arc::new(SyncRwLock::new(HashMap::new())),
|
federation_txn_state: Arc::new(SyncRwLock::new(HashMap::new())),
|
||||||
servername_txnid_active: Arc::new(SyncRwLock::new(HashMap::new())),
|
last_cleanup: AtomicU64::new(0),
|
||||||
max_active_txns: args
|
|
||||||
.depend::<crate::config::Service>("config")
|
|
||||||
.max_concurrent_inbound_transactions,
|
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn clear_cache(&self) {
|
async fn clear_cache(&self) {
|
||||||
let mut state = self.servername_txnid_response_cache.write();
|
let mut state = self.federation_txn_state.write();
|
||||||
state.clear();
|
// Only clear cached entries, preserve active transactions
|
||||||
|
state.retain(|_, v| matches!(v, TxnState::Active(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Service {
|
impl Service {
|
||||||
|
/// Returns the count of currently active (in-progress) transactions.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn txn_active_handle_count(&self) -> usize {
|
pub fn txn_active_handle_count(&self) -> usize {
|
||||||
let state = self.servername_txnid_active.read();
|
let state = self.federation_txn_state.read();
|
||||||
state.len()
|
state
|
||||||
|
.values()
|
||||||
|
.filter(|v| matches!(v, TxnState::Active(_)))
|
||||||
|
.count()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_client_txnid(
|
pub fn add_client_txnid(
|
||||||
@@ -84,80 +138,169 @@ impl Service {
|
|||||||
self.db.userdevicetxnid_response.qry(&key).await
|
self.db.userdevicetxnid_response.qry(&key).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Fetches a receiver channel for the given transaction, if any exists.
|
/// Atomically gets a cached response, joins an active transaction, or
|
||||||
/// If the given txn is not active, None is returned.
|
/// starts a new one.
|
||||||
#[must_use]
|
pub fn get_or_start_federation_txn(&self, key: TxnKey) -> Result<FederationTxnState> {
|
||||||
pub fn get_active_federation_txn(
|
// Only one upgradable lock can be held at a time, and there aren't any
|
||||||
&self,
|
// read-only locks, so no point being upgradable
|
||||||
key: &TxnKey,
|
let mut state = self.federation_txn_state.write();
|
||||||
) -> Option<Receiver<WrappedTransactionResponse>> {
|
|
||||||
let state = self.servername_txnid_active.read();
|
|
||||||
state.get(key).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Starts a new inbound transaction handler, returning the appropriate
|
// Check existing state for this key
|
||||||
/// sender to broadcast the response via.
|
if let Some(txn_state) = state.get(&key) {
|
||||||
///
|
return Ok(match txn_state {
|
||||||
/// If the given key is already active, a rate-limited response is returned.
|
| TxnState::Cached(cached) => FederationTxnState::Cached(cached.response.clone()),
|
||||||
pub fn start_federation_txn(
|
| TxnState::Active(receiver) => FederationTxnState::Active(receiver.clone()),
|
||||||
&self,
|
});
|
||||||
key: TxnKey,
|
}
|
||||||
) -> Result<Sender<WrappedTransactionResponse>> {
|
|
||||||
let mut state = self.servername_txnid_active.write();
|
// Check if another transaction from this origin is already running
|
||||||
if state.get(&key).is_some() {
|
let has_active_from_origin = state
|
||||||
debug!(
|
.iter()
|
||||||
origin = ?key.0,
|
.any(|(k, v)| k.0 == key.0 && matches!(v, TxnState::Active(_)));
|
||||||
id = ?key.1,
|
|
||||||
"Origin re-sent already running transaction"
|
if has_active_from_origin {
|
||||||
);
|
|
||||||
Err(Error::BadRequest(
|
|
||||||
LimitExceeded { retry_after: None },
|
|
||||||
"Transaction is already being handled",
|
|
||||||
))
|
|
||||||
} else if state.keys().any(|k| k.0 == key.0) {
|
|
||||||
debug_warn!(
|
debug_warn!(
|
||||||
origin = ?key.0,
|
origin = ?key.0,
|
||||||
"Got concurrent transaction request from an origin with an active transaction"
|
"Got concurrent transaction request from an origin with an active transaction"
|
||||||
);
|
);
|
||||||
Err(Error::BadRequest(
|
return Err(Error::BadRequest(
|
||||||
LimitExceeded { retry_after: None },
|
LimitExceeded { retry_after: None },
|
||||||
"Still processing another transaction from this origin",
|
"Still processing another transaction from this origin",
|
||||||
))
|
));
|
||||||
} else if state.len() >= self.max_active_txns {
|
}
|
||||||
|
|
||||||
|
let max_active_txns = self.services.config.max_concurrent_inbound_transactions;
|
||||||
|
|
||||||
|
// Check if we're at capacity
|
||||||
|
if state.len() >= max_active_txns
|
||||||
|
&& let active_count = state
|
||||||
|
.values()
|
||||||
|
.filter(|v| matches!(v, TxnState::Active(_)))
|
||||||
|
.count() && active_count >= max_active_txns
|
||||||
|
{
|
||||||
warn!(
|
warn!(
|
||||||
active = state.len(),
|
active = active_count,
|
||||||
max = self.max_active_txns,
|
max = max_active_txns,
|
||||||
"Server is overloaded, dropping incoming transaction"
|
"Server is overloaded, dropping incoming transaction"
|
||||||
);
|
);
|
||||||
Err(Error::BadRequest(
|
return Err(Error::BadRequest(
|
||||||
LimitExceeded { retry_after: None },
|
LimitExceeded { retry_after: None },
|
||||||
"Server is overloaded, try again later",
|
"Server is overloaded, try again later",
|
||||||
))
|
));
|
||||||
} else {
|
}
|
||||||
let (tx, rx) = tokio::sync::watch::channel(None);
|
|
||||||
state.insert(key, rx);
|
// Start new transaction
|
||||||
Ok(tx)
|
let (sender, receiver) = tokio::sync::watch::channel(None);
|
||||||
|
state.insert(key, TxnState::Active(receiver.clone()));
|
||||||
|
|
||||||
|
Ok(FederationTxnState::Started { receiver, sender })
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Finishes a transaction by transitioning it from active to cached state.
|
||||||
|
/// Additionally may trigger cleanup of old entries.
|
||||||
|
pub fn finish_federation_txn(
|
||||||
|
&self,
|
||||||
|
key: TxnKey,
|
||||||
|
sender: Sender<Option<send_transaction_message::v1::Response>>,
|
||||||
|
response: send_transaction_message::v1::Response,
|
||||||
|
) {
|
||||||
|
// Check if cleanup might be needed before acquiring the lock
|
||||||
|
let should_try_cleanup = self.should_try_cleanup();
|
||||||
|
|
||||||
|
let mut state = self.federation_txn_state.write();
|
||||||
|
|
||||||
|
// Explicitly set cached first so there is no gap where receivers get a closed
|
||||||
|
// channel
|
||||||
|
state.insert(
|
||||||
|
key,
|
||||||
|
TxnState::Cached(CachedTxnResponse {
|
||||||
|
response: response.clone(),
|
||||||
|
created: SystemTime::now(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
sender
|
||||||
|
.send(Some(response))
|
||||||
|
.expect("couldn't send response to channel");
|
||||||
|
|
||||||
|
// explicitly close
|
||||||
|
drop(sender);
|
||||||
|
|
||||||
|
// This task is dangling, we can try clean caches now
|
||||||
|
if should_try_cleanup {
|
||||||
|
self.cleanup_entries_locked(&mut state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finishes a transaction, removing it from the active txns registry.
|
pub fn remove_federation_txn(&self, key: &TxnKey) {
|
||||||
pub fn finish_federation_txn(&self, key: &TxnKey) {
|
let mut state = self.federation_txn_state.write();
|
||||||
let mut state = self.servername_txnid_active.write();
|
|
||||||
state.remove(key);
|
state.remove(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Gets a cached transaction response, if the given key has a value.
|
/// Checks if enough time has passed since the last cleanup to consider
|
||||||
#[must_use]
|
/// running another. Updates the last cleanup time if returning true.
|
||||||
pub fn get_cached_txn(&self, key: &TxnKey) -> Option<send_transaction_message::v1::Response> {
|
fn should_try_cleanup(&self) -> bool {
|
||||||
let state = self.servername_txnid_response_cache.read();
|
let now = SystemTime::now()
|
||||||
state.get(key).cloned()
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.expect("SystemTime before UNIX_EPOCH")
|
||||||
|
.as_secs();
|
||||||
|
let last = self.last_cleanup.load(Ordering::Relaxed);
|
||||||
|
|
||||||
|
if now.saturating_sub(last) >= CLEANUP_INTERVAL_SECS {
|
||||||
|
// CAS: only update if no one else has updated it since we read
|
||||||
|
self.last_cleanup
|
||||||
|
.compare_exchange(last, now, Ordering::Relaxed, Ordering::Relaxed)
|
||||||
|
.is_ok()
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets a cached transaction response. The existing key will be overwritten
|
/// Cleans up cached entries based on age and count limits.
|
||||||
/// if it exists.
|
///
|
||||||
pub fn set_cached_txn(&self, key: TxnKey, response: send_transaction_message::v1::Response) {
|
/// First removes all cached entries older than the configured max age.
|
||||||
let mut state = self.servername_txnid_response_cache.write();
|
/// Then, if the cache still exceeds the max entry count, removes the oldest
|
||||||
// TODO: time-to-live?
|
/// cached entries until the count is within limits.
|
||||||
state.insert(key, response);
|
///
|
||||||
|
/// Must be called with write lock held on the state map.
|
||||||
|
fn cleanup_entries_locked(&self, state: &mut HashMap<TxnKey, TxnState>) {
|
||||||
|
let max_age_secs = self.services.config.transaction_id_cache_max_age_secs;
|
||||||
|
let max_entries = self.services.config.transaction_id_cache_max_entries;
|
||||||
|
|
||||||
|
// First pass: remove all cached entries older than max age
|
||||||
|
let cutoff = SystemTime::now()
|
||||||
|
.checked_sub(Duration::from_secs(max_age_secs))
|
||||||
|
.unwrap_or(SystemTime::UNIX_EPOCH);
|
||||||
|
|
||||||
|
state.retain(|_, v| match v {
|
||||||
|
| TxnState::Active(_) => true, // Never remove active transactions
|
||||||
|
| TxnState::Cached(cached) => cached.created > cutoff,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Count cached entries
|
||||||
|
let cached_count = state
|
||||||
|
.values()
|
||||||
|
.filter(|v| matches!(v, TxnState::Cached(_)))
|
||||||
|
.count();
|
||||||
|
|
||||||
|
// Second pass: if still over max entries, remove oldest cached entries
|
||||||
|
if cached_count > max_entries {
|
||||||
|
let excess = cached_count.saturating_sub(max_entries);
|
||||||
|
|
||||||
|
// Collect cached entries sorted by age (oldest first)
|
||||||
|
let mut cached_entries: Vec<_> = state
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(k, v)| match v {
|
||||||
|
| TxnState::Cached(cached) => Some((k.clone(), cached.created)),
|
||||||
|
| TxnState::Active(_) => None,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
cached_entries.sort_by(|a, b| a.1.cmp(&b.1));
|
||||||
|
|
||||||
|
// Remove the oldest cached entries to get under the limit
|
||||||
|
for (key, _) in cached_entries.into_iter().take(excess) {
|
||||||
|
state.remove(&key);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user