From c2b8900488c596a582a241390af495d1b8d683b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20H=C3=B6lzl?= Date: Tue, 29 Apr 2025 10:33:58 +0200 Subject: [PATCH 1/7] polling for challenge ready and certs with timeout --- src/order.rs | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 86 insertions(+) diff --git a/src/order.rs b/src/order.rs index 341205a..8cf701b 100644 --- a/src/order.rs +++ b/src/order.rs @@ -194,6 +194,92 @@ impl Order { Ok(&self.state) } + /// Poll the order with timeout until in a final state + /// + /// Refresh the order state from the server with a timeout `total_tmo`. + /// The polling interval has an exponential characteristic starting with `min_delay`. + /// The delay is increased by a factor of two till a maximum delay for polling is reached. + /// After that a constant delay `max_delay` is used until the provided timeout is reached. + /// + /// Yields the [`OrderStatus`] immediately if `Ready` or `Invalid`. + pub async fn poll_ready( + &mut self, + min_delay: Duration, + max_delay: Duration, + total_tmo: Duration, + ) -> Result { + let mut polling: Duration = Duration::from_secs(0); + + let mut delay: Duration = min_delay; + + loop { + if delay > max_delay { + delay = max_delay; + } + + // adjust the final polling interval + if polling + delay > total_tmo + min_delay { + polling = total_tmo - delay; + } + + sleep(delay).await; + + let state = self.refresh().await?; + + polling += delay; + + if let OrderStatus::Ready | OrderStatus::Invalid = state.status { + return Ok(state.status); + } else if polling >= total_tmo { + return Ok(state.status); + } + + delay *= 2; + } + } + + /// Wait for certificate with timeout + /// + /// Query the issued certificate from the server with a timeout `total_tmo`. + /// The polling interval has an exponential characteristic starting with `min_delay`. + /// The delay is increased by a factor of two till a maximum delay for polling is reached. + /// After that a constant delay `max_delay` is used until the provided timeout is reached. + /// + /// Yields the certificate for the order. + pub async fn poll_cert( + &mut self, + min_delay: Duration, + max_delay: Duration, + total_tmo: Duration, + ) -> Result, Error> { + let mut polling: Duration = Duration::from_secs(0); + + let mut delay: Duration = min_delay; + + loop { + if delay > max_delay { + delay = max_delay; + } + + // adjust the final polling interval + if polling + delay > total_tmo + min_delay { + polling = total_tmo - delay; + } + + sleep(delay).await; + + let cert_chain = self.certificate().await?; + + polling += delay; + + if cert_chain.is_some() || polling >= total_tmo { + return Ok(cert_chain); + } + + delay *= 2; + } + } + /// Extract the URL and last known state from the `Order` pub fn into_parts(self) -> (String, OrderState) { (self.url, self.state) From 66b945c041af70649bd98be687010f51b90701e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20H=C3=B6lzl?= Date: Tue, 6 May 2025 09:53:01 +0200 Subject: [PATCH 2/7] next iteration with retry-after and httpdate (wait_ready) --- Cargo.toml | 1 + src/lib.rs | 9 +++++++ src/order.rs | 70 +++++++++++++++++++++++++++++++++------------------- 3 files changed, 55 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index edf4e38..43a2971 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -25,6 +25,7 @@ aws-lc-rs = { version = "1.8.0", optional = true } base64 = "0.22" bytes = "1" http = "1" +httpdate = "1.0" http-body = "1" http-body-util = "0.1.2" hyper = { version = "1.3.1", features = ["client", "http1", "http2"], optional = true } diff --git a/src/lib.rs b/src/lib.rs index 6169a30..35256ca 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,6 +158,13 @@ fn nonce_from_response(rsp: &BytesResponse) -> Option { .and_then(|hv| String::from_utf8(hv.as_ref().to_vec()).ok()) } +fn retry_after_from_response(rsp: &BytesResponse) -> Option { + rsp.parts + .headers + .get(RETRY_AFTER) + .and_then(|header_value| String::from_utf8(header_value.as_ref().to_vec()).ok()) +} + #[cfg(feature = "hyper-rustls")] struct DefaultClient(HyperClient, Full>); @@ -320,6 +327,8 @@ mod crypto { const JOSE_JSON: &str = "application/jose+json"; const REPLAY_NONCE: &str = "Replay-Nonce"; +const RETRY_AFTER: &str = "Retry-After"; + #[cfg(test)] mod tests { diff --git a/src/order.rs b/src/order.rs index 8cf701b..7526cbd 100644 --- a/src/order.rs +++ b/src/order.rs @@ -196,46 +196,66 @@ impl Order { /// Poll the order with timeout until in a final state /// - /// Refresh the order state from the server with a timeout `total_tmo`. - /// The polling interval has an exponential characteristic starting with `min_delay`. - /// The delay is increased by a factor of two till a maximum delay for polling is reached. - /// After that a constant delay `max_delay` is used until the provided timeout is reached. + /// Refresh the order state from the server with a timeout `timeout`. + /// If a server sets the Retry-After header for rate-limiting access to the CA, the + /// provided value is used for polling as long as it fits into the boxed timeout window. /// /// Yields the [`OrderStatus`] immediately if `Ready` or `Invalid`. - pub async fn poll_ready( - &mut self, - min_delay: Duration, - max_delay: Duration, - total_tmo: Duration, - ) -> Result { - let mut polling: Duration = Duration::from_secs(0); + pub async fn wait_ready(&mut self, timeout: Duration) -> Result { + let started = std::time::Instant::now(); - let mut delay: Duration = min_delay; + // Yields the order status immediately if Ready or Invalid. + let mut next_retry = Duration::from_secs(0); + + // This is the same retry fallback as ACME4J + let fallback_retry = Duration::from_secs(3); + + let boxed = started + timeout; loop { - if delay > max_delay { - delay = max_delay; + let now = std::time::Instant::now(); + + if now > boxed { + break; } - // adjust the final polling interval - if polling + delay > total_tmo + min_delay { - polling = total_tmo - delay; + if now + next_retry > boxed { + next_retry = boxed - now; } - sleep(delay).await; + sleep(next_retry).await; - let state = self.refresh().await?; + let rsp = self + .account + .post(None::<&Empty>, self.nonce.take(), &self.url) + .await?; - polling += delay; + self.nonce = nonce_from_response(&rsp); - if let OrderStatus::Ready | OrderStatus::Invalid = state.status { - return Ok(state.status); - } else if polling >= total_tmo { - return Ok(state.status); + next_retry = fallback_retry; + + // Should retry_after become a member of Order that is updated in refresh()? + + if let Some(retry_after) = retry_after_from_response(&rsp) { + if let Ok(retry_after_datetime) = httpdate::parse_http_date(&retry_after) { + if let Ok(next_duration) = + retry_after_datetime.duration_since(std::time::SystemTime::now()) + { + next_retry = next_duration; + } + } else if let Ok(retry_after_seconds) = retry_after.parse::() { + next_retry = Duration::from_secs(retry_after_seconds); + } } - delay *= 2; + self.state = Problem::check::(rsp).await?; + + if let OrderStatus::Ready | OrderStatus::Invalid = self.state.status { + return Ok(self.state.status); + }; } + + Ok(self.state.status) } /// Wait for certificate with timeout From a8a06089a8278e0b13bf6c039375072088d9e45b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20H=C3=B6lzl?= Date: Tue, 6 May 2025 09:59:25 +0200 Subject: [PATCH 3/7] next iteration with retry-after and httpdate (wait_ready) --- src/order.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/order.rs b/src/order.rs index 7526cbd..6b1ef3a 100644 --- a/src/order.rs +++ b/src/order.rs @@ -14,7 +14,7 @@ use crate::types::{ Authorization, AuthorizationState, AuthorizationStatus, AuthorizedIdentifier, Challenge, ChallengeType, Empty, FinalizeRequest, OrderState, OrderStatus, Problem, }; -use crate::{Error, Key, crypto, nonce_from_response}; +use crate::{Error, Key, crypto, nonce_from_response, retry_after_from_response}; /// An ACME order as described in RFC 8555 (section 7.1.3) /// From 4321544a0b887268b619191b1e7f034ea2070770 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20H=C3=B6lzl?= Date: Tue, 6 May 2025 10:29:22 +0200 Subject: [PATCH 4/7] lint --- src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 35256ca..617a9fb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -329,7 +329,6 @@ const JOSE_JSON: &str = "application/jose+json"; const REPLAY_NONCE: &str = "Replay-Nonce"; const RETRY_AFTER: &str = "Retry-After"; - #[cfg(test)] mod tests { use super::*; From 1110d5ab6ff6f107e937ab253491cb25abe8330a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20H=C3=B6lzl?= Date: Tue, 6 May 2025 10:53:09 +0200 Subject: [PATCH 5/7] lint and deduplication --- src/order.rs | 81 ++++++++++++++++++++-------------------------------- 1 file changed, 31 insertions(+), 50 deletions(-) diff --git a/src/order.rs b/src/order.rs index 6b1ef3a..cb1a71b 100644 --- a/src/order.rs +++ b/src/order.rs @@ -202,6 +202,29 @@ impl Order { /// /// Yields the [`OrderStatus`] immediately if `Ready` or `Invalid`. pub async fn wait_ready(&mut self, timeout: Duration) -> Result { + self.wait_status_internal(timeout, &[OrderStatus::Ready, OrderStatus::Invalid]) + .await + } + + /// Wait for certificate with timeout + /// + /// Query the issued certificate from the server with a timeout `timeout`. + /// If a server sets the Retry-After header for rate-limiting access to the CA, the + /// provided value is used for polling as long as it fits into the boxed timeout window. + /// + /// Yields the certificate for the order. + pub async fn wait_certificate(&mut self, timeout: Duration) -> Result, Error> { + let _ = self + .wait_status_internal(timeout, &[OrderStatus::Valid, OrderStatus::Invalid]) + .await?; + self.certificate().await + } + + async fn wait_status_internal( + &mut self, + timeout: Duration, + order_states: &[OrderStatus], + ) -> Result { let started = std::time::Instant::now(); // Yields the order status immediately if Ready or Invalid. @@ -213,16 +236,6 @@ impl Order { let boxed = started + timeout; loop { - let now = std::time::Instant::now(); - - if now > boxed { - break; - } - - if now + next_retry > boxed { - next_retry = boxed - now; - } - sleep(next_retry).await; let rsp = self @@ -250,54 +263,22 @@ impl Order { self.state = Problem::check::(rsp).await?; - if let OrderStatus::Ready | OrderStatus::Invalid = self.state.status { + if order_states.contains(&self.state.status) { return Ok(self.state.status); }; - } - - Ok(self.state.status) - } - - /// Wait for certificate with timeout - /// - /// Query the issued certificate from the server with a timeout `total_tmo`. - /// The polling interval has an exponential characteristic starting with `min_delay`. - /// The delay is increased by a factor of two till a maximum delay for polling is reached. - /// After that a constant delay `max_delay` is used until the provided timeout is reached. - /// - /// Yields the certificate for the order. - pub async fn poll_cert( - &mut self, - min_delay: Duration, - max_delay: Duration, - total_tmo: Duration, - ) -> Result, Error> { - let mut polling: Duration = Duration::from_secs(0); - - let mut delay: Duration = min_delay; - loop { - if delay > max_delay { - delay = max_delay; - } + let now = std::time::Instant::now(); - // adjust the final polling interval - if polling + delay > total_tmo + min_delay { - polling = total_tmo - delay; + if now > boxed { + break; } - sleep(delay).await; - - let cert_chain = self.certificate().await?; - - polling += delay; - - if cert_chain.is_some() || polling >= total_tmo { - return Ok(cert_chain); + if now + next_retry > boxed { + next_retry = boxed - now; } - - delay *= 2; } + + Ok(self.state.status) } /// Extract the URL and last known state from the `Order` From 29f6984940342c4b565d674a37955b308e198c08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20H=C3=B6lzl?= Date: Tue, 13 May 2025 21:31:51 +0200 Subject: [PATCH 6/7] api change and pebble test --- examples/provision.rs | 8 +- src/account.rs | 8 +- src/lib.rs | 15 +++- src/order.rs | 167 +++++++++++++++++++++++------------------- tests/pebble.rs | 31 +++++++- 5 files changed, 149 insertions(+), 80 deletions(-) diff --git a/examples/provision.rs b/examples/provision.rs index 3425e80..89a3752 100644 --- a/examples/provision.rs +++ b/examples/provision.rs @@ -7,7 +7,7 @@ use tracing::info; use instant_acme::{ Account, AuthorizationStatus, ChallengeType, Identifier, LetsEncrypt, NewAccount, NewOrder, - OrderStatus, + OrderStatus, PollingStrategy, }; #[tokio::main] @@ -81,7 +81,11 @@ async fn main() -> anyhow::Result<()> { // Exponentially back off until the order becomes ready or invalid. - let status = order.poll(5, Duration::from_millis(250)).await?; + let polling_strategy = PollingStrategy::ExponentialBackoff { + tries: 5, + delay: Duration::from_millis(250), + }; + let status = order.poll(polling_strategy).await?; if status != OrderStatus::Ready { return Err(anyhow::anyhow!("unexpected order status: {status:?}")); } diff --git a/src/account.rs b/src/account.rs index a1733b1..66b6869 100644 --- a/src/account.rs +++ b/src/account.rs @@ -19,7 +19,10 @@ use crate::types::{ }; #[cfg(feature = "time")] use crate::types::{CertificateIdentifier, RenewalInfo}; -use crate::{BytesResponse, Client, Error, HttpClient, crypto, nonce_from_response}; +use crate::{ + BytesResponse, Client, Error, HttpClient, crypto, nonce_from_response, + retry_after_from_response, +}; /// An ACME account as described in RFC 8555 (section 7.1.2) /// @@ -185,6 +188,7 @@ impl Account { .await?; let nonce = nonce_from_response(&rsp); + let retry_after = retry_after_from_response(&rsp); let order_url = rsp .parts .headers @@ -214,6 +218,7 @@ impl Account { Ok(Order { account: self.inner.clone(), nonce, + retry_after, state, url: order_url.ok_or("no order URL found")?, }) @@ -229,6 +234,7 @@ impl Account { Ok(Order { account: self.inner.clone(), nonce: nonce_from_response(&rsp), + retry_after: retry_after_from_response(&rsp), // Order of fields matters! We return errors from Problem::check // before emitting an error if there is no order url. Or the // simple no url error hides the causing error in `Problem::check`. diff --git a/src/lib.rs b/src/lib.rs index 617a9fb..8ab6487 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,6 +8,7 @@ use std::error::Error as StdError; use std::fmt; use std::future::Future; use std::pin::Pin; +use std::time::Duration; use async_trait::async_trait; use bytes::Bytes; @@ -28,6 +29,7 @@ pub use account::{Account, ExternalAccountKey}; mod order; pub use order::{ AuthorizationHandle, Authorizations, ChallengeHandle, Identifiers, KeyAuthorization, Order, + PollingStrategy, }; mod types; #[cfg(feature = "time")] @@ -158,11 +160,22 @@ fn nonce_from_response(rsp: &BytesResponse) -> Option { .and_then(|hv| String::from_utf8(hv.as_ref().to_vec()).ok()) } -fn retry_after_from_response(rsp: &BytesResponse) -> Option { +fn retry_after_from_response(rsp: &BytesResponse) -> Option { rsp.parts .headers .get(RETRY_AFTER) .and_then(|header_value| String::from_utf8(header_value.as_ref().to_vec()).ok()) + .and_then(|retry_after_header| { + if let Ok(retry_after_datetime) = httpdate::parse_http_date(&retry_after_header) { + retry_after_datetime + .duration_since(std::time::SystemTime::now()) + .ok() + } else if let Ok(retry_after_seconds) = retry_after_header.parse::() { + Some(Duration::from_secs(retry_after_seconds)) + } else { + None + } + }) } #[cfg(feature = "hyper-rustls")] diff --git a/src/order.rs b/src/order.rs index cb1a71b..3e70014 100644 --- a/src/order.rs +++ b/src/order.rs @@ -26,10 +26,39 @@ use crate::{Error, Key, crypto, nonce_from_response, retry_after_from_response}; pub struct Order { pub(crate) account: Arc, pub(crate) nonce: Option, + pub(crate) retry_after: Option, pub(crate) url: String, pub(crate) state: OrderState, } +/// Definition of polling strategy to wait for an `Order` status +#[derive(Debug, Clone)] +pub enum PollingStrategy { + /// Exponential backoff + /// Retry polling the `Order` status from the ACME server for `tries` times, + /// waiting `delay` before the first attempt and increasing the delay + /// by a factor of 2 for each subsequent attempt. + /// This strategy is good to achieve a low latency for DNS-01 challenge. + /// (Empirically, we've had good results with 5 tries and an initial delay of 250ms.) + ExponentialBackoff { + /// Number of retries + tries: usize, + /// Initial delay that is doubled for subsequent attempts. + /// Note that a large value of tries + delay: Duration, + }, + /// Total timeout with rate limiting + /// Refresh the order state from the ACME server with a timeout `total_timeout`. + /// If a server sets the `Retry-After` header for rate-limiting access to the CA, the + /// provided value is used for polling as long as it fits into the boxed timeout window. + /// If the ACME server does not provide `Retry-After` polling is repeated every 3 seconds. + /// Use this strategy if you want to achieve uniform distributed status polling. + TotalTimeoutWithRateLimiting { + /// A total timeout that is used for polling attempts + total_timeout: Duration, + }, +} + impl Order { /// Retrieve the authorizations for this order /// @@ -113,6 +142,7 @@ impl Order { .post(None::<&Empty>, self.nonce.take(), &self.url) .await?; self.nonce = nonce_from_response(&rsp); + self.retry_after = retry_after_from_response(&rsp); self.state = Problem::check::(rsp).await?; } @@ -157,29 +187,18 @@ impl Order { } } - /// Poll the order with exponential backoff until in a final state - /// - /// Refresh the order state from the server for `tries` times, waiting `delay` before the - /// first attempt and increasing the delay by a factor of 2 for each subsequent attempt. + /// Poll the order until in a final state /// - /// Yields the [`OrderStatus`] immediately if `Ready` or `Invalid`, or after `tries` attempts. + /// Provide a polling strategy `polling_strategy` to adjust the frequency the ACME server + /// is polls the `OrderStatus`. /// - /// (Empirically, we've had good results with 5 tries and an initial delay of 250ms.) - pub async fn poll(&mut self, mut tries: u8, mut delay: Duration) -> Result { - loop { - sleep(delay).await; - let state = self.refresh().await?; - if let Some(error) = &state.error { - return Err(Error::Api(error.clone())); - } else if let OrderStatus::Ready | OrderStatus::Invalid = state.status { - return Ok(state.status); - } else if tries <= 1 { - return Ok(state.status); - } - - delay *= 2; - tries -= 1; - } + /// Yields the [`OrderStatus`] immediately if `Ready` or `Invalid`. + pub async fn poll(&mut self, polling_strategy: PollingStrategy) -> Result { + self.wait_status_internal( + polling_strategy, + &[OrderStatus::Ready, OrderStatus::Invalid], + ) + .await } /// Refresh the current state of the order @@ -190,91 +209,91 @@ impl Order { .await?; self.nonce = nonce_from_response(&rsp); + self.retry_after = retry_after_from_response(&rsp); self.state = Problem::check::(rsp).await?; Ok(&self.state) } - /// Poll the order with timeout until in a final state - /// - /// Refresh the order state from the server with a timeout `timeout`. - /// If a server sets the Retry-After header for rate-limiting access to the CA, the - /// provided value is used for polling as long as it fits into the boxed timeout window. - /// - /// Yields the [`OrderStatus`] immediately if `Ready` or `Invalid`. - pub async fn wait_ready(&mut self, timeout: Duration) -> Result { - self.wait_status_internal(timeout, &[OrderStatus::Ready, OrderStatus::Invalid]) - .await - } - /// Wait for certificate with timeout /// - /// Query the issued certificate from the server with a timeout `timeout`. - /// If a server sets the Retry-After header for rate-limiting access to the CA, the - /// provided value is used for polling as long as it fits into the boxed timeout window. + /// Provide a polling strategy `polling_strategy` to adjust the frequency the ACME server + /// is polls the certificate. /// /// Yields the certificate for the order. - pub async fn wait_certificate(&mut self, timeout: Duration) -> Result, Error> { + pub async fn wait_certificate( + &mut self, + polling_strategy: PollingStrategy, + ) -> Result, Error> { let _ = self - .wait_status_internal(timeout, &[OrderStatus::Valid, OrderStatus::Invalid]) + .wait_status_internal( + polling_strategy, + &[OrderStatus::Valid, OrderStatus::Invalid], + ) .await?; self.certificate().await } + /// Wait by timeout until a defined order status is reached + /// + /// This method periodically polls the Order status. It yields the status immediately + /// if it is contained in the `order_states` array. Waiting ends if the `timeout` is + /// reached. + /// + /// Polling the status is optimized and respects a `Retry-After` header if the ACME server + /// is providing this HTTP header for rate-limiting access. The default polling interval is + /// three seconds if no such HTTP header is there. async fn wait_status_internal( &mut self, - timeout: Duration, + polling_strategy: PollingStrategy, order_states: &[OrderStatus], ) -> Result { let started = std::time::Instant::now(); + // let boxed = std::time::Instant::now() + timeout; - // Yields the order status immediately if Ready or Invalid. - let mut next_retry = Duration::from_secs(0); + // Yields the order status immediately if contained in order_states. + let mut next_retry = match polling_strategy { + PollingStrategy::TotalTimeoutWithRateLimiting { .. } => Duration::from_secs(0), + PollingStrategy::ExponentialBackoff { delay, .. } => delay, + }; // This is the same retry fallback as ACME4J let fallback_retry = Duration::from_secs(3); - let boxed = started + timeout; - loop { sleep(next_retry).await; - let rsp = self - .account - .post(None::<&Empty>, self.nonce.take(), &self.url) - .await?; - - self.nonce = nonce_from_response(&rsp); - - next_retry = fallback_retry; + self.refresh().await?; - // Should retry_after become a member of Order that is updated in refresh()? - - if let Some(retry_after) = retry_after_from_response(&rsp) { - if let Ok(retry_after_datetime) = httpdate::parse_http_date(&retry_after) { - if let Ok(next_duration) = - retry_after_datetime.duration_since(std::time::SystemTime::now()) - { - next_retry = next_duration; - } - } else if let Ok(retry_after_seconds) = retry_after.parse::() { - next_retry = Duration::from_secs(retry_after_seconds); - } - } - - self.state = Problem::check::(rsp).await?; - - if order_states.contains(&self.state.status) { + if let Some(error) = &self.state.error { + return Err(Error::Api(error.clone())); + } else if order_states.contains(&self.state.status) { return Ok(self.state.status); }; - let now = std::time::Instant::now(); + match polling_strategy { + PollingStrategy::ExponentialBackoff { + mut tries, + delay: _, + } => { + tries -= 1; + if tries < 1 { + break; + } + next_retry *= 2; + } + PollingStrategy::TotalTimeoutWithRateLimiting { total_timeout } => { + next_retry = self.retry_after.take().unwrap_or(fallback_retry); + let now = std::time::Instant::now(); - if now > boxed { - break; - } + if now > started + total_timeout { + break; + } - if now + next_retry > boxed { - next_retry = boxed - now; + // Adjustment of the last retry to not exceed the total timeout. + if now + next_retry > started + total_timeout { + next_retry = started + total_timeout - now; + } + } } } diff --git a/tests/pebble.rs b/tests/pebble.rs index 8337e37..4da1d92 100644 --- a/tests/pebble.rs +++ b/tests/pebble.rs @@ -25,7 +25,7 @@ use hyper_util::client::legacy::connect::HttpConnector; use hyper_util::rt::TokioExecutor; use instant_acme::{ Account, AuthorizationStatus, ChallengeHandle, ChallengeType, Error, ExternalAccountKey, - Identifier, KeyAuthorization, NewAccount, NewOrder, Order, OrderStatus, + Identifier, KeyAuthorization, NewAccount, NewOrder, Order, OrderStatus, PollingStrategy, }; #[cfg(all(feature = "time", feature = "x509-parser"))] use instant_acme::{CertificateIdentifier, RevocationRequest}; @@ -63,6 +63,26 @@ async fn http_01() -> Result<(), Box> { .map(|_| ()) } +#[tokio::test] +#[ignore] +async fn poll_with_timeout() -> Result<(), Box> { + try_tracing_init(); + + let mut identifiers = dns_identifiers(["http01.example.com"]); + identifiers.push(Identifier::Ip(IpAddr::from_str("::1").unwrap())); + identifiers.push(Identifier::Ip(IpAddr::from_str("127.0.0.1").unwrap())); + + let mut env = Environment::new(EnvironmentConfig::default()).await?; + + env.polling_strategy = PollingStrategy::TotalTimeoutWithRateLimiting { + total_timeout: Duration::from_secs(60), + }; + + env.test::(&NewOrder::new(&identifiers)) + .await + .map(|_| ()) +} + #[tokio::test] #[ignore] async fn dns_01() -> Result<(), Box> { @@ -397,6 +417,7 @@ struct Environment { #[allow(dead_code)] // Held for the lifetime of the environment. challtestsrv: Subprocess, client: HyperClient, Full>, + polling_strategy: PollingStrategy, } impl Environment { @@ -489,6 +510,11 @@ impl Environment { .await?; info!(account_id = account.id(), "created ACME account"); + let polling_strategy = PollingStrategy::ExponentialBackoff { + tries: 10, + delay: Duration::from_millis(250), + }; + Ok(Self { account, config, @@ -496,6 +522,7 @@ impl Environment { pebble, challtestsrv, client, + polling_strategy, }) } @@ -530,7 +557,7 @@ impl Environment { } // Poll until the order is ready. - let status = order.poll(10, Duration::from_millis(250)).await?; + let status = order.poll(self.polling_strategy.clone()).await?; if status != OrderStatus::Ready { return Err(format!("unexpected order status: {status:?}").into()); } From d42ec9a332944960bda218512b058f387ffcbd84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Christian=20H=C3=B6lzl?= Date: Tue, 13 May 2025 22:36:48 +0200 Subject: [PATCH 7/7] fixed comments --- src/order.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/order.rs b/src/order.rs index 3e70014..52ec1ee 100644 --- a/src/order.rs +++ b/src/order.rs @@ -44,7 +44,7 @@ pub enum PollingStrategy { /// Number of retries tries: usize, /// Initial delay that is doubled for subsequent attempts. - /// Note that a large value of tries + /// Note that for a large `tries` value the delays become exponentially long. delay: Duration, }, /// Total timeout with rate limiting @@ -189,8 +189,8 @@ impl Order { /// Poll the order until in a final state /// - /// Provide a polling strategy `polling_strategy` to adjust the frequency the ACME server - /// is polls the `OrderStatus`. + /// Provide a polling strategy `polling_strategy` to adjust how frequently the ACME server + /// is polled until a final `OrderStatus` state (`Ready` or `Invalid`) is reached. /// /// Yields the [`OrderStatus`] immediately if `Ready` or `Invalid`. pub async fn poll(&mut self, polling_strategy: PollingStrategy) -> Result { @@ -216,8 +216,8 @@ impl Order { /// Wait for certificate with timeout /// - /// Provide a polling strategy `polling_strategy` to adjust the frequency the ACME server - /// is polls the certificate. + /// Provide a polling strategy `polling_strategy` to adjust how frequently the ACME server + /// is polled to yield a certificate. /// /// Yields the certificate for the order. pub async fn wait_certificate( @@ -248,7 +248,6 @@ impl Order { order_states: &[OrderStatus], ) -> Result { let started = std::time::Instant::now(); - // let boxed = std::time::Instant::now() + timeout; // Yields the order status immediately if contained in order_states. let mut next_retry = match polling_strategy {