1use std::collections::HashMap;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::Arc;
11use std::time::Duration;
12
13use serde::Serialize;
14use tokio::sync::{Mutex, Notify};
15use tracing::{error, info, warn};
16
17use super::challenge::TlsAlpnSolver;
18use super::client::{AcmeClient, AcmeClientFactory};
19use super::metrics::AcmeMetrics;
20use super::order::{issue, KeyType};
21use super::storage::CertStorage;
22use super::{
23 load_certified_key, now_unix, parse_cert_meta, placeholder_cert, publish, update, AcmeError,
24 CertId, CertMeta, CertState, ManagedCert, ManagedCerts,
25};
26
27pub const LEASE_TTL: Duration = Duration::from_secs(300);
28const ARI_INTERVAL_SECS: i64 = 3_600;
30const PLACEHOLDER_REMINT_MARGIN_SECS: i64 = 300;
35
36pub fn when_to_renew(not_after: i64, ari: Option<(i64, i64)>, renew_before_secs: i64) -> i64 {
39 let by_window = not_after - renew_before_secs;
40 match ari {
41 Some((start, _)) => by_window.min(start),
42 None => by_window,
43 }
44}
45
46pub fn backoff_secs(failures: u32) -> u64 {
48 if failures == 0 {
49 return 0;
50 }
51 60u64
52 .saturating_mul(1u64 << (failures - 1).min(10))
53 .min(3_600)
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
57#[serde(rename_all = "snake_case")]
58pub enum RenewOutcome {
59 Scheduled,
60 NotDue,
61 InProgress,
62 Unknown,
63}
64
65#[derive(Debug, Clone)]
66pub struct ManagedSlot {
67 pub id: CertId,
68 pub domains: Vec<String>,
69}
70
71#[derive(Debug, Clone)]
72pub struct ManagerConfig {
73 pub key_type: KeyType,
74 pub renew_before: Duration,
75 pub poll_interval: Duration,
77 pub lease_ttl: Duration,
78 pub peer_poll: Duration,
80 pub challenge_refresh: Duration,
82}
83
84impl Default for ManagerConfig {
85 fn default() -> Self {
86 Self {
87 key_type: KeyType::EcdsaP256,
88 renew_before: Duration::from_secs(30 * 86_400),
89 poll_interval: Duration::from_secs(60),
90 lease_ttl: LEASE_TTL,
91 peer_poll: Duration::from_secs(60),
92 challenge_refresh: Duration::from_secs(2),
93 }
94 }
95}
96
97struct SlotControl {
98 waker: Notify,
99 force: AtomicBool,
100 in_flight: AtomicBool,
101}
102
103struct AbortOnDrop(tokio::task::JoinHandle<()>);
109
110impl Drop for AbortOnDrop {
111 fn drop(&mut self) {
112 self.0.abort();
113 }
114}
115
116pub struct Manager {
117 cfg: ManagerConfig,
118 factory: Arc<dyn AcmeClientFactory>,
119 client: Mutex<Option<Arc<dyn AcmeClient>>>,
120 storage: Arc<dyn CertStorage>,
121 solver: Arc<TlsAlpnSolver>,
122 certs: ManagedCerts,
123 slots: Vec<ManagedSlot>,
124 controls: HashMap<String, SlotControl>,
125 metrics: Option<Arc<AcmeMetrics>>,
126 owner: String,
127}
128
129impl Manager {
130 pub fn new(
131 cfg: ManagerConfig,
132 factory: Arc<dyn AcmeClientFactory>,
133 storage: Arc<dyn CertStorage>,
134 solver: Arc<TlsAlpnSolver>,
135 certs: ManagedCerts,
136 slots: Vec<ManagedSlot>,
137 metrics: Option<Arc<AcmeMetrics>>,
138 ) -> Arc<Self> {
139 let controls = slots
140 .iter()
141 .map(|s| {
142 (
143 s.id.as_str().to_string(),
144 SlotControl {
145 waker: Notify::new(),
146 force: AtomicBool::new(false),
147 in_flight: AtomicBool::new(false),
148 },
149 )
150 })
151 .collect();
152 let owner = format!(
153 "{}:{}:{}",
154 std::env::var("HOSTNAME").unwrap_or_else(|_| "gateway".into()),
155 std::process::id(),
156 uuid::Uuid::new_v4().simple()
157 );
158 Arc::new(Self {
159 cfg,
160 factory,
161 client: Mutex::new(None),
162 storage,
163 solver,
164 certs,
165 slots,
166 controls,
167 metrics,
168 owner,
169 })
170 }
171
172 pub fn slots(&self) -> &[ManagedSlot] {
173 &self.slots
174 }
175
176 pub fn in_flight(&self, id: &str) -> bool {
177 self.controls
178 .get(id)
179 .is_some_and(|c| c.in_flight.load(Ordering::SeqCst))
180 }
181
182 pub fn renew_now(&self, id: &str, force: bool) -> RenewOutcome {
186 let Some(ctl) = self.controls.get(id) else {
187 return RenewOutcome::Unknown;
188 };
189 if ctl.in_flight.load(Ordering::SeqCst) {
190 return RenewOutcome::InProgress;
191 }
192 let due = self
193 .certs
194 .load()
195 .get(id)
196 .map(|c| {
197 c.state != CertState::Issued
198 || c.meta.next_renewal_at.is_some_and(|t| t <= now_unix())
199 })
200 .unwrap_or(true);
201 if !due && !force {
202 return RenewOutcome::NotDue;
203 }
204 ctl.force.store(true, Ordering::SeqCst);
205 ctl.waker.notify_one();
206 RenewOutcome::Scheduled
207 }
208
209 pub async fn run(self: Arc<Self>) {
211 for slot in self.slots.clone() {
212 let me = self.clone();
213 tokio::spawn(async move { me.run_slot(slot).await });
214 }
215 }
216
217 async fn client(&self) -> Result<Arc<dyn AcmeClient>, AcmeError> {
218 let mut guard = self.client.lock().await;
219 if let Some(c) = guard.as_ref() {
220 return Ok(c.clone());
221 }
222 let c = self.factory.connect().await?;
223 *guard = Some(c.clone());
224 Ok(c)
225 }
226
227 fn snapshot(&self, id: &CertId) -> Option<ManagedCert> {
228 self.certs.load().get(id.as_str()).cloned()
229 }
230
231 fn observe(&self, id: &CertId) {
232 if let (Some(m), Some(c)) = (&self.metrics, self.snapshot(id)) {
233 let not_after = if c.state == CertState::Placeholder {
234 0
235 } else {
236 c.meta.not_after
237 };
238 m.observe(id.as_str(), c.state, not_after);
239 }
240 }
241
242 fn remint_placeholder_if_stale(&self, slot: &ManagedSlot, current: ManagedCert) -> ManagedCert {
250 if current.state != CertState::Placeholder
251 || current.meta.not_after - now_unix() >= PLACEHOLDER_REMINT_MARGIN_SECS
252 {
253 return current;
254 }
255 let (key, leaf) = match placeholder_cert(&slot.domains) {
256 Ok(v) => v,
257 Err(e) => {
258 warn!(
259 "acme: could not re-mint the placeholder for {}: {}",
260 slot.id, e
261 );
262 return current;
263 }
264 };
265 let meta = match parse_cert_meta(&leaf) {
266 Ok(m) => m,
267 Err(e) => {
268 warn!(
269 "acme: could not read the re-minted placeholder for {}: {}",
270 slot.id, e
271 );
272 return current;
273 }
274 };
275 let fresh = ManagedCert {
276 key,
277 leaf_der: leaf,
278 state: CertState::Placeholder,
279 meta: CertMeta {
280 next_renewal_at: current.meta.next_renewal_at,
281 last_attempt_at: current.meta.last_attempt_at,
282 last_error: current.meta.last_error.clone(),
283 ..meta
284 },
285 domains: slot.domains.clone(),
286 };
287 info!(
288 "acme: re-minted the self-signed placeholder for {} (still awaiting a real certificate)",
289 slot.id
290 );
291 publish(&self.certs, &slot.id, fresh);
292 self.snapshot(&slot.id).unwrap_or(current)
293 }
294
295 async fn run_slot(self: Arc<Self>, slot: ManagedSlot) {
296 let ctl = &self.controls[slot.id.as_str()];
297 let mut failures: u32 = 0;
298 let mut ari: Option<(i64, i64)> = None;
299 let mut ari_checked_at: i64 = 0;
300 self.observe(&slot.id);
301 loop {
302 let Some(current) = self.snapshot(&slot.id) else {
303 return;
304 };
305 let current = self.remint_placeholder_if_stale(&slot, current);
306 let forced = ctl.force.swap(false, Ordering::SeqCst);
307 let now = now_unix();
308
309 let due_at = if forced {
313 now
314 } else if failures > 0 {
315 current.meta.last_attempt_at.unwrap_or(now) + backoff_secs(failures) as i64
316 } else if current.state == CertState::Placeholder {
317 now
318 } else {
319 if now - ari_checked_at >= ARI_INTERVAL_SECS {
320 if let Ok(client) = self.client().await {
321 ari = client
322 .renewal_window(¤t.leaf_der)
323 .await
324 .unwrap_or(None);
325 }
326 ari_checked_at = now;
327 }
328 let t = when_to_renew(
329 current.meta.not_after,
330 ari,
331 self.cfg.renew_before.as_secs() as i64,
332 );
333 update(&self.certs, &slot.id, |c| c.meta.next_renewal_at = Some(t));
334 t
335 };
336
337 if now < due_at {
338 let wait = Duration::from_secs((due_at - now) as u64).min(self.cfg.poll_interval);
339 tokio::select! {
340 _ = tokio::time::sleep(wait) => {}
341 _ = ctl.waker.notified() => {}
342 }
343 continue;
344 }
345
346 ctl.in_flight.store(true, Ordering::SeqCst);
348 let outcome = self.attempt(&slot).await;
349 ctl.in_flight.store(false, Ordering::SeqCst);
350 match outcome {
351 Ok(true) => {
352 failures = 0;
353 ari_checked_at = 0; }
355 Ok(false) => {
356 if self.follow_peer(&slot, ctl).await {
358 failures = 0;
366 ari = None;
367 ari_checked_at = 0;
368 }
369 }
370 Err(e) => {
371 failures += 1;
372 error!(
373 "acme: issuance for {} failed (attempt {}): {}",
374 slot.id, failures, e
375 );
376 let now = now_unix();
377 update(&self.certs, &slot.id, |c| {
384 if c.state != CertState::Placeholder {
385 c.state = CertState::Failed;
386 }
387 c.meta.last_attempt_at = Some(now);
388 c.meta.last_error = Some(e.to_string());
389 c.meta.next_renewal_at = Some(now + backoff_secs(failures) as i64);
390 });
391 if let Some(m) = &self.metrics {
392 m.attempt(slot.id.as_str(), false, now);
393 }
394 if self
395 .snapshot(&slot.id)
396 .is_some_and(|c| c.state == CertState::Placeholder)
397 {
398 warn!(
399 "acme: {} is still serving a self-signed placeholder",
400 slot.id
401 );
402 }
403 }
404 }
405 self.observe(&slot.id);
406 }
407 }
408
409 async fn attempt(&self, slot: &ManagedSlot) -> Result<bool, AcmeError> {
411 if !self
412 .storage
413 .try_acquire_lease(&slot.id, &self.owner, self.cfg.lease_ttl)
414 .await?
415 {
416 return Ok(false);
417 }
418 update(&self.certs, &slot.id, |c| {
419 if c.state == CertState::Issued || c.state == CertState::Failed {
420 c.state = CertState::Renewing;
421 }
422 });
423 self.observe(&slot.id);
424 let result = async {
425 let client = self.client().await?;
426 let keepalive = AbortOnDrop({
432 let storage = self.storage.clone();
433 let id = slot.id.clone();
434 let owner = self.owner.clone();
435 let ttl = self.cfg.lease_ttl;
436 tokio::spawn(async move {
437 loop {
438 tokio::time::sleep(ttl / 4).await;
439 let _ = storage.renew_lease(&id, &owner, ttl).await;
440 }
441 })
442 });
443 let stored = issue(
444 client.as_ref(),
445 &self.solver,
446 &slot.domains,
447 self.cfg.key_type,
448 )
449 .await?;
450 self.storage.save_cert(&slot.id, &stored).await?;
451 drop(keepalive);
452 let (key, leaf) = load_certified_key(&stored.chain_pem, &stored.key_pem)?;
453 let meta = parse_cert_meta(&leaf)?;
454 let now = now_unix();
455 info!(
456 "acme: issued certificate for {} (serial {}, expires {})",
457 slot.id, meta.serial, meta.not_after
458 );
459 publish(
460 &self.certs,
461 &slot.id,
462 ManagedCert {
463 key,
464 leaf_der: leaf,
465 state: CertState::Issued,
466 meta: super::CertMeta {
467 last_attempt_at: Some(now),
468 ..meta
469 },
470 domains: slot.domains.clone(),
471 },
472 );
473 if let Some(m) = &self.metrics {
474 m.attempt(slot.id.as_str(), true, now);
475 }
476 Ok::<(), AcmeError>(())
477 }
478 .await;
479 if let Err(e) = self.storage.release_lease(&slot.id, &self.owner).await {
480 warn!("acme: releasing lease for {} failed: {}", slot.id, e);
481 }
482 if result.is_err() {
484 update(&self.certs, &slot.id, |c| {
485 if c.state == CertState::Renewing {
486 c.state = CertState::Failed;
487 }
488 });
489 }
490 result.map(|_| true)
491 }
492
493 async fn follow_peer(&self, slot: &ManagedSlot, ctl: &SlotControl) -> bool {
499 let deadline = tokio::time::Instant::now() + self.cfg.lease_ttl;
500 let mut next_adopt = tokio::time::Instant::now();
501 while tokio::time::Instant::now() < deadline {
502 if let Err(e) = self.solver.refresh_from_storage(&slot.domains).await {
503 warn!("acme: challenge refresh for {} failed: {}", slot.id, e);
504 }
505 if tokio::time::Instant::now() >= next_adopt {
506 next_adopt = tokio::time::Instant::now() + self.cfg.peer_poll;
507 match self.adopt_from_storage(slot).await {
508 Ok(true) => return true,
509 Ok(false) => {}
510 Err(e) => warn!(
511 "acme: reading peer certificate for {} failed: {}",
512 slot.id, e
513 ),
514 }
515 }
516 if ctl.force.load(Ordering::SeqCst) {
517 return false;
518 }
519 tokio::time::sleep(self.cfg.challenge_refresh).await;
520 }
521 false
522 }
523
524 async fn adopt_from_storage(&self, slot: &ManagedSlot) -> Result<bool, AcmeError> {
526 let Some(stored) = self.storage.load_cert(&slot.id).await? else {
527 return Ok(false);
528 };
529 let (key, leaf) = load_certified_key(&stored.chain_pem, &stored.key_pem)?;
530 let meta = parse_cert_meta(&leaf)?;
531 let current = self.snapshot(&slot.id);
532 let newer = current
533 .as_ref()
534 .map(|c| c.state == CertState::Placeholder || meta.not_after > c.meta.not_after)
535 .unwrap_or(true);
536 if !newer || meta.not_after <= now_unix() {
537 return Ok(false);
538 }
539 publish(
540 &self.certs,
541 &slot.id,
542 ManagedCert {
543 key,
544 leaf_der: leaf,
545 state: CertState::Issued,
546 meta,
547 domains: slot.domains.clone(),
548 },
549 );
550 info!("acme: adopted certificate for {} issued by a peer", slot.id);
551 self.observe(&slot.id);
552 Ok(true)
553 }
554}
555
556#[cfg(test)]
557mod tests {
558 use super::*;
559 use crate::acme::client::mock::{MockAcmeClient, MockBehavior, MockFactory, MockStep};
560 use crate::acme::storage::fs::FsCertStorage;
561 use crate::acme::{new_managed_certs, placeholder_cert, publish, CertState, ManagedCert};
562
563 #[test]
564 fn when_to_renew_takes_the_earlier_of_renew_before_and_ari() {
565 let not_after = 1_000_000;
566 assert_eq!(when_to_renew(not_after, None, 100), 999_900);
567 assert_eq!(
568 when_to_renew(not_after, Some((999_000, 999_500)), 100),
569 999_000
570 );
571 assert_eq!(
572 when_to_renew(not_after, Some((999_950, 999_990)), 100),
573 999_900
574 );
575 }
576
577 #[test]
578 fn backoff_doubles_from_a_minute_and_caps_at_an_hour() {
579 assert_eq!(backoff_secs(0), 0);
580 assert_eq!(backoff_secs(1), 60);
581 assert_eq!(backoff_secs(2), 120);
582 assert_eq!(backoff_secs(6), 1_920);
583 assert_eq!(backoff_secs(7), 3_600);
584 assert_eq!(backoff_secs(40), 3_600);
585 }
586
587 struct Harness {
588 client: Arc<MockAcmeClient>,
589 factory: Arc<MockFactory>,
590 storage: Arc<dyn CertStorage>,
591 certs: ManagedCerts,
592 slot: ManagedSlot,
593 }
594
595 fn harness(tag: &str, behavior: MockBehavior) -> Harness {
596 let d = std::env::temp_dir().join(format!("fb_acme_mgr_{}_{}", tag, std::process::id()));
597 let _ = std::fs::remove_dir_all(&d);
598 let storage: Arc<dyn CertStorage> = Arc::new(FsCertStorage::new(d));
599 let client = MockAcmeClient::new(behavior);
600 let factory = MockFactory::new(client.clone());
601 let (id, domains) = CertId::from_domains(&["m.example.com".into()]).unwrap();
602 let certs = new_managed_certs();
603 let (key, leaf) = placeholder_cert(&domains).unwrap();
604 let meta = crate::acme::parse_cert_meta(&leaf).unwrap();
608 publish(
609 &certs,
610 &id,
611 ManagedCert {
612 key,
613 leaf_der: leaf,
614 state: CertState::Placeholder,
615 meta,
616 domains: domains.clone(),
617 },
618 );
619 Harness {
620 client,
621 factory,
622 storage,
623 certs,
624 slot: ManagedSlot { id, domains },
625 }
626 }
627
628 fn fast_cfg() -> ManagerConfig {
629 ManagerConfig {
630 poll_interval: Duration::from_millis(50),
631 peer_poll: Duration::from_millis(50),
632 challenge_refresh: Duration::from_millis(20),
633 ..Default::default()
634 }
635 }
636
637 fn manager(h: &Harness, cfg: ManagerConfig) -> Arc<Manager> {
638 Manager::new(
639 cfg,
640 h.factory.clone(),
641 h.storage.clone(),
642 TlsAlpnSolver::new(h.storage.clone()),
643 h.certs.clone(),
644 vec![h.slot.clone()],
645 None,
646 )
647 }
648
649 async fn wait_until(
650 certs: &ManagedCerts,
651 id: &CertId,
652 what: &str,
653 pred: impl Fn(&ManagedCert) -> bool,
654 ) -> ManagedCert {
655 let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
656 loop {
657 if let Some(c) = certs.load().get(id.as_str()) {
658 if pred(c) {
659 return c.clone();
660 }
661 }
662 assert!(
663 tokio::time::Instant::now() < deadline,
664 "timed out waiting for {what}"
665 );
666 tokio::time::sleep(Duration::from_millis(20)).await;
667 }
668 }
669
670 async fn wait_state(certs: &ManagedCerts, id: &CertId, want: CertState) -> ManagedCert {
671 wait_until(certs, id, &format!("{want:?}"), |c| c.state == want).await
672 }
673
674 async fn wait_failed_placeholder(certs: &ManagedCerts, id: &CertId) -> ManagedCert {
677 wait_until(certs, id, "placeholder with a recorded error", |c| {
678 c.state == CertState::Placeholder && c.meta.last_error.is_some()
679 })
680 .await
681 }
682
683 async fn force_renew_scheduled(m: &Manager, id: &str) {
689 let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
690 loop {
691 match m.renew_now(id, true) {
692 RenewOutcome::Scheduled => return,
693 RenewOutcome::InProgress => {}
694 other => panic!(
695 "renew_now({id}, true) returned {other:?}, expected Scheduled or InProgress"
696 ),
697 }
698 assert!(
699 tokio::time::Instant::now() < deadline,
700 "timed out getting renew_now to schedule for {id}"
701 );
702 tokio::time::sleep(Duration::from_millis(10)).await;
703 }
704 }
705
706 async fn wait_fail_connects(factory: &MockFactory, want: usize) {
715 let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
716 while factory
717 .fail_connects
718 .load(std::sync::atomic::Ordering::SeqCst)
719 != want
720 {
721 assert!(
722 tokio::time::Instant::now() < deadline,
723 "timed out waiting for fail_connects to reach {want}"
724 );
725 tokio::time::sleep(Duration::from_millis(10)).await;
726 }
727 }
728
729 #[tokio::test]
730 async fn placeholder_is_issued_on_start_and_persisted() {
731 let h = harness("issue", MockBehavior::default());
732 let m = manager(&h, fast_cfg());
733 m.clone().run().await;
734 let issued = wait_state(&h.certs, &h.slot.id, CertState::Issued).await;
735 assert!(issued.meta.issuer.contains("Mock ACME CA"));
736 assert!(issued.meta.next_renewal_at.is_some());
737 assert!(h.storage.load_cert(&h.slot.id).await.unwrap().is_some());
738 assert_eq!(h.client.orders(), 1);
739 assert_eq!(m.renew_now(h.slot.id.as_str(), false), RenewOutcome::NotDue);
741 assert_eq!(m.renew_now("nope", true), RenewOutcome::Unknown);
742 assert_eq!(
743 m.renew_now(h.slot.id.as_str(), true),
744 RenewOutcome::Scheduled
745 );
746 let deadline = tokio::time::Instant::now() + Duration::from_secs(10);
747 while h.client.orders() < 2 {
748 assert!(tokio::time::Instant::now() < deadline);
749 tokio::time::sleep(Duration::from_millis(20)).await;
750 }
751 let renewed = wait_state(&h.certs, &h.slot.id, CertState::Issued).await;
752 assert_ne!(renewed.meta.serial, issued.meta.serial);
753 }
754
755 #[tokio::test]
756 async fn failure_keeps_placeholder_records_error_and_backs_off() {
757 let h = harness(
758 "fail",
759 MockBehavior {
760 fail_step: Some(MockStep::Finalize),
761 ..Default::default()
762 },
763 );
764 let before = h
765 .certs
766 .load()
767 .get(h.slot.id.as_str())
768 .unwrap()
769 .leaf_der
770 .clone();
771 let m = manager(&h, fast_cfg());
772 m.clone().run().await;
773 let failed = wait_failed_placeholder(&h.certs, &h.slot.id).await;
774 assert_eq!(
775 failed.state,
776 CertState::Placeholder,
777 "readiness stays keyed on Placeholder, not Failed"
778 );
779 assert!(failed
780 .meta
781 .last_error
782 .as_deref()
783 .unwrap_or("")
784 .contains("badCSR"));
785 assert!(failed.meta.last_attempt_at.is_some());
786 assert_eq!(failed.leaf_der, before, "placeholder keeps serving");
787 let n = h.client.orders();
788 tokio::time::sleep(Duration::from_millis(300)).await;
789 assert_eq!(
790 h.client.orders(),
791 n,
792 "backoff (≥60 s) prevents a hot retry loop"
793 );
794 h.client.set_behavior(MockBehavior::default());
796 assert_eq!(
797 m.renew_now(h.slot.id.as_str(), false),
798 RenewOutcome::Scheduled,
799 "a placeholder is always due"
800 );
801 wait_state(&h.certs, &h.slot.id, CertState::Issued).await;
802 }
803
804 #[tokio::test]
805 async fn unreachable_directory_at_start_is_retried() {
806 let h = harness("connect", MockBehavior::default());
807 h.factory
808 .fail_connects
809 .store(2, std::sync::atomic::Ordering::SeqCst);
810 let m = manager(&h, fast_cfg());
811 m.clone().run().await;
812 wait_failed_placeholder(&h.certs, &h.slot.id).await;
817 force_renew_scheduled(&m, h.slot.id.as_str()).await;
818 wait_fail_connects(&h.factory, 0).await;
823 force_renew_scheduled(&m, h.slot.id.as_str()).await;
824 wait_state(&h.certs, &h.slot.id, CertState::Issued).await;
825 }
826
827 #[tokio::test]
828 async fn peer_holding_the_lease_makes_us_adopt_its_certificate() {
829 let h = harness("peer", MockBehavior::default());
830 assert!(h
832 .storage
833 .try_acquire_lease(&h.slot.id, "peer", Duration::from_secs(30))
834 .await
835 .unwrap());
836 let m = manager(&h, fast_cfg());
837 m.clone().run().await;
838 tokio::time::sleep(Duration::from_millis(200)).await;
839 assert_eq!(
840 h.client.orders(),
841 0,
842 "must not order while a peer holds the lease"
843 );
844 let solver = TlsAlpnSolver::new(h.storage.clone());
845 let stored =
846 crate::acme::order::issue(&h.client, &solver, &h.slot.domains, KeyType::EcdsaP256)
847 .await
848 .unwrap();
849 h.storage.save_cert(&h.slot.id, &stored).await.unwrap();
850 let adopted = wait_state(&h.certs, &h.slot.id, CertState::Issued).await;
851 assert!(adopted.meta.issuer.contains("Mock ACME CA"));
852 assert_eq!(h.client.orders(), 1, "only the peer's order happened");
853 }
854
855 #[tokio::test]
863 async fn peer_adoption_resets_backoff_so_no_stale_duplicate_order_follows() {
864 let h = harness(
865 "peer_backoff",
866 MockBehavior {
867 fail_step: Some(MockStep::NewOrder),
868 ..Default::default()
869 },
870 );
871 let m = manager(&h, fast_cfg());
872 m.clone().run().await;
873 wait_failed_placeholder(&h.certs, &h.slot.id).await;
875 assert_eq!(h.client.orders(), 1);
876
877 assert!(h
881 .storage
882 .try_acquire_lease(&h.slot.id, "peer", Duration::from_secs(30))
883 .await
884 .unwrap());
885 force_renew_scheduled(&m, h.slot.id.as_str()).await;
886 tokio::time::sleep(Duration::from_millis(100)).await;
887 assert_eq!(
888 h.client.orders(),
889 1,
890 "must not order while a peer holds the lease"
891 );
892
893 h.client.set_behavior(MockBehavior::default());
896 let solver = TlsAlpnSolver::new(h.storage.clone());
897 let stored =
898 crate::acme::order::issue(&h.client, &solver, &h.slot.domains, KeyType::EcdsaP256)
899 .await
900 .unwrap();
901 h.storage.save_cert(&h.slot.id, &stored).await.unwrap();
902 let adopted = wait_state(&h.certs, &h.slot.id, CertState::Issued).await;
903 assert!(adopted.meta.issuer.contains("Mock ACME CA"));
904 assert_eq!(
905 h.client.orders(),
906 2,
907 "our failed attempt + the peer's order"
908 );
909
910 let scheduled = wait_until(
919 &h.certs,
920 &h.slot.id,
921 "next_renewal_at recorded after adoption (proves the failure \
922 streak was reset, not stuck on the stale-backoff branch)",
923 |c| c.state == CertState::Issued && c.meta.next_renewal_at.is_some(),
924 )
925 .await;
926 assert!(scheduled.meta.next_renewal_at.unwrap() > now_unix());
927
928 tokio::time::sleep(Duration::from_millis(300)).await;
931 assert_eq!(
932 h.client.orders(),
933 2,
934 "adopting a peer's certificate must reset local backoff state, \
935 not leave a duplicate order pending"
936 );
937 }
938
939 #[tokio::test]
944 async fn a_placeholder_close_to_expiry_is_reminted_while_staying_a_placeholder() {
945 let h = harness(
946 "remint",
947 MockBehavior {
948 fail_step: Some(MockStep::NewOrder),
949 ..Default::default()
950 },
951 );
952 let seeded = h.certs.load().get(h.slot.id.as_str()).unwrap().clone();
954 let before = seeded.leaf_der.clone();
955 publish(
956 &h.certs,
957 &h.slot.id,
958 ManagedCert {
959 meta: crate::acme::CertMeta {
960 not_after: now_unix() + 60,
961 ..seeded.meta.clone()
962 },
963 ..seeded
964 },
965 );
966
967 let m = manager(&h, fast_cfg());
968 m.clone().run().await;
969
970 let reminted = wait_until(&h.certs, &h.slot.id, "a re-minted placeholder", |c| {
971 c.state == CertState::Placeholder && c.leaf_der != before
972 })
973 .await;
974 assert_eq!(reminted.state, CertState::Placeholder);
975 assert!(
976 reminted.meta.not_after > now_unix() + 60,
977 "the fresh placeholder carries a full validity window: {:?}",
978 reminted.meta
979 );
980 wait_failed_placeholder(&h.certs, &h.slot.id).await;
982 }
983}