1use super::config::{ReloadApplyMode, ReloadDetectMode, TlsReloadOptions};
19#[cfg(test)]
20use super::fingerprint::TargetTlsFingerprint;
21use super::fingerprint::{TargetTlsGeneration, build_target_tls_fingerprint};
22use super::metrics::{record_target_tls_publication_fail, record_target_tls_reload_result, record_target_tls_reload_skipped};
23#[cfg(test)]
24use super::state::TargetTlsInputSet;
25use super::state::{TargetTlsPublishedState, TargetTlsRuntimeState, TargetTlsStatusSnapshot};
26use super::r#trait::ReloadableTargetTls;
27use super::validate::validate_tls_material;
28use crate::error::TargetError;
29use std::collections::HashMap;
30use std::sync::Arc;
31use std::time::{Duration, SystemTime, UNIX_EPOCH};
32use tokio::sync::RwLock;
33use tokio::task::JoinHandle;
34use tracing::{debug, info, warn};
35
36const MIN_RELOAD_INTERVAL: Duration = Duration::from_secs(1);
39
40const REPLACE_JOIN_TIMEOUT: Duration = Duration::from_secs(5);
43
44struct TargetReloadEntry {
45 #[expect(dead_code)]
46 target_label: String,
47 cancel_tx: tokio::sync::mpsc::Sender<()>,
48 poll_handle: JoinHandle<()>,
49}
50
51pub struct TargetTlsReloadCoordinator {
57 entries: RwLock<HashMap<String, TargetReloadEntry>>,
58}
59
60impl Default for TargetTlsReloadCoordinator {
61 fn default() -> Self {
62 Self::new()
63 }
64}
65
66impl TargetTlsReloadCoordinator {
67 pub fn new() -> Self {
68 Self {
69 entries: RwLock::new(HashMap::new()),
70 }
71 }
72
73 pub async fn register<T: ReloadableTargetTls>(
78 &self,
79 target: Arc<T>,
80 options: TlsReloadOptions,
81 ) -> Result<Arc<TargetTlsRuntimeState<T::Material>>, TargetError> {
82 if !options.enabled {
83 return Err(TargetError::Configuration("TLS reload is disabled".to_string()));
84 }
85
86 let inputs = target.tls_input_set();
87 let target_label = inputs.target_label.clone();
88
89 let initial_fingerprint =
95 build_target_tls_fingerprint(&inputs.ca_path, &inputs.client_cert_path, &inputs.client_key_path).await?;
96 let initial_material = Arc::new(target.build_tls_material().await?);
97
98 let initial_state = Arc::new(TargetTlsPublishedState {
99 generation: TargetTlsGeneration(1),
100 fingerprint: initial_fingerprint,
101 material: initial_material,
102 loaded_at_unix_ms: unix_time_ms(),
103 });
104
105 let runtime_state = Arc::new(TargetTlsRuntimeState::new(initial_state, inputs));
106
107 let mut entries = self.entries.write().await;
111
112 if let Some(previous) = entries.remove(&target_label) {
116 let _ = previous.cancel_tx.send(()).await;
117 if tokio::time::timeout(REPLACE_JOIN_TIMEOUT, previous.poll_handle)
118 .await
119 .is_err()
120 {
121 warn!(target = %target_label, "Timed out joining previous TLS reload loop; detaching");
122 }
123 info!(target = %target_label, "Replaced existing TLS reload loop (stop-before-start)");
124 }
125
126 let detect_mode = options.detect_mode;
127 let (cancel_tx, cancel_rx) = tokio::sync::mpsc::channel(1);
128 let poll_handle = tokio::spawn(spawn_target_poll_loop(target, Arc::clone(&runtime_state), options, cancel_rx));
129 entries.insert(
130 target_label.clone(),
131 TargetReloadEntry {
132 target_label: target_label.clone(),
133 cancel_tx,
134 poll_handle,
135 },
136 );
137 info!(target = %target_label, detect_mode = ?detect_mode, "Registered target for TLS reload coordinator");
138
139 Ok(runtime_state)
140 }
141
142 pub async fn unregister(&self, target_label: &str) -> Result<(), TargetError> {
144 let mut entries = self.entries.write().await;
145 if let Some(entry) = entries.remove(target_label) {
146 let _ = entry.cancel_tx.send(()).await;
147 entry.poll_handle.abort();
148 info!(target = %target_label, "Unregistered target from TLS reload coordinator");
149 }
150 Ok(())
151 }
152
153 pub async fn force_reload<T: ReloadableTargetTls>(
156 &self,
157 target: &T,
158 runtime_state: &TargetTlsRuntimeState<T::Material>,
159 options: &TlsReloadOptions,
160 ) -> Result<TargetTlsGeneration, TargetError> {
161 reload_target_once(target, runtime_state, options).await
162 }
163
164 pub async fn shutdown(&self) {
166 let mut entries = self.entries.write().await;
167 for (label, entry) in entries.drain() {
168 let _ = entry.cancel_tx.send(()).await;
169 entry.poll_handle.abort();
170 debug!(target = %label, "Stopped TLS reload poll loop");
171 }
172 }
173
174 pub fn build_status_snapshot<M>(
178 runtime_state: &TargetTlsRuntimeState<M>,
179 options: &TlsReloadOptions,
180 ) -> TargetTlsStatusSnapshot {
181 let current = runtime_state.current.load();
182 let last_attempt = runtime_state.last_attempt_unix_ms();
183 let last_success = runtime_state.last_success_unix_ms();
184 let last_error = runtime_state.last_error.read().clone();
185
186 TargetTlsStatusSnapshot {
187 target_label: runtime_state.inputs.target_label.clone(),
188 generation: current.generation.0,
189 reload_enabled: options.enabled,
190 detect_mode: match options.detect_mode {
191 ReloadDetectMode::Poll => "poll",
192 ReloadDetectMode::Watch => "watch",
193 ReloadDetectMode::Hybrid => "hybrid",
194 },
195 apply_mode: match options.apply_hint {
196 ReloadApplyMode::Lazy => "lazy",
197 ReloadApplyMode::SoftReconnect => "soft_reconnect",
198 },
199 last_attempt_time: if last_attempt > 0 { Some(last_attempt) } else { None },
200 last_success_time: if last_success > 0 { Some(last_success) } else { None },
201 last_error,
202 ca_path: runtime_state.inputs.ca_path.clone(),
203 client_cert_path: runtime_state.inputs.client_cert_path.clone(),
204 client_key_path: runtime_state.inputs.client_key_path.clone(),
205 }
206 }
207}
208
209async fn spawn_target_poll_loop<T: ReloadableTargetTls>(
211 target: Arc<T>,
212 runtime_state: Arc<TargetTlsRuntimeState<T::Material>>,
213 options: TlsReloadOptions,
214 mut cancel_rx: tokio::sync::mpsc::Receiver<()>,
215) {
216 let interval_period = effective_reload_interval(options.interval);
219 let mut interval = tokio::time::interval(interval_period);
220 interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay);
221 interval.tick().await; let label = &runtime_state.inputs.target_label;
224 let debounce = options.debounce;
225 debug!(target = %label, interval_secs = interval_period.as_secs(), "TLS reload poll loop started");
226
227 loop {
228 tokio::select! {
229 biased;
230 _ = cancel_rx.recv() => {
231 info!(target = %label, "TLS reload poll loop stopped");
232 return;
233 }
234 _ = interval.tick() => {
235 let last_attempt = runtime_state.last_attempt_unix_ms();
239 if last_attempt > 0 {
240 let elapsed_since_last = unix_time_ms().saturating_sub(last_attempt);
241 if elapsed_since_last < debounce.as_millis() as u64 {
242 continue;
243 }
244 }
245
246 if let Err(err) = reload_target_once(target.as_ref(), runtime_state.as_ref(), &options).await {
247 warn!(target = %label, error = %err, "TLS reload poll check failed (will retry)");
248 }
249 }
250 }
251 }
252}
253
254async fn reload_target_once<T: ReloadableTargetTls>(
259 target: &T,
260 runtime_state: &TargetTlsRuntimeState<T::Material>,
261 options: &TlsReloadOptions,
262) -> Result<TargetTlsGeneration, TargetError> {
263 let _reload_guard = runtime_state.reload_lock.lock().await;
267
268 let now = unix_time_ms();
269 runtime_state.mark_attempt(now);
270 let started_at = std::time::Instant::now();
271 let label = &runtime_state.inputs.target_label;
272
273 let inputs = &runtime_state.inputs;
275 let next_fingerprint =
276 match build_target_tls_fingerprint(&inputs.ca_path, &inputs.client_cert_path, &inputs.client_key_path).await {
277 Ok(fingerprint) => fingerprint,
278 Err(err) => {
279 *runtime_state.last_error.write() = Some(err.to_string());
283 record_target_tls_publication_fail(label);
284 return Err(err);
285 }
286 };
287
288 let current = runtime_state.current.load();
290 if current.fingerprint == next_fingerprint {
291 record_target_tls_reload_skipped(label, "unchanged");
292 return Ok(current.generation);
293 }
294
295 if let Err(err) = validate_tls_material(&inputs.ca_path, &inputs.client_cert_path, &inputs.client_key_path) {
297 *runtime_state.last_error.write() = Some(err.to_string());
298 record_target_tls_publication_fail(label);
299 return Err(err);
300 }
301
302 if let Err(err) = target.validate_tls_files().await {
304 *runtime_state.last_error.write() = Some(err.to_string());
305 record_target_tls_publication_fail(label);
306 return Err(err);
307 }
308
309 let new_material = match target.build_tls_material().await {
311 Ok(m) => Arc::new(m),
312 Err(err) => {
313 *runtime_state.last_error.write() = Some(err.to_string());
314 record_target_tls_publication_fail(label);
315 return Err(err);
316 }
317 };
318
319 let new_generation = runtime_state.bump_generation();
321 if let Err(err) = target
322 .apply_tls_material(new_generation, Arc::clone(&new_material), options.apply_hint)
323 .await
324 {
325 *runtime_state.last_error.write() = Some(err.to_string());
326 record_target_tls_publication_fail(label);
327 return Err(err);
328 }
329
330 let published = Arc::new(TargetTlsPublishedState {
332 generation: new_generation,
333 fingerprint: next_fingerprint,
334 material: new_material,
335 loaded_at_unix_ms: now,
336 });
337 runtime_state.current.store(published.clone());
338 runtime_state.last_good.store(published);
339 runtime_state.mark_success(now);
340 *runtime_state.last_error.write() = None;
341
342 record_target_tls_reload_result(label, "ok", started_at.elapsed().as_secs_f64(), new_generation.0);
343
344 debug!(target = %label, generation = new_generation.0, "TLS reload successful");
345 Ok(new_generation)
346}
347
348fn unix_time_ms() -> u64 {
349 SystemTime::now().duration_since(UNIX_EPOCH).unwrap_or_default().as_millis() as u64
350}
351
352fn effective_reload_interval(interval: Duration) -> Duration {
355 if interval.is_zero() { MIN_RELOAD_INTERVAL } else { interval }
356}
357
358#[cfg(test)]
359mod tests {
360 use super::*;
361 use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
362
363 struct MockTarget {
364 inputs: TargetTlsInputSet,
365 build_calls: AtomicUsize,
366 apply_calls: AtomicUsize,
367 validate_calls: AtomicUsize,
368 should_fail_build: AtomicBool,
369 should_fail_apply: AtomicBool,
370 should_fail_validate: AtomicBool,
371 }
372
373 impl MockTarget {
374 fn new(label: &str) -> Self {
375 Self {
376 inputs: TargetTlsInputSet {
377 ca_path: String::new(),
378 client_cert_path: String::new(),
379 client_key_path: String::new(),
380 target_label: label.to_string(),
381 },
382 build_calls: AtomicUsize::new(0),
383 apply_calls: AtomicUsize::new(0),
384 validate_calls: AtomicUsize::new(0),
385 should_fail_build: AtomicBool::new(false),
386 should_fail_apply: AtomicBool::new(false),
387 should_fail_validate: AtomicBool::new(false),
388 }
389 }
390 }
391
392 #[async_trait::async_trait]
393 impl ReloadableTargetTls for MockTarget {
394 type Material = String;
395
396 fn tls_input_set(&self) -> TargetTlsInputSet {
397 self.inputs.clone()
398 }
399
400 async fn build_tls_material(&self) -> Result<Self::Material, TargetError> {
401 self.build_calls.fetch_add(1, Ordering::SeqCst);
402 if self.should_fail_build.load(Ordering::SeqCst) {
403 return Err(TargetError::Configuration("build failed".to_string()));
404 }
405 Ok("mock-material".to_string())
406 }
407
408 async fn apply_tls_material(
409 &self,
410 _generation: TargetTlsGeneration,
411 _material: Arc<Self::Material>,
412 _mode: ReloadApplyMode,
413 ) -> Result<(), TargetError> {
414 self.apply_calls.fetch_add(1, Ordering::SeqCst);
415 if self.should_fail_apply.load(Ordering::SeqCst) {
416 return Err(TargetError::Configuration("apply failed".to_string()));
417 }
418 Ok(())
419 }
420
421 async fn validate_tls_files(&self) -> Result<(), TargetError> {
422 self.validate_calls.fetch_add(1, Ordering::SeqCst);
423 if self.should_fail_validate.load(Ordering::SeqCst) {
424 return Err(TargetError::Configuration("validate failed".to_string()));
425 }
426 Ok(())
427 }
428 }
429
430 fn default_options() -> TlsReloadOptions {
431 TlsReloadOptions {
432 enabled: true,
433 detect_mode: ReloadDetectMode::Poll,
434 interval: std::time::Duration::from_secs(1),
435 debounce: std::time::Duration::from_secs(1),
436 min_stable_age: std::time::Duration::from_millis(100),
437 apply_hint: ReloadApplyMode::Lazy,
438 }
439 }
440
441 #[tokio::test]
442 async fn register_builds_initial_material() {
443 let coordinator = TargetTlsReloadCoordinator::new();
444 let target = Arc::new(MockTarget::new("test:webhook"));
445
446 let options = TlsReloadOptions {
447 detect_mode: ReloadDetectMode::Watch,
448 ..default_options()
449 };
450 let state = coordinator.register(target.clone(), options).await.unwrap();
451
452 assert_eq!(state.current.load().generation, TargetTlsGeneration(1));
453 assert_eq!(target.build_calls.load(Ordering::SeqCst), 1);
454 }
455
456 #[tokio::test]
457 async fn register_disabled_returns_error() {
458 let coordinator = TargetTlsReloadCoordinator::new();
459 let target = Arc::new(MockTarget::new("test:webhook"));
460
461 let options = TlsReloadOptions {
462 enabled: false,
463 ..default_options()
464 };
465 let result = coordinator.register(target, options).await;
466 assert!(result.is_err());
467 }
468
469 #[tokio::test]
470 async fn shutdown_stops_all_loops() {
471 let coordinator = TargetTlsReloadCoordinator::new();
472 let target = Arc::new(MockTarget::new("test:webhook"));
473
474 let _state = coordinator.register(target.clone(), default_options()).await.unwrap();
475 assert_eq!(coordinator.entries.read().await.len(), 1);
476
477 coordinator.shutdown().await;
478 assert!(coordinator.entries.read().await.is_empty());
479 }
480
481 #[tokio::test]
482 async fn force_reload_calls_build_and_apply() {
483 let target = MockTarget::new("test:webhook");
484 let initial_material = Arc::new("initial".to_string());
485 let initial_state = Arc::new(TargetTlsPublishedState {
486 generation: TargetTlsGeneration(1),
487 fingerprint: TargetTlsFingerprint::default(),
488 material: initial_material,
489 loaded_at_unix_ms: 0,
490 });
491 let inputs = target.tls_input_set();
492 let runtime_state = Arc::new(TargetTlsRuntimeState::new(initial_state, inputs));
493 let options = default_options();
494
495 let result = reload_target_once(&target, &runtime_state, &options).await.unwrap();
498 assert_eq!(result, TargetTlsGeneration(1));
500 assert_eq!(target.build_calls.load(Ordering::SeqCst), 0);
502 }
503
504 #[tokio::test]
505 async fn build_failure_preserves_old_generation() {
506 let target = MockTarget::new("test:webhook");
507 target.should_fail_build.store(true, Ordering::SeqCst);
508
509 let initial_material = Arc::new("initial".to_string());
512 let initial_fingerprint = TargetTlsFingerprint {
513 ca_sha256: Some([1; 32]),
514 client_cert_sha256: None,
515 client_key_sha256: None,
516 };
517 let initial_state = Arc::new(TargetTlsPublishedState {
518 generation: TargetTlsGeneration(1),
519 fingerprint: initial_fingerprint,
520 material: initial_material,
521 loaded_at_unix_ms: 0,
522 });
523 let runtime_state = Arc::new(TargetTlsRuntimeState::new(initial_state, target.tls_input_set()));
524 let options = default_options();
525
526 let result = reload_target_once(&target, &runtime_state, &options).await;
529 assert!(result.is_err());
530 assert_eq!(runtime_state.current_generation(), TargetTlsGeneration(1));
532 assert_eq!(target.build_calls.load(Ordering::SeqCst), 1);
533 }
534
535 #[tokio::test]
536 async fn status_snapshot_reflects_state() {
537 let target = MockTarget::new("test:webhook");
538 let initial_material = Arc::new("initial".to_string());
539 let initial_state = Arc::new(TargetTlsPublishedState {
540 generation: TargetTlsGeneration(1),
541 fingerprint: TargetTlsFingerprint::default(),
542 material: initial_material,
543 loaded_at_unix_ms: 0,
544 });
545 let runtime_state = Arc::new(TargetTlsRuntimeState::new(initial_state, target.tls_input_set()));
546 let options = default_options();
547
548 let snapshot = TargetTlsReloadCoordinator::build_status_snapshot(runtime_state.as_ref(), &options);
549 assert_eq!(snapshot.target_label, "test:webhook");
550 assert_eq!(snapshot.generation, 1);
551 assert!(snapshot.reload_enabled);
552 assert_eq!(snapshot.detect_mode, "poll");
553 assert_eq!(snapshot.apply_mode, "lazy");
554 assert!(snapshot.last_attempt_time.is_none());
555 assert!(snapshot.last_error.is_none());
556 }
557
558 #[tokio::test]
559 async fn apply_failure_preserves_old_generation() {
560 let target = MockTarget::new("test:webhook");
561 target.should_fail_apply.store(true, Ordering::SeqCst);
562
563 let initial_material = Arc::new("initial".to_string());
565 let initial_fingerprint = TargetTlsFingerprint {
566 ca_sha256: Some([42; 32]),
567 client_cert_sha256: None,
568 client_key_sha256: None,
569 };
570 let initial_state = Arc::new(TargetTlsPublishedState {
571 generation: TargetTlsGeneration(3),
572 fingerprint: initial_fingerprint,
573 material: initial_material,
574 loaded_at_unix_ms: 0,
575 });
576 let runtime_state = Arc::new(TargetTlsRuntimeState::new(initial_state, target.tls_input_set()));
577 let options = default_options();
578
579 let result = reload_target_once(&target, &runtime_state, &options).await;
580 assert!(result.is_err());
581 assert_eq!(runtime_state.current_generation(), TargetTlsGeneration(3));
583 assert!(runtime_state.last_error.read().is_some());
584 }
585
586 #[tokio::test]
587 async fn validate_failure_prevents_build() {
588 let target = MockTarget::new("test:kafka");
589 target.should_fail_validate.store(true, Ordering::SeqCst);
590
591 let initial_material = Arc::new("initial".to_string());
593 let initial_fingerprint = TargetTlsFingerprint {
594 ca_sha256: Some([99; 32]),
595 client_cert_sha256: None,
596 client_key_sha256: None,
597 };
598 let initial_state = Arc::new(TargetTlsPublishedState {
599 generation: TargetTlsGeneration(1),
600 fingerprint: initial_fingerprint,
601 material: initial_material,
602 loaded_at_unix_ms: 0,
603 });
604 let runtime_state = Arc::new(TargetTlsRuntimeState::new(initial_state, target.tls_input_set()));
605 let options = default_options();
606
607 let result = reload_target_once(&target, &runtime_state, &options).await;
608 assert!(result.is_err());
609 assert_eq!(target.build_calls.load(Ordering::SeqCst), 0);
611 assert!(target.validate_calls.load(Ordering::SeqCst) > 0);
613 assert_eq!(runtime_state.current_generation(), TargetTlsGeneration(1));
614 }
615
616 #[tokio::test]
617 async fn error_is_cleared_on_successful_reload() {
618 let target = MockTarget::new("test:nats");
619
620 let initial_material = Arc::new("initial".to_string());
621 let initial_fingerprint = TargetTlsFingerprint {
622 ca_sha256: Some([1; 32]),
623 client_cert_sha256: None,
624 client_key_sha256: None,
625 };
626 let initial_state = Arc::new(TargetTlsPublishedState {
627 generation: TargetTlsGeneration(1),
628 fingerprint: initial_fingerprint,
629 material: initial_material,
630 loaded_at_unix_ms: 0,
631 });
632 let runtime_state = Arc::new(TargetTlsRuntimeState::new(initial_state, target.tls_input_set()));
633 let options = default_options();
634
635 target.should_fail_build.store(true, Ordering::SeqCst);
637 let _ = reload_target_once(&target, &runtime_state, &options).await;
638 assert!(runtime_state.last_error.read().is_some());
639
640 target.should_fail_build.store(false, Ordering::SeqCst);
642 let result = reload_target_once(&target, &runtime_state, &options).await;
643 assert!(result.is_ok());
644 assert!(runtime_state.last_error.read().is_none());
645 assert!(runtime_state.last_success_unix_ms() > 0);
646 }
647
648 #[tokio::test]
649 async fn last_good_is_never_overwritten_by_failed_reload() {
650 let target = MockTarget::new("test:amqp");
651
652 let initial_material = Arc::new("good".to_string());
653 let initial_fingerprint = TargetTlsFingerprint {
654 ca_sha256: Some([5; 32]),
655 client_cert_sha256: None,
656 client_key_sha256: None,
657 };
658 let initial_state = Arc::new(TargetTlsPublishedState {
659 generation: TargetTlsGeneration(2),
660 fingerprint: initial_fingerprint.clone(),
661 material: initial_material.clone(),
662 loaded_at_unix_ms: 100,
663 });
664 let runtime_state = Arc::new(TargetTlsRuntimeState::new(initial_state, target.tls_input_set()));
665
666 let good = runtime_state.last_good.load();
668 assert_eq!(good.generation, TargetTlsGeneration(2));
669
670 target.should_fail_build.store(true, Ordering::SeqCst);
672 let _ = reload_target_once(&target, &runtime_state, &default_options()).await;
673
674 let good_after = runtime_state.last_good.load();
676 assert_eq!(good_after.generation, TargetTlsGeneration(2));
677 assert_eq!(good_after.fingerprint, initial_fingerprint);
678 }
679
680 #[tokio::test]
681 async fn unregister_stops_target_poll_loop() {
682 let coordinator = TargetTlsReloadCoordinator::new();
683 let target = Arc::new(MockTarget::new("test:pulsar"));
684
685 let _state = coordinator.register(target.clone(), default_options()).await.unwrap();
686 assert_eq!(coordinator.entries.read().await.len(), 1);
687
688 coordinator.unregister("test:pulsar").await.unwrap();
689 assert!(coordinator.entries.read().await.is_empty());
690 }
691
692 #[test]
693 fn effective_reload_interval_normalizes_zero() {
694 assert_eq!(effective_reload_interval(std::time::Duration::ZERO), MIN_RELOAD_INTERVAL);
695 let nonzero = std::time::Duration::from_secs(7);
696 assert_eq!(effective_reload_interval(nonzero), nonzero);
697 }
698
699 #[tokio::test]
700 async fn zero_interval_registration_does_not_panic() {
701 let coordinator = TargetTlsReloadCoordinator::new();
702 let target = Arc::new(MockTarget::new("test:zero-interval"));
703
704 let options = TlsReloadOptions {
705 interval: std::time::Duration::ZERO,
706 ..default_options()
707 };
708 let state = coordinator.register(target, options).await.unwrap();
711 assert_eq!(state.current.load().generation, TargetTlsGeneration(1));
712 assert_eq!(coordinator.entries.read().await.len(), 1);
713 }
714
715 #[tokio::test]
716 async fn watch_mode_starts_detection_loop() {
717 let coordinator = TargetTlsReloadCoordinator::new();
718 let target = Arc::new(MockTarget::new("test:watch"));
719
720 let options = TlsReloadOptions {
721 detect_mode: ReloadDetectMode::Watch,
722 ..default_options()
723 };
724 let _state = coordinator.register(target, options).await.unwrap();
726 assert_eq!(coordinator.entries.read().await.len(), 1);
727 }
728
729 #[tokio::test]
730 async fn duplicate_label_registration_replaces_previous_loop() {
731 let coordinator = TargetTlsReloadCoordinator::new();
732 let first = Arc::new(MockTarget::new("test:dup"));
733 let second = Arc::new(MockTarget::new("test:dup"));
734
735 let _s1 = coordinator.register(first, default_options()).await.unwrap();
736 assert_eq!(coordinator.entries.read().await.len(), 1);
737
738 let _s2 = coordinator.register(second, default_options()).await.unwrap();
741 assert_eq!(coordinator.entries.read().await.len(), 1);
742 }
743
744 #[tokio::test]
745 async fn first_step_fingerprint_failure_records_error_and_metric() {
746 let mut target = MockTarget::new("test:fp-fail");
747 target.inputs.ca_path = "/nonexistent/rustfs-tls-test/ca-does-not-exist.pem".to_string();
750
751 let initial_state = Arc::new(TargetTlsPublishedState {
752 generation: TargetTlsGeneration(1),
753 fingerprint: TargetTlsFingerprint::default(),
754 material: Arc::new("initial".to_string()),
755 loaded_at_unix_ms: 0,
756 });
757 let runtime_state = Arc::new(TargetTlsRuntimeState::new(initial_state, target.tls_input_set()));
758
759 let result = reload_target_once(&target, &runtime_state, &default_options()).await;
760 assert!(result.is_err());
761 assert!(runtime_state.last_error.read().is_some());
763 assert_eq!(target.build_calls.load(Ordering::SeqCst), 0);
765 }
766
767 #[tokio::test]
768 async fn bump_generation_saturates_at_max() {
769 let target = MockTarget::new("test:saturation");
770 let initial_material = Arc::new("initial".to_string());
771 let max_gen_state = Arc::new(TargetTlsPublishedState {
772 generation: TargetTlsGeneration(u64::MAX),
773 fingerprint: TargetTlsFingerprint::default(),
774 material: initial_material,
775 loaded_at_unix_ms: 0,
776 });
777 let runtime_state = Arc::new(TargetTlsRuntimeState::new(max_gen_state, target.tls_input_set()));
778
779 let bumped = runtime_state.bump_generation();
780 assert_eq!(bumped, TargetTlsGeneration(u64::MAX)); }
782}