Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/provision.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ use tracing::info;

use instant_acme::{
Account, AuthorizationStatus, ChallengeType, Identifier, LetsEncrypt, NewAccount, NewOrder,
OrderStatus,
OrderStatus, RetryPolicy,
};

#[tokio::main]
Expand Down Expand Up @@ -81,7 +81,7 @@ 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 status = order.poll(&RetryPolicy::default()).await?;
if status != OrderStatus::Ready {
return Err(anyhow::anyhow!("unexpected order status: {status:?}"));
}
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ pub use account::{Account, ExternalAccountKey};
mod order;
pub use order::{
AuthorizationHandle, Authorizations, ChallengeHandle, Identifiers, KeyAuthorization, Order,
RetryPolicy,
};
mod types;
#[cfg(feature = "time")]
Expand Down
90 changes: 76 additions & 14 deletions src/order.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::ops::Deref;
use std::ops::{ControlFlow, Deref};
use std::sync::Arc;
use std::time::Duration;
use std::{fmt, slice};
Expand Down Expand Up @@ -157,28 +157,22 @@ 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 with the given [`RetryPolicy`]
///
/// Yields the [`OrderStatus`] immediately if `Ready` or `Invalid`, or after `tries` attempts.
///
/// (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<OrderStatus, Error> {
pub async fn poll(&mut self, retries: &RetryPolicy) -> Result<OrderStatus, Error> {
let mut retrying = retries.state();
loop {
sleep(delay).await;
if let ControlFlow::Break(()) = retrying.wait().await {
return Ok(self.state.status);
}

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;
}
}

Expand Down Expand Up @@ -491,3 +485,71 @@ impl fmt::Debug for KeyAuthorization {
f.debug_tuple("KeyAuthorization").finish()
}
}

/// A policy for retrying API requests
///
/// 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.
#[derive(Debug, Clone, Copy)]
pub struct RetryPolicy {
tries: u8,
delay: Duration,
}

impl RetryPolicy {
/// A constructor for the default `RetryPolicy`
///
/// Will retry 5 times with an initial delay of 250ms.
pub const fn new() -> Self {
Self {
tries: 5,
delay: Duration::from_millis(250),
}
}

/// Set the initial delay
///
/// This is the delay before the first retry attempt. The delay will be doubled for each
/// subsequent retry attempt.
pub const fn initial_delay(mut self, delay: Duration) -> Self {
self.delay = delay;
self
}

/// Set the number of retry attempts
pub const fn tries(mut self, tries: u8) -> Self {
self.tries = tries;
self
}

fn state(&self) -> RetryState {
RetryState {
tries: self.tries,
delay: self.delay,
}
}
}

impl Default for RetryPolicy {
fn default() -> Self {
Self::new()
}
}

struct RetryState {
tries: u8,
delay: Duration,
}

impl RetryState {
async fn wait(&mut self) -> ControlFlow<(), ()> {
if self.tries == 0 {
return ControlFlow::Break(());
}

sleep(self.delay).await;
self.delay *= 2;
self.tries -= 1;
ControlFlow::Continue(())
}
}
5 changes: 3 additions & 2 deletions tests/pebble.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, RetryPolicy,
};
#[cfg(all(feature = "time", feature = "x509-parser"))]
use instant_acme::{CertificateIdentifier, RevocationRequest};
Expand Down Expand Up @@ -530,7 +530,7 @@ impl Environment {
}

// Poll until the order is ready.
let status = order.poll(10, Duration::from_millis(250)).await?;
let status = order.poll(&RETRY_POLICY).await?;
if status != OrderStatus::Ready {
return Err(format!("unexpected order status: {status:?}").into());
}
Expand Down Expand Up @@ -900,3 +900,4 @@ impl Drop for Subprocess {
}

static NEXT_PORT: AtomicU16 = AtomicU16::new(5555);
const RETRY_POLICY: RetryPolicy = RetryPolicy::new().tries(10);