fix: labrinth memory leaks (#5980)
This commit is contained in:
@@ -26,6 +26,19 @@ pub mod util;
|
|||||||
const DEFAULT_EXPIRY: i64 = 60 * 60 * 12; // 12 hours
|
const DEFAULT_EXPIRY: i64 = 60 * 60 * 12; // 12 hours
|
||||||
const ACTUAL_EXPIRY: i64 = 60 * 30; // 30 minutes
|
const ACTUAL_EXPIRY: i64 = 60 * 30; // 30 minutes
|
||||||
|
|
||||||
|
// Bound how many commands we send in a single Redis pipeline. The multiplexed
|
||||||
|
// connection's BytesMut write buffer keeps its peak capacity for the life of
|
||||||
|
// the connection, so larger pipelines cause higher steady-state RSS.
|
||||||
|
const PIPELINE_CHUNK_SIZE: usize = 25;
|
||||||
|
// Bound how many keys we send in a single MGET. Each MGET response must fit
|
||||||
|
// into the connection's read buffer, which also retains its peak capacity. At
|
||||||
|
// ~1 MB per cached value, 32 keys caps any single response at ~32 MB.
|
||||||
|
const MGET_CHUNK_SIZE: usize = 32;
|
||||||
|
// How long a pooled Redis connection lives before being recycled, regardless
|
||||||
|
// of activity. Forced recycling is the only way to release the per-connection
|
||||||
|
// BytesMut peak capacity that builds up under steady load.
|
||||||
|
const REDIS_MAX_CONN_AGE: Duration = Duration::from_secs(120);
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct RedisPool {
|
pub struct RedisPool {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
@@ -85,14 +98,19 @@ impl RedisPool {
|
|||||||
});
|
});
|
||||||
|
|
||||||
let interval = Duration::from_secs(30);
|
let interval = Duration::from_secs(30);
|
||||||
let max_age = Duration::from_secs(5 * 60); // 5 minutes
|
let max_idle = Duration::from_secs(5 * 60); // 5 minutes
|
||||||
let pool_ref = pool.clone();
|
let pool_ref = pool.clone();
|
||||||
tokio::spawn(async move {
|
tokio::spawn(async move {
|
||||||
loop {
|
loop {
|
||||||
tokio::time::sleep(interval).await;
|
tokio::time::sleep(interval).await;
|
||||||
pool_ref
|
pool_ref.pool.retain(|_, metrics| {
|
||||||
.pool
|
// Drop connections that have been idle too long, OR that
|
||||||
.retain(|_, metrics| metrics.last_used() < max_age);
|
// are older than REDIS_MAX_CONN_AGE regardless of use.
|
||||||
|
// The age-based recycle is what releases the per-connection
|
||||||
|
// BytesMut peak capacity under steady traffic.
|
||||||
|
metrics.last_used() < max_idle
|
||||||
|
&& metrics.created.elapsed() < REDIS_MAX_CONN_AGE
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -303,13 +321,16 @@ impl RedisPool {
|
|||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let v = cmd("MGET")
|
let mut v = Vec::new();
|
||||||
.arg(&args)
|
for chunk in args.chunks(MGET_CHUNK_SIZE) {
|
||||||
.query_async::<Vec<Option<String>>>(&mut connection)
|
let part = cmd("MGET")
|
||||||
.await?
|
.arg(chunk)
|
||||||
.into_iter()
|
.query_async::<Vec<Option<String>>>(
|
||||||
.flatten()
|
&mut connection,
|
||||||
.collect::<Vec<_>>();
|
)
|
||||||
|
.await?;
|
||||||
|
v.extend(part.into_iter().flatten());
|
||||||
|
}
|
||||||
Ok::<_, DatabaseError>(v)
|
Ok::<_, DatabaseError>(v)
|
||||||
}
|
}
|
||||||
.instrument(info_span!("get slug ids"))
|
.instrument(info_span!("get slug ids"))
|
||||||
@@ -331,19 +352,20 @@ impl RedisPool {
|
|||||||
.map(|x| format!("{}_{namespace}:{x}", self.meta_namespace))
|
.map(|x| format!("{}_{namespace}:{x}", self.meta_namespace))
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
let cached_values = cmd("MGET")
|
let mut cached_values = HashMap::new();
|
||||||
.arg(&args)
|
for chunk in args.chunks(MGET_CHUNK_SIZE) {
|
||||||
.query_async::<Vec<Option<String>>>(&mut connection)
|
let part = cmd("MGET")
|
||||||
.await?
|
.arg(chunk)
|
||||||
.into_iter()
|
.query_async::<Vec<Option<String>>>(&mut connection)
|
||||||
.filter_map(|x| {
|
.await?;
|
||||||
|
cached_values.extend(part.into_iter().filter_map(|x| {
|
||||||
x.and_then(|val| {
|
x.and_then(|val| {
|
||||||
serde_json::from_str::<RedisValue<T, K, S>>(&val)
|
serde_json::from_str::<RedisValue<T, K, S>>(&val)
|
||||||
.ok()
|
.ok()
|
||||||
})
|
})
|
||||||
.map(|val| (val.key.clone(), val))
|
.map(|val| (val.key.clone(), val))
|
||||||
})
|
}));
|
||||||
.collect::<HashMap<_, _>>();
|
}
|
||||||
|
|
||||||
Ok::<_, DatabaseError>((cached_values, ids))
|
Ok::<_, DatabaseError>((cached_values, ids))
|
||||||
}
|
}
|
||||||
@@ -440,6 +462,8 @@ impl RedisPool {
|
|||||||
let mut return_values = HashMap::new();
|
let mut return_values = HashMap::new();
|
||||||
|
|
||||||
let mut pipe = redis_pipe();
|
let mut pipe = redis_pipe();
|
||||||
|
let mut pipe_cmds: usize = 0;
|
||||||
|
let mut connection = self.pool.get().await?;
|
||||||
// Doesn't need to be atomic
|
// Doesn't need to be atomic
|
||||||
|
|
||||||
if !vals.is_empty() {
|
if !vals.is_empty() {
|
||||||
@@ -459,6 +483,7 @@ impl RedisPool {
|
|||||||
serde_json::to_string(&value)?,
|
serde_json::to_string(&value)?,
|
||||||
DEFAULT_EXPIRY as u64,
|
DEFAULT_EXPIRY as u64,
|
||||||
);
|
);
|
||||||
|
pipe_cmds += 1;
|
||||||
|
|
||||||
if let Some(slug) = slug {
|
if let Some(slug) = slug {
|
||||||
ids.remove(&slug.to_string());
|
ids.remove(&slug.to_string());
|
||||||
@@ -478,46 +503,31 @@ impl RedisPool {
|
|||||||
key.to_string(),
|
key.to_string(),
|
||||||
DEFAULT_EXPIRY as u64,
|
DEFAULT_EXPIRY as u64,
|
||||||
);
|
);
|
||||||
|
pipe_cmds += 1;
|
||||||
/*
|
|
||||||
if let Some(_sentinel) =
|
|
||||||
cache_writers.remove(&actual_slug)
|
|
||||||
{
|
|
||||||
// drop it
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let key_str = key.to_string();
|
let key_str = key.to_string();
|
||||||
ids.remove(&key_str);
|
ids.remove(&key_str);
|
||||||
|
|
||||||
/*
|
|
||||||
if let Some(_sentinel) = cache_writers.remove(&key_str)
|
|
||||||
{
|
|
||||||
// drop it
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
if let Ok(value) = key_str.parse::<u64>() {
|
if let Ok(value) = key_str.parse::<u64>() {
|
||||||
let base62 = to_base62(value);
|
let base62 = to_base62(value);
|
||||||
ids.remove(&base62);
|
ids.remove(&base62);
|
||||||
|
|
||||||
/*
|
|
||||||
if let Some(_sentinel) =
|
|
||||||
cache_writers.remove(&base62)
|
|
||||||
{
|
|
||||||
// drop it
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return_values.insert(key, value);
|
return_values.insert(key, value);
|
||||||
|
|
||||||
|
if pipe_cmds >= PIPELINE_CHUNK_SIZE {
|
||||||
|
pipe.query_async::<()>(&mut connection).await?;
|
||||||
|
pipe = redis_pipe();
|
||||||
|
pipe_cmds = 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut connection = self.pool.get().await?;
|
if pipe_cmds > 0 {
|
||||||
pipe.query_async::<()>(&mut connection).await?;
|
pipe.query_async::<()>(&mut connection).await?;
|
||||||
|
}
|
||||||
|
|
||||||
drop(cache_writers);
|
drop(cache_writers);
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
//
|
//
|
||||||
// TODO: PR something into sentry_actix to let us customize this
|
// TODO: PR something into sentry_actix to let us customize this
|
||||||
|
|
||||||
use std::{borrow::Cow, pin::Pin, rc::Rc};
|
use std::{borrow::Cow, pin::Pin, rc::Rc, sync::Arc};
|
||||||
|
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
StatusCode,
|
StatusCode,
|
||||||
@@ -83,7 +83,11 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
let hub = Hub::current();
|
// Fork a Hub per request so the scope mutations below (event processor
|
||||||
|
// capturing the request, span attachment) live only for this request
|
||||||
|
// and are dropped when the future completes. Mutating the shared
|
||||||
|
// thread-local hub instead would leak one event processor per request.
|
||||||
|
let hub = Arc::new(Hub::new_from_top(Hub::main()));
|
||||||
let client = hub.client();
|
let client = hub.client();
|
||||||
|
|
||||||
let max_request_body_size = client
|
let max_request_body_size = client
|
||||||
@@ -110,7 +114,6 @@ where
|
|||||||
);
|
);
|
||||||
|
|
||||||
let transaction = hub.start_transaction(ctx);
|
let transaction = hub.start_transaction(ctx);
|
||||||
transaction.set_request(sentry_req.clone());
|
|
||||||
transaction.set_origin("auto.http.actix");
|
transaction.set_origin("auto.http.actix");
|
||||||
transaction
|
transaction
|
||||||
};
|
};
|
||||||
@@ -127,13 +130,13 @@ where
|
|||||||
sentry_req.data = Some(capture_request_body(&mut req).await);
|
sentry_req.data = Some(capture_request_body(&mut req).await);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parent_span = hub.configure_scope(|scope| {
|
transaction.set_request(sentry_req.clone());
|
||||||
let parent_span = scope.get_span();
|
|
||||||
|
hub.configure_scope(|scope| {
|
||||||
scope.set_span(Some(transaction.clone().into()));
|
scope.set_span(Some(transaction.clone().into()));
|
||||||
scope.add_event_processor(move |event| {
|
scope.add_event_processor(move |event| {
|
||||||
Some(process_event(event, &sentry_req))
|
Some(process_event(event, &sentry_req))
|
||||||
});
|
});
|
||||||
parent_span
|
|
||||||
});
|
});
|
||||||
|
|
||||||
let fut =
|
let fut =
|
||||||
@@ -150,7 +153,6 @@ where
|
|||||||
transaction.set_status(status);
|
transaction.set_status(status);
|
||||||
}
|
}
|
||||||
transaction.finish();
|
transaction.finish();
|
||||||
hub.configure_scope(|scope| scope.set_span(parent_span));
|
|
||||||
return Err(actix_err);
|
return Err(actix_err);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -167,7 +169,6 @@ where
|
|||||||
transaction.set_status(status);
|
transaction.set_status(status);
|
||||||
}
|
}
|
||||||
transaction.finish();
|
transaction.finish();
|
||||||
hub.configure_scope(|scope| scope.set_span(parent_span));
|
|
||||||
|
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user