@@ -26,6 +26,19 @@ pub mod util;
2626const DEFAULT_EXPIRY : i64 = 60 * 60 * 12 ; // 12 hours
2727const ACTUAL_EXPIRY : i64 = 60 * 30 ; // 30 minutes
2828
29+ // Bound how many commands we send in a single Redis pipeline. The multiplexed
30+ // connection's BytesMut write buffer keeps its peak capacity for the life of
31+ // the connection, so larger pipelines cause higher steady-state RSS.
32+ const PIPELINE_CHUNK_SIZE : usize = 25 ;
33+ // Bound how many keys we send in a single MGET. Each MGET response must fit
34+ // into the connection's read buffer, which also retains its peak capacity. At
35+ // ~1 MB per cached value, 32 keys caps any single response at ~32 MB.
36+ const MGET_CHUNK_SIZE : usize = 32 ;
37+ // How long a pooled Redis connection lives before being recycled, regardless
38+ // of activity. Forced recycling is the only way to release the per-connection
39+ // BytesMut peak capacity that builds up under steady load.
40+ const REDIS_MAX_CONN_AGE : Duration = Duration :: from_secs ( 120 ) ;
41+
2942#[ derive( Clone ) ]
3043pub struct RedisPool {
3144 pub url : String ,
@@ -85,14 +98,19 @@ impl RedisPool {
8598 } ) ;
8699
87100 let interval = Duration :: from_secs ( 30 ) ;
88- let max_age = Duration :: from_secs ( 5 * 60 ) ; // 5 minutes
101+ let max_idle = Duration :: from_secs ( 5 * 60 ) ; // 5 minutes
89102 let pool_ref = pool. clone ( ) ;
90103 tokio:: spawn ( async move {
91104 loop {
92105 tokio:: time:: sleep ( interval) . await ;
93- pool_ref
94- . pool
95- . retain ( |_, metrics| metrics. last_used ( ) < max_age) ;
106+ pool_ref. pool . retain ( |_, metrics| {
107+ // Drop connections that have been idle too long, OR that
108+ // are older than REDIS_MAX_CONN_AGE regardless of use.
109+ // The age-based recycle is what releases the per-connection
110+ // BytesMut peak capacity under steady traffic.
111+ metrics. last_used ( ) < max_idle
112+ && metrics. created . elapsed ( ) < REDIS_MAX_CONN_AGE
113+ } ) ;
96114 }
97115 } ) ;
98116
@@ -303,13 +321,16 @@ impl RedisPool {
303321 } )
304322 . collect :: < Vec < _ > > ( ) ;
305323
306- let v = cmd ( "MGET" )
307- . arg ( & args)
308- . query_async :: < Vec < Option < String > > > ( & mut connection)
309- . await ?
310- . into_iter ( )
311- . flatten ( )
312- . collect :: < Vec < _ > > ( ) ;
324+ let mut v = Vec :: new ( ) ;
325+ for chunk in args. chunks ( MGET_CHUNK_SIZE ) {
326+ let part = cmd ( "MGET" )
327+ . arg ( chunk)
328+ . query_async :: < Vec < Option < String > > > (
329+ & mut connection,
330+ )
331+ . await ?;
332+ v. extend ( part. into_iter ( ) . flatten ( ) ) ;
333+ }
313334 Ok :: < _ , DatabaseError > ( v)
314335 }
315336 . instrument ( info_span ! ( "get slug ids" ) )
@@ -331,19 +352,20 @@ impl RedisPool {
331352 . map ( |x| format ! ( "{}_{namespace}:{x}" , self . meta_namespace) )
332353 . collect :: < Vec < _ > > ( ) ;
333354
334- let cached_values = cmd ( "MGET" )
335- . arg ( & args)
336- . query_async :: < Vec < Option < String > > > ( & mut connection)
337- . await ?
338- . into_iter ( )
339- . filter_map ( |x| {
355+ let mut cached_values = HashMap :: new ( ) ;
356+ for chunk in args. chunks ( MGET_CHUNK_SIZE ) {
357+ let part = cmd ( "MGET" )
358+ . arg ( chunk)
359+ . query_async :: < Vec < Option < String > > > ( & mut connection)
360+ . await ?;
361+ cached_values. extend ( part. into_iter ( ) . filter_map ( |x| {
340362 x. and_then ( |val| {
341363 serde_json:: from_str :: < RedisValue < T , K , S > > ( & val)
342364 . ok ( )
343365 } )
344366 . map ( |val| ( val. key . clone ( ) , val) )
345- } )
346- . collect :: < HashMap < _ , _ > > ( ) ;
367+ } ) ) ;
368+ }
347369
348370 Ok :: < _ , DatabaseError > ( ( cached_values, ids) )
349371 }
@@ -440,6 +462,8 @@ impl RedisPool {
440462 let mut return_values = HashMap :: new ( ) ;
441463
442464 let mut pipe = redis_pipe ( ) ;
465+ let mut pipe_cmds: usize = 0 ;
466+ let mut connection = self . pool . get ( ) . await ?;
443467 // Doesn't need to be atomic
444468
445469 if !vals. is_empty ( ) {
@@ -459,6 +483,7 @@ impl RedisPool {
459483 serde_json:: to_string ( & value) ?,
460484 DEFAULT_EXPIRY as u64 ,
461485 ) ;
486+ pipe_cmds += 1 ;
462487
463488 if let Some ( slug) = slug {
464489 ids. remove ( & slug. to_string ( ) ) ;
@@ -478,46 +503,31 @@ impl RedisPool {
478503 key. to_string ( ) ,
479504 DEFAULT_EXPIRY as u64 ,
480505 ) ;
481-
482- /*
483- if let Some(_sentinel) =
484- cache_writers.remove(&actual_slug)
485- {
486- // drop it
487- }
488- */
506+ pipe_cmds += 1 ;
489507 }
490508 }
491509
492510 let key_str = key. to_string ( ) ;
493511 ids. remove ( & key_str) ;
494512
495- /*
496- if let Some(_sentinel) = cache_writers.remove(&key_str)
497- {
498- // drop it
499- }
500- */
501-
502513 if let Ok ( value) = key_str. parse :: < u64 > ( ) {
503514 let base62 = to_base62 ( value) ;
504515 ids. remove ( & base62) ;
505-
506- /*
507- if let Some(_sentinel) =
508- cache_writers.remove(&base62)
509- {
510- // drop it
511- }
512- */
513516 }
514517
515518 return_values. insert ( key, value) ;
519+
520+ if pipe_cmds >= PIPELINE_CHUNK_SIZE {
521+ pipe. query_async :: < ( ) > ( & mut connection) . await ?;
522+ pipe = redis_pipe ( ) ;
523+ pipe_cmds = 0 ;
524+ }
516525 }
517526 }
518527
519- let mut connection = self . pool . get ( ) . await ?;
520- pipe. query_async :: < ( ) > ( & mut connection) . await ?;
528+ if pipe_cmds > 0 {
529+ pipe. query_async :: < ( ) > ( & mut connection) . await ?;
530+ }
521531
522532 drop ( cache_writers) ;
523533
0 commit comments