11use std:: fmt:: { Debug , Formatter } ;
22use std:: io;
33use std:: net:: IpAddr ;
4- use std:: path:: PathBuf ;
4+ use std:: path:: { Path , PathBuf } ;
55use std:: sync:: Arc ;
66use std:: sync:: atomic:: { AtomicUsize , Ordering } ;
77use std:: time:: { Duration , Instant } ;
@@ -74,6 +74,7 @@ pub struct UpstreamLoadBalancer {
7474 queue_policy : LoadBalanceQueueConfig ,
7575 queue_waiting : Arc < AtomicUsize > ,
7676 runtime_state_file : Option < Arc < PathBuf > > ,
77+ runtime_state_save_lock : Arc < std:: sync:: Mutex < ( ) > > ,
7778 round_robin_cursor : Arc < AtomicUsize > ,
7879 state_prune_counter : Arc < AtomicUsize > ,
7980 counters : Arc < BackendConnectionCounters > ,
@@ -663,12 +664,14 @@ impl UpstreamLoadBalancer {
663664 let selected = self . prepare_selected ( selected, persistence_outcome) ?;
664665 if let ( Some ( persistence) , Some ( key) ) = ( & self . persistence , persistence_key) {
665666 persistence. record ( key, backend_key ( & selected. backend ) ) ;
666- self . save_runtime_state_if_configured ( "persistence_record" ) ;
667+ self . save_runtime_state_if_configured_in_background ( "persistence_record" ) ;
667668 } else if let Some ( persistence) = & self . persistence
668669 && let Some ( ( key, cookie) ) = persistence. new_managed_cookie ( )
669670 {
670671 persistence. record ( & key, backend_key ( & selected. backend ) ) ;
671- self . save_runtime_state_if_configured ( "managed_cookie_persistence_record" ) ;
672+ self . save_runtime_state_if_configured_in_background (
673+ "managed_cookie_persistence_record" ,
674+ ) ;
672675 let mut selected = selected;
673676 selected. managed_affinity_cookie = Some ( cookie) ;
674677 return Some ( selected) ;
@@ -757,6 +760,7 @@ impl UpstreamLoadBalancer {
757760 queue_policy : config. load_balance . queue . clone ( ) ,
758761 queue_waiting : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
759762 runtime_state_file : config. load_balance . runtime_state_file . clone ( ) . map ( Arc :: new) ,
763+ runtime_state_save_lock : Arc :: new ( std:: sync:: Mutex :: new ( ( ) ) ) ,
760764 round_robin_cursor : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
761765 state_prune_counter : Arc :: new ( AtomicUsize :: new ( 0 ) ) ,
762766 counters : Arc :: new ( BackendConnectionCounters :: default ( ) ) ,
@@ -775,27 +779,23 @@ impl UpstreamLoadBalancer {
775779 return ;
776780 }
777781 let backends = self . inner . backends ( ) ;
778- let live_connection_keys = backends
779- . iter ( )
780- . map ( backend_key)
781- . collect :: < std:: collections:: HashSet < _ > > ( ) ;
782- let live_policy_keys = backends
782+ let live_keys = backends
783783 . iter ( )
784784 . map ( backend_key)
785785 . collect :: < std:: collections:: HashSet < _ > > ( ) ;
786- self . counters . prune_stale ( & live_connection_keys ) ;
787- self . backend_policy . prune_stale ( & live_policy_keys ) ;
786+ self . counters . prune_stale ( & live_keys ) ;
787+ self . backend_policy . prune_stale ( & live_keys ) ;
788788 if let Some ( persistence) = & self . persistence {
789- persistence. prune_stale_for_live_backends ( & live_policy_keys ) ;
789+ persistence. prune_stale_for_live_backends ( & live_keys ) ;
790790 }
791791 if let Some ( passive_health) = & self . passive_health {
792- passive_health. prune_stale ( & live_connection_keys ) ;
792+ passive_health. prune_stale ( & live_keys ) ;
793793 }
794794 if let Some ( slow_start) = & self . slow_start {
795- slow_start. prune_stale ( & live_connection_keys ) ;
795+ slow_start. prune_stale ( & live_keys ) ;
796796 }
797797 if let Some ( latency) = self . inner . latency_state ( ) {
798- latency. prune_stale ( & live_connection_keys ) ;
798+ latency. prune_stale ( & live_keys ) ;
799799 }
800800 }
801801
@@ -1057,19 +1057,29 @@ impl UpstreamLoadBalancer {
10571057 "unsupported load balancer runtime state version" ,
10581058 ) ) ;
10591059 }
1060- self . backend_policy
1061- . restore_runtime_snapshot ( & snapshot. runtime_overrides )
1060+ let prepared_policy = self
1061+ . backend_policy
1062+ . prepare_runtime_snapshot ( & snapshot. runtime_overrides )
10621063 . map_err ( |error| io:: Error :: new ( io:: ErrorKind :: InvalidData , error) ) ?;
10631064 let live_keys = self . live_backend_keys ( ) ;
1064- let persistence_entries = if let ( Some ( persistence) , Some ( snapshot) ) =
1065+ let prepared_persistence = if let ( Some ( persistence) , Some ( snapshot) ) =
10651066 ( & self . persistence , & snapshot. persistence )
10661067 {
1067- persistence
1068- . restore_snapshot ( snapshot, & live_keys)
1069- . map_err ( |error| io:: Error :: new ( io:: ErrorKind :: InvalidData , error) ) ?
1068+ Some (
1069+ persistence
1070+ . prepare_snapshot ( snapshot, & live_keys)
1071+ . map_err ( |error| io:: Error :: new ( io:: ErrorKind :: InvalidData , error) ) ?,
1072+ )
10701073 } else {
1071- 0
1074+ None
10721075 } ;
1076+ let persistence_entries = prepared_persistence
1077+ . as_ref ( )
1078+ . map_or ( 0 , |snapshot| snapshot. restored_entries ( ) ) ;
1079+ self . backend_policy . commit_runtime_snapshot ( prepared_policy) ;
1080+ if let ( Some ( persistence) , Some ( snapshot) ) = ( & self . persistence , prepared_persistence) {
1081+ persistence. commit_snapshot ( snapshot) ;
1082+ }
10731083 Ok ( LoadBalancerRuntimeStateRestore {
10741084 persistence_entries,
10751085 } )
@@ -1108,25 +1118,51 @@ impl UpstreamLoadBalancer {
11081118 let Some ( path) = & self . runtime_state_file else {
11091119 return ;
11101120 } ;
1121+ let _guard = self
1122+ . runtime_state_save_lock
1123+ . lock ( )
1124+ . unwrap_or_else ( |poisoned| poisoned. into_inner ( ) ) ;
11111125 let snapshot = self . runtime_state_snapshot ( ) ;
1112- match write_runtime_state_file ( path, & snapshot) {
1113- Ok ( ( ) ) => log :: debug! (
1114- target : "fluxheim::load_balancer" ,
1115- "load balancer runtime state saved path={} reason={}" ,
1116- path . display ( ) ,
1117- reason
1118- ) ,
1119- Err ( error ) => log :: warn! (
1120- target : "fluxheim::security" ,
1121- "load balancer runtime state save failed path={} reason={} error={}" ,
1122- path . display ( ) ,
1123- reason ,
1124- error
1125- ) ,
1126+ write_runtime_state_snapshot ( path, & snapshot, reason ) ;
1127+ }
1128+
1129+ fn save_runtime_state_if_configured_in_background ( & self , reason : & ' static str ) {
1130+ if self . runtime_state_file . is_none ( ) {
1131+ return ;
1132+ }
1133+ let balancer = self . clone ( ) ;
1134+ if let Ok ( handle ) = tokio :: runtime :: Handle :: try_current ( ) {
1135+ handle . spawn_blocking ( move || {
1136+ balancer . save_runtime_state_if_configured ( reason ) ;
1137+ } ) ;
1138+ } else {
1139+ self . save_runtime_state_if_configured ( reason ) ;
11261140 }
11271141 }
11281142}
11291143
1144+ fn write_runtime_state_snapshot (
1145+ path : & Path ,
1146+ snapshot : & LoadBalancerRuntimeStateSnapshot ,
1147+ reason : & str ,
1148+ ) {
1149+ match write_runtime_state_file ( path, snapshot) {
1150+ Ok ( ( ) ) => log:: debug!(
1151+ target: "fluxheim::load_balancer" ,
1152+ "load balancer runtime state saved path={} reason={}" ,
1153+ path. display( ) ,
1154+ reason
1155+ ) ,
1156+ Err ( error) => log:: warn!(
1157+ target: "fluxheim::security" ,
1158+ "load balancer runtime state save failed path={} reason={} error={}" ,
1159+ path. display( ) ,
1160+ reason,
1161+ error
1162+ ) ,
1163+ }
1164+ }
1165+
11301166fn backend_runtime_status_eligible ( backend : & LoadBalancerBackendRuntimeStats ) -> bool {
11311167 backend. ready
11321168 && !backend. disabled
@@ -1914,6 +1950,64 @@ mod tests {
19141950 assert_eq ! ( selected. backend. addr. to_string( ) , "127.0.0.1:3000" ) ;
19151951 }
19161952
1953+ #[ test]
1954+ fn runtime_state_file_restore_is_all_or_nothing ( ) {
1955+ install_test_crypto_provider ( ) ;
1956+ let dir = unique_temp_path ( "lb-runtime-state-atomic-restore" ) ;
1957+ std:: fs:: create_dir_all ( & dir) . unwrap ( ) ;
1958+ let state_file = safe_child_path ( & dir, "lb-state.json" ) ;
1959+ let disabled_key = backend_key ( & FluxBackend :: new ( "127.0.0.1:3001" ) . unwrap ( ) ) ;
1960+ std:: fs:: write (
1961+ & state_file,
1962+ format ! (
1963+ r#"{{
1964+ "version": 1,
1965+ "runtime_overrides": {{
1966+ "states": [
1967+ {{
1968+ "key": {disabled_key},
1969+ "state": "disabled",
1970+ "changed_at_unix_secs": 1
1971+ }}
1972+ ],
1973+ "weights": []
1974+ }},
1975+ "persistence": {{
1976+ "entries": [
1977+ {{
1978+ "key": [],
1979+ "backend_key": {disabled_key},
1980+ "ttl_remaining_secs": 60
1981+ }}
1982+ ]
1983+ }}
1984+ }}"#
1985+ ) ,
1986+ )
1987+ . unwrap ( ) ;
1988+ let config = ProxyConfig {
1989+ upstreams : vec ! [ "127.0.0.1:3000" . to_owned( ) , "127.0.0.1:3001" . to_owned( ) ] ,
1990+ load_balance : LoadBalanceConfig {
1991+ runtime_state_file : Some ( state_file) ,
1992+ persistence : LoadBalancePersistenceConfig {
1993+ enabled : true ,
1994+ ttl_secs : 60 ,
1995+ table_max_entries : 16 ,
1996+ ..LoadBalancePersistenceConfig :: default ( )
1997+ } ,
1998+ ..LoadBalanceConfig :: default ( )
1999+ } ,
2000+ ..ProxyConfig :: default ( )
2001+ } ;
2002+
2003+ let balancer = UpstreamLoadBalancer :: from_proxy_config ( & config)
2004+ . unwrap ( )
2005+ . unwrap ( ) ;
2006+ let stats = balancer. runtime_stats ( ) ;
2007+ assert_eq ! ( stats. runtime_disabled_backend_count, 0 ) ;
2008+ assert_eq ! ( stats. persistence. entry_count, 0 ) ;
2009+ }
2010+
19172011 #[ test]
19182012 fn header_persistence_reuses_selected_backend ( ) {
19192013 install_test_crypto_provider ( ) ;
0 commit comments