@@ -124,7 +124,7 @@ impl DiscoveryManager {
124124 pub fn add_server_request ( & self , added_by : IpAddr , features : ServerFeatures ) -> Result < ( ) > {
125125 self . verify_compatibility ( & features) ?;
126126
127- let mut queue = self . queue . write ( ) . unwrap ( ) ;
127+ let mut queue = self . queue . write ( ) . unwrap_or_else ( |e| e . into_inner ( ) ) ;
128128 ensure ! ( queue. len( ) < MAX_QUEUE_SIZE , "queue size exceeded" ) ;
129129
130130 // TODO optimize
@@ -205,7 +205,7 @@ impl DiscoveryManager {
205205 /// before being removed due to unavailability.
206206 pub fn add_default_server ( & self , hostname : Hostname , services : Vec < Service > ) -> Result < ( ) > {
207207 let addr = ServerAddr :: resolve ( & hostname) ?;
208- let mut queue = self . queue . write ( ) . unwrap ( ) ;
208+ let mut queue = self . queue . write ( ) . unwrap_or_else ( |e| e . into_inner ( ) ) ;
209209 queue. extend (
210210 services
211211 . into_iter ( )
@@ -219,7 +219,7 @@ impl DiscoveryManager {
219219 // XXX return a random sample instead of everything?
220220 self . healthy
221221 . read ( )
222- . unwrap ( )
222+ . unwrap_or_else ( |e| e . into_inner ( ) )
223223 . iter ( )
224224 . map ( |( addr, server) | {
225225 ServerEntry ( addr. clone ( ) , server. hostname . clone ( ) , server. feature_strs ( ) )
@@ -234,14 +234,19 @@ impl DiscoveryManager {
234234 /// Run the next health check in the queue (a single one)
235235 fn run_health_check ( & self ) -> Result < ( ) > {
236236 // abort if there are no entries in the queue, or its still too early for the next one up
237- if self . queue . read ( ) . unwrap ( ) . peek ( ) . map_or ( true , |next| {
237+ if self . queue . read ( ) . unwrap_or_else ( |e| e . into_inner ( ) ) . peek ( ) . map_or ( true , |next| {
238238 next. last_check
239239 . map_or ( false , |t| t. elapsed ( ) < HEALTH_CHECK_FREQ )
240240 } ) {
241241 return Ok ( ( ) ) ;
242242 }
243243
244- let mut job = self . queue . write ( ) . unwrap ( ) . pop ( ) . unwrap ( ) ;
244+ // Only spawn_jobs_thread calls pop(), so this is the sole consumer; the
245+ // let-else guards against the (currently unreachable) race where the queue
246+ // drains between the peek above and this pop.
247+ let Some ( mut job) = self . queue . write ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) . pop ( ) else {
248+ return Ok ( ( ) ) ;
249+ } ;
245250 debug ! ( "processing {:?}" , job) ;
246251
247252 let was_healthy = job. is_healthy ( ) ;
@@ -259,7 +264,7 @@ impl DiscoveryManager {
259264 job. last_healthy = job. last_check ;
260265 job. consecutive_failures = 0 ;
261266 // schedule the next health check
262- self . queue . write ( ) . unwrap ( ) . push ( job) ;
267+ self . queue . write ( ) . unwrap_or_else ( |e| e . into_inner ( ) ) . push ( job) ;
263268
264269 Ok ( ( ) )
265270 }
@@ -275,7 +280,7 @@ impl DiscoveryManager {
275280 job. consecutive_failures += 1 ;
276281
277282 if job. should_retry ( ) {
278- self . queue . write ( ) . unwrap ( ) . push ( job) ;
283+ self . queue . write ( ) . unwrap_or_else ( |e| e . into_inner ( ) ) . push ( job) ;
279284 } else {
280285 debug ! ( "giving up on {:?}" , job) ;
281286 }
@@ -288,7 +293,11 @@ impl DiscoveryManager {
288293 /// Upsert the server/service into the healthy set
289294 fn save_healthy_service ( & self , job : & HealthCheck , features : ServerFeatures ) {
290295 let addr = job. addr . clone ( ) ;
291- let mut healthy = self . healthy . write ( ) . unwrap ( ) ;
296+ debug ! (
297+ "saving healthy service hostname='{}' addr='{}' service={:?}" ,
298+ job. hostname, addr, job. service
299+ ) ;
300+ let mut healthy = self . healthy . write ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
292301 healthy
293302 . entry ( addr)
294303 . or_insert_with ( || Server :: new ( job. hostname . clone ( ) , features) )
@@ -299,16 +308,29 @@ impl DiscoveryManager {
299308 /// Remove the service, and remove the server entirely if it has no other reamining healthy services
300309 fn remove_unhealthy_service ( & self , job : & HealthCheck ) {
301310 let addr = job. addr . clone ( ) ;
302- let mut healthy = self . healthy . write ( ) . unwrap ( ) ;
311+ debug ! (
312+ "removing unhealthy service hostname='{}' addr='{}' service={:?}" ,
313+ job. hostname, addr, job. service
314+ ) ;
315+ let mut healthy = self . healthy . write ( ) . unwrap_or_else ( |e| e. into_inner ( ) ) ;
303316 if let Entry :: Occupied ( mut entry) = healthy. entry ( addr) {
304317 let server = entry. get_mut ( ) ;
305- assert ! ( server. services. remove( & job. service) ) ;
318+ if !server. services . remove ( & job. service ) {
319+ warn ! (
320+ "service={:?} hostname='{}' missing from healthy set, corrupted state" ,
321+ job. service,
322+ job. hostname
323+ ) ;
324+ }
306325 if server. services . is_empty ( ) {
307326 entry. remove_entry ( ) ;
308327 }
309328 } else {
310- // FIXME This was an unreachable but it was reached.
311- log:: warn!( "missing expected server, corrupted state" ) ;
329+ warn ! (
330+ "hostname='{}' addr='{}' missing from healthy map, corrupted state" ,
331+ job. hostname,
332+ job. addr
333+ ) ;
312334 }
313335 }
314336
@@ -376,9 +398,28 @@ impl DiscoveryManager {
376398 }
377399
378400 pub fn spawn_jobs_thread ( manager : Arc < DiscoveryManager > ) {
401+ // Two-layer panic hardening:
402+ // 1. catch_unwind prevents a panic in any single iteration from killing this thread.
403+ // 2. All RwLock accesses use unwrap_or_else(|e| e.into_inner()) so that if a panic
404+ // fires while a lock is held (poisoning it), the next iteration recovers the inner
405+ // value and continues rather than dying on the poisoned lock. The recovered state
406+ // may be partially-modified, but for best-effort peer discovery that is acceptable.
407+ // These two defenses are co-dependent: catch_unwind prevents thread death;
408+ // unwrap_or_else prevents the *next* iteration from dying due to the poisoned lock
409+ // left behind by the previous panic.
379410 spawn_thread ( "discovery-jobs" , move || loop {
380- if let Err ( e) = manager. run_health_check ( ) {
381- debug ! ( "health check failed: {:?}" , e) ;
411+ let result = std:: panic:: catch_unwind ( std:: panic:: AssertUnwindSafe ( || {
412+ if let Err ( e) = manager. run_health_check ( ) {
413+ debug ! ( "health check failed: {:?}" , e) ;
414+ }
415+ } ) ) ;
416+ if let Err ( panic) = result {
417+ let msg = panic
418+ . downcast_ref :: < & str > ( )
419+ . copied ( )
420+ . or_else ( || panic. downcast_ref :: < String > ( ) . map ( String :: as_str) )
421+ . unwrap_or ( "unknown payload" ) ;
422+ error ! ( "discovery-jobs panicked reason='{}', continuing after delay" , msg) ;
382423 }
383424 // XXX use a dynamic JOB_INTERVAL, adjusted according to the queue size and HEALTH_CHECK_FREQ?
384425 thread:: sleep ( JOB_INTERVAL ) ;
0 commit comments