44use std:: sync:: Arc ;
55use std:: time:: Duration ;
66
7+ use moka:: notification:: RemovalCause ;
78use moka:: ops:: compute:: { CompResult , Op } ;
89use moka:: sync:: Cache ;
910
@@ -12,11 +13,35 @@ use crate::routing_state::{RoutingTargetKey, StargateState};
1213use super :: expression:: RejectionError ;
1314use super :: target_state:: LoadBalancerDefinition ;
1415
15- struct DynamicConfigEntry {
16+ #[ derive( Clone ) ]
17+ struct Configuration {
1618 expression : String ,
1719 definition : LoadBalancerDefinition ,
1820}
1921
22+ // Gateway replicas cache model metadata separately, so after an owner update they send the
23+ // old and new expressions alternately until their caches converge. Keeping the previous
24+ // expression makes that window a run of hits instead of a rebuild per switch.
25+ struct DynamicConfigEntry {
26+ current : Configuration ,
27+ previous : Option < Configuration > ,
28+ }
29+
30+ impl DynamicConfigEntry {
31+ fn definition_for ( & self , header : & str ) -> Option < & LoadBalancerDefinition > {
32+ std:: iter:: once ( & self . current )
33+ . chain ( & self . previous )
34+ . find ( |configuration| configuration. expression == header)
35+ . map ( |configuration| & configuration. definition )
36+ }
37+
38+ // A replacement carries `current` into the new entry as its `previous`.
39+ fn dropped ( & self , cause : RemovalCause ) -> impl Iterator < Item = & Configuration > {
40+ let current = ( cause != RemovalCause :: Replaced ) . then_some ( & self . current ) ;
41+ current. into_iter ( ) . chain ( & self . previous )
42+ }
43+ }
44+
2045#[ derive( Clone , Copy , Debug , PartialEq , Eq ) ]
2146pub ( crate ) enum Outcome {
2247 Hit ,
@@ -49,9 +74,11 @@ impl DynamicConfigCache {
4974 . max_capacity ( max_entries)
5075 . time_to_idle ( idle)
5176 . eviction_listener (
52- move |target : Arc < RoutingTargetKey > , entry : Arc < DynamicConfigEntry > , _| {
53- entry. definition . retire ( ) ;
54- state. forget_load_balancer_instance ( & target, & entry. definition ) ;
77+ move |target : Arc < RoutingTargetKey > , entry : Arc < DynamicConfigEntry > , cause| {
78+ for configuration in entry. dropped ( cause) {
79+ configuration. definition . retire ( ) ;
80+ state. forget_load_balancer_instance ( & target, & configuration. definition ) ;
81+ }
5582 } ,
5683 )
5784 . build ( ) ,
@@ -66,23 +93,24 @@ impl DynamicConfigCache {
6693 ) -> Result < ( LoadBalancerDefinition , Outcome ) , RejectionError > {
6794 // Hits skip the per-key compute lock so they never wait behind a rebuild.
6895 if let Some ( entry) = self . entries . get ( target)
69- && entry . expression == header
96+ && let Some ( definition ) = entry . definition_for ( header)
7097 {
71- return Ok ( ( entry . definition . clone ( ) , Outcome :: Hit ) ) ;
98+ return Ok ( ( definition. clone ( ) , Outcome :: Hit ) ) ;
7299 }
73100 let result = self
74101 . entries
75102 . entry_by_ref ( target)
76- . and_try_compute_with ( |current| {
77- if current
78- . as_ref ( )
79- . is_some_and ( |entry| entry. value ( ) . expression == header)
80- {
103+ . and_try_compute_with ( |existing| {
104+ let existing = existing. as_ref ( ) . map ( |entry| entry. value ( ) ) ;
105+ if existing. is_some_and ( |entry| entry. definition_for ( header) . is_some ( ) ) {
81106 return Ok ( Op :: Nop ) ;
82107 }
83108 Ok ( Op :: Put ( Arc :: new ( DynamicConfigEntry {
84- expression : header. to_owned ( ) ,
85- definition : build ( ) ?,
109+ current : Configuration {
110+ expression : header. to_owned ( ) ,
111+ definition : build ( ) ?,
112+ } ,
113+ previous : existing. map ( |entry| entry. current . clone ( ) ) ,
86114 } ) ) )
87115 } ) ?;
88116 let ( entry, outcome) = match result {
@@ -94,7 +122,12 @@ impl DynamicConfigCache {
94122 unreachable ! ( "dynamic config compute returned no entry" )
95123 }
96124 } ;
97- Ok ( ( entry. into_value ( ) . definition . clone ( ) , outcome) )
125+ let definition = entry
126+ . into_value ( )
127+ . definition_for ( header)
128+ . cloned ( )
129+ . expect ( "a hit matched this expression and a build stored it as current" ) ;
130+ Ok ( ( definition, outcome) )
98131 }
99132
100133 #[ cfg( test) ]
@@ -207,7 +240,7 @@ mod tests {
207240 }
208241
209242 #[ tokio:: test]
210- async fn test_resolve_changed_bytes_forgets_only_superseded_instance ( ) {
243+ async fn test_resolve_keeps_previous_expression_and_forgets_older_ones ( ) {
211244 let state = Arc :: new ( StargateState :: new ( ) ) ;
212245 let target = RoutingTargetKey :: new ( None , "model" ) ;
213246 let snapshot = register_target ( & state, & target) . await ;
@@ -219,27 +252,54 @@ mod tests {
219252 . resolve ( & target, EXPRESSION , || compile ( EXPRESSION ) )
220253 . unwrap ( ) ;
221254 let first_instance = snapshot. load_balancers ( ) . load_balancer ( & first) ;
222- assert_eq ! ( snapshot. load_balancers( ) . instance_count( ) , 2 ) ;
223255
224- // Equivalent values with different bytes must still replace the entry .
256+ // Equivalent values with different bytes still build a new configuration .
225257 let changed = "ROUND_ROBIN; require_input_tokens=false" ;
226258 let ( second, outcome) = cache
227259 . resolve ( & target, changed, || compile ( changed) )
228260 . unwrap ( ) ;
229261 assert_eq ! ( outcome, Outcome :: Rebuild ) ;
230262 assert_ne ! ( first, second) ;
231- assert_eq ! ( snapshot. load_balancers( ) . instance_count( ) , 1 ) ;
232263 let second_instance = snapshot. load_balancers ( ) . load_balancer ( & second) ;
233- assert ! ( !Arc :: ptr_eq( & first_instance, & second_instance) ) ;
264+ assert_eq ! ( snapshot. load_balancers( ) . instance_count( ) , 3 ) ;
265+
266+ // Gateway replicas alternating between the two values only hit.
267+ for _ in 0 ..4 {
268+ for ( header, definition, instance) in [
269+ ( EXPRESSION , & first, & first_instance) ,
270+ ( changed, & second, & second_instance) ,
271+ ] {
272+ let ( resolved, outcome) = cache
273+ . resolve ( & target, header, || panic ! ( "alternation rebuilt {header}" ) )
274+ . unwrap ( ) ;
275+ assert_eq ! ( ( & resolved, outcome) , ( definition, Outcome :: Hit ) ) ;
276+ assert ! ( Arc :: ptr_eq(
277+ instance,
278+ & snapshot. load_balancers( ) . load_balancer( & resolved)
279+ ) ) ;
280+ }
281+ }
282+
283+ // A third value keeps the current one as previous and forgets the older one.
284+ let third = "round-robin;require_input_tokens=true" ;
285+ let ( third_definition, outcome) = cache. resolve ( & target, third, || compile ( third) ) . unwrap ( ) ;
286+ assert_eq ! ( outcome, Outcome :: Rebuild ) ;
287+ assert ! ( !snapshot. load_balancers( ) . contains( & first) ) ;
288+ assert ! ( snapshot. load_balancers( ) . contains( & second) ) ;
289+ assert_eq ! (
290+ cache. resolve( & target, changed, || panic!( "previous rebuilt" ) ) ,
291+ Ok ( ( second. clone( ) , Outcome :: Hit ) )
292+ ) ;
293+ assert_ne ! ( third_definition, second) ;
234294 assert ! ( Arc :: ptr_eq(
235295 & static_instance,
236296 & snapshot. load_balancers( ) . load_balancer( & static_definition)
237297 ) ) ;
238298
239- // A request paused after resolution must not reinsert an evicted definition.
299+ // A request paused after resolution must not reinsert a forgotten definition.
240300 let _in_flight = snapshot. load_balancers ( ) . load_balancer ( & first) ;
241- assert_eq ! ( snapshot. load_balancers( ) . instance_count( ) , 2 ) ;
242301 assert ! ( !snapshot. load_balancers( ) . contains( & first) ) ;
302+ assert_eq ! ( snapshot. load_balancers( ) . instance_count( ) , 2 ) ;
243303 }
244304
245305 #[ tokio:: test]
@@ -328,16 +388,19 @@ mod tests {
328388 let snapshot = register_target ( & state, & target) . await ;
329389 let cache =
330390 DynamicConfigCache :: new ( state, Duration :: from_secs ( 60 ) , DYNAMIC_CONFIG_MAX_ENTRIES ) ;
331- let ( mut current , _) = cache
391+ let ( mut older , _) = cache
332392 . resolve ( & target, EXPRESSION , || compile ( EXPRESSION ) )
333393 . unwrap ( ) ;
334- for value in 1 ..=32 {
394+ let header = "round-robin;max_input_work_seconds=1" ;
395+ let ( mut newer, _) = cache. resolve ( & target, header, || compile ( header) ) . unwrap ( ) ;
396+ for value in 2 ..=33 {
335397 let barrier = Barrier :: new ( 2 ) ;
336398 let header = format ! ( "round-robin;max_input_work_seconds={value}" ) ;
399+ // The next rebuild drops `older`; race a selection on it against that rebuild.
337400 let next = std:: thread:: scope ( |scope| {
338401 let selection = scope. spawn ( || {
339402 barrier. wait ( ) ;
340- snapshot. load_balancers ( ) . load_balancer ( & current )
403+ snapshot. load_balancers ( ) . load_balancer ( & older )
341404 } ) ;
342405 barrier. wait ( ) ;
343406 let ( next, outcome) = cache
@@ -347,9 +410,10 @@ mod tests {
347410 let _in_flight = selection. join ( ) . unwrap ( ) ;
348411 next
349412 } ) ;
350- assert ! ( !snapshot. load_balancers( ) . contains( & current ) ) ;
413+ assert ! ( !snapshot. load_balancers( ) . contains( & older ) ) ;
351414 assert_eq ! ( snapshot. load_balancers( ) . instance_count( ) , 0 ) ;
352- current = next;
415+ older = newer;
416+ newer = next;
353417 }
354418 }
355419
@@ -364,9 +428,16 @@ mod tests {
364428 . resolve ( & target, EXPRESSION , || compile ( EXPRESSION ) )
365429 . unwrap ( ) ;
366430 let _instance = snapshot. load_balancers ( ) . load_balancer ( & first) ;
431+ let changed = "round-robin;require_input_tokens=true" ;
432+ let ( previous_holder, _) = cache
433+ . resolve ( & target, changed, || compile ( changed) )
434+ . unwrap ( ) ;
435+ let _instance = snapshot. load_balancers ( ) . load_balancer ( & previous_holder) ;
436+ assert_eq ! ( snapshot. load_balancers( ) . instance_count( ) , 2 ) ;
367437 cache. run_pending_tasks ( ) ;
368438 tokio:: time:: sleep ( Duration :: from_millis ( 100 ) ) . await ;
369439 cache. run_pending_tasks ( ) ;
440+ // Expiry forgets both the current and the previous configuration.
370441 assert ! ( cache. entries. get( & target) . is_none( ) ) ;
371442 assert_eq ! ( snapshot. load_balancers( ) . instance_count( ) , 0 ) ;
372443 let ( second, outcome) = cache
@@ -456,7 +527,7 @@ mod tests {
456527 cache
457528 . entries
458529 . get( & target)
459- . map( |entry| entry. definition. clone( ) ) ,
530+ . map( |entry| entry. current . definition. clone( ) ) ,
460531 before
461532 ) ;
462533 }
0 commit comments