Skip to content

Commit e3d9394

Browse files
lwshangclaude
andauthored
chore: replace unmaintained backoff with backon (release 0.49.1) (#738)
* chore: replace unmaintained backoff with backon `backoff` is flagged as unmaintained. Replace it with `backon` for the `request_status` polling backoff in ic-agent. - Wrap `backon::ExponentialBackoff` in a small `RetryPolicy` that tracks wall-clock elapsed time (via `web-time`, so it works on wasm32) and bounds polling by `max_polling_time`. Delay schedule is unchanged: 500ms initial, x1.4 growth capped at 1s, jittered. - `backoff/wasm-bindgen` previously enabled `getrandom` 0.2's `js` backend for the transitive crypto crates on wasm. Restore that explicitly via a renamed `getrandom_0_2` dependency gated on ic-agent's `wasm-bindgen` feature. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * chore: release 0.49.1 — replace unmaintained backoff with backon Bump all workspace crates 0.49.0 -> 0.49.1. This is a patch release: the backoff -> backon swap is an internal dependency change with no public API or behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * refactor: use backon ExponentialBackoff directly, drop RetryPolicy The RetryPolicy wrapper was a 1:1 re-implementation of the old `backoff::Backoff` trait (next_backoff / reset / get_elapsed_time). The polling schedule is an internal detail, so lean on backon's building blocks instead: - Drive `backon::ExponentialBackoff` (an Iterator<Item = Duration>) directly in the poll loops. - Bound the polling budget with backon's `with_total_delay(max_polling_time)` so the iterator yields None once the cumulative sleep is spent — no manual elapsed-time bookkeeping. - Track a small `slept` accumulator only for the existing 5-minute "Unknown => ingress message lost" guard. This also drops the `web-time` dependency added earlier (it was only needed for the wrapper's wall-clock Instant). `max_polling_time` now bounds cumulative backoff sleep rather than wall-clock elapsed; in practice they differ only by per-poll request latency. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * review: clarify slept-based timeout comment; align getrandom_0_2 enablement Address Copilot review feedback on PR #738: - Clarify the `Unknown => ingress message lost` comments in both poll loops: the 5-minute threshold is now cumulative backoff sleep (`slept`), not wall-clock, so it excludes per-poll request latency. - Enable `getrandom_0_2` via `dep:getrandom_0_2` + `features = ["js"]` on the dependency, mirroring the sibling `getrandom` (0.4) entry exactly. This is unambiguous and suppresses the implicit `getrandom_0_2` feature that `getrandom_0_2/js` would otherwise leak on ic-agent's public feature set. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent e9e76f7 commit e3d9394

5 files changed

Lines changed: 82 additions & 41 deletions

File tree

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88

99
## Unreleased
1010

11+
## [0.49.1] - 2026-07-20
12+
13+
* `ic-agent`: Replaced the unmaintained `backoff` dependency with `backon` for the `request_status` polling backoff, using `backon`'s `ExponentialBackoff` iterator directly. The schedule is unchanged (500ms initial delay, growing by 1.4x up to 1s, jittered). `max_polling_time` now bounds the cumulative backoff sleep (via `backon`'s `with_total_delay`) rather than wall-clock elapsed time; in practice they differ only by per-poll request latency.
14+
1115
## [0.49.0] - 2026-07-13
1216

1317
* `ic-utils`: Bump `ic-management-canister-types` to 0.8.0.

Cargo.lock

Lines changed: 18 additions & 11 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ members = [
1313
]
1414

1515
[workspace.package]
16-
version = "0.49.0"
16+
version = "0.49.1"
1717
authors = ["DFINITY Stiftung <sdk@dfinity.org>"]
1818
edition = "2021"
1919
repository = "https://github.com/dfinity/agent-rs"
@@ -30,10 +30,10 @@ license = "Apache-2.0"
3030
# a comment listing those crates). Otherwise, features are declared in the individual crate Cargo.toml.
3131
#
3232
# The path dependencies below ensure all workspace members use the same version of internal crates.
33-
ic-agent = { path = "ic-agent", version = "0.49.0", default-features = false }
34-
ic-identity-hsm = { path = "ic-identity-hsm", version = "0.49.0" }
35-
ic-transport-types = { path = "ic-transport-types", version = "0.49.0" }
36-
ic-utils = { path = "ic-utils", version = "0.49.0" }
33+
ic-agent = { path = "ic-agent", version = "0.49.1", default-features = false }
34+
ic-identity-hsm = { path = "ic-identity-hsm", version = "0.49.1" }
35+
ic-transport-types = { path = "ic-transport-types", version = "0.49.1" }
36+
ic-utils = { path = "ic-utils", version = "0.49.1" }
3737
ic-utils-bindgen = { path = "ic-utils-bindgen" }
3838
ref-tests = { path = "ref-tests" }
3939

@@ -44,7 +44,7 @@ async-channel = "2.5"
4444
async-lock = "3.4"
4545
async-trait = "0.1"
4646
async-watch = "0.3"
47-
backoff = "0.4.0"
47+
backon = { version = "1", default-features = false }
4848
base64 = "0.22"
4949
bytes = "1.11"
5050
cached = { version = "0.56", default-features = false }
@@ -57,6 +57,11 @@ ed25519-consensus = "2.1.0"
5757
elliptic-curve = "0.13"
5858
futures-util = "0.3.31"
5959
getrandom = "0.4"
60+
# getrandom 0.2 is pulled in only transitively (via rand_core 0.6, used by the
61+
# crypto crates). On wasm it needs its `js` backend enabled; ic-agent turns that
62+
# on through this renamed entry under its `wasm-bindgen` feature. This activation
63+
# was previously provided by `backoff/wasm-bindgen`.
64+
getrandom_0_2 = { package = "getrandom", version = "0.2" }
6065
hex = "0.4.3"
6166
http = "1.0.0"
6267
http-body = "1.0.0"

ic-agent/Cargo.toml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ wasm-bindgen = [
2828
"dep:wasm-bindgen",
2929
"dep:wasm-bindgen-futures",
3030
"dep:getrandom",
31+
"dep:getrandom_0_2",
3132
"dep:web-sys",
3233
"time/wasm-bindgen",
33-
"backoff/wasm-bindgen",
3434
"cached/wasm",
3535
]
3636
_internal_dynamic-routing = []
@@ -41,7 +41,7 @@ arc-swap = { workspace = true }
4141
async-channel = { workspace = true }
4242
async-lock = { workspace = true }
4343
async-trait = { workspace = true }
44-
backoff = { workspace = true }
44+
backon = { workspace = true, features = ["std"] }
4545
bytes = { workspace = true }
4646
cached = { workspace = true, features = ["ahash"], default-features = false }
4747
candid = { workspace = true }
@@ -89,7 +89,16 @@ tokio = { workspace = true, default-features = false, features = ["time", "sync"
8989

9090
[target.'cfg(target_family = "wasm")'.dependencies]
9191
async-watch = { workspace = true }
92+
# getrandom 0.4 is required (on all targets) by `rand` 0.10, which ic-agent uses
93+
# directly (e.g. nonce generation). On native it selects a backend automatically;
94+
# on wasm the `wasm_js` backend must be turned on explicitly, which is the only
95+
# reason for this direct, wasm-only entry. Enabled via the `wasm-bindgen` feature.
9296
getrandom = { workspace = true, optional = true, features = ["wasm_js"] }
97+
# Likewise, getrandom 0.2 is pulled in (on all targets) only transitively, by the
98+
# crypto crates via rand_core 0.6; on wasm its `js` backend must be turned on. This
99+
# renamed entry does that (see the workspace manifest), and was previously provided
100+
# by `backoff/wasm-bindgen`. Enabled via the `wasm-bindgen` feature.
101+
getrandom_0_2 = { workspace = true, optional = true, features = ["js"] }
93102
js-sys = { workspace = true, optional = true }
94103
wasm-bindgen = { workspace = true, optional = true }
95104
wasm-bindgen-futures = { workspace = true, optional = true }

ic-agent/src/agent/mod.rs

Lines changed: 38 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,7 @@ use crate::{
5353
identity::Identity,
5454
to_request_id, RequestId,
5555
};
56-
use backoff::{backoff::Backoff, ExponentialBackoffBuilder};
57-
use backoff::{exponential::ExponentialBackoff, SystemClock};
56+
use backon::{BackoffBuilder, ExponentialBackoff, ExponentialBuilder};
5857
use ic_certification::{Certificate, Delegation, Label};
5958
use ic_transport_types::{
6059
signed::{SignedQuery, SignedRequestStatus, SignedUpdate},
@@ -838,12 +837,17 @@ impl Agent {
838837
})
839838
}
840839

841-
fn get_retry_policy(&self) -> ExponentialBackoff<SystemClock> {
842-
ExponentialBackoffBuilder::new()
843-
.with_initial_interval(Duration::from_millis(500))
844-
.with_max_interval(Duration::from_secs(1))
845-
.with_multiplier(1.4)
846-
.with_max_elapsed_time(Some(self.max_polling_time))
840+
/// Backoff schedule for polling `request_status`: 500ms initial delay, growing by 1.4x up to
841+
/// 1s, jittered. `with_total_delay` bounds the cumulative sleep by `max_polling_time`, so the
842+
/// iterator yields `None` (i.e. "give up") once the budget is spent.
843+
fn poll_backoff(&self) -> ExponentialBackoff {
844+
ExponentialBuilder::default()
845+
.with_min_delay(Duration::from_millis(500))
846+
.with_max_delay(Duration::from_secs(1))
847+
.with_factor(1.4)
848+
.with_jitter()
849+
.without_max_times()
850+
.with_total_delay(Some(self.max_polling_time))
847851
.build()
848852
}
849853

@@ -859,7 +863,8 @@ impl Agent {
859863
signed_request_status: Vec<u8>,
860864
) -> Result<(Vec<u8>, Certificate), AgentError> {
861865
let effective_id = effective_id.into();
862-
let mut retry_policy = self.get_retry_policy();
866+
let mut backoff = self.poll_backoff();
867+
let mut slept = Duration::ZERO;
863868

864869
let mut request_accepted = false;
865870
loop {
@@ -868,15 +873,18 @@ impl Agent {
868873
.await?;
869874
match resp {
870875
RequestStatusResponse::Unknown => {
871-
// If status is still `Unknown` after 5 minutes, the ingress message is lost.
872-
if retry_policy.get_elapsed_time() > Duration::from_secs(5 * 60) {
876+
// If the status is still `Unknown` after 5 minutes of cumulative polling
877+
// backoff, the ingress message is presumed lost. `slept` counts backoff
878+
// sleep, not wall-clock, so this excludes per-poll request latency.
879+
if slept > Duration::from_secs(5 * 60) {
873880
return Err(AgentError::TimeoutWaitingForResponse());
874881
}
875882
}
876883

877884
RequestStatusResponse::Received | RequestStatusResponse::Processing => {
878885
if !request_accepted {
879-
retry_policy.reset();
886+
backoff = self.poll_backoff();
887+
slept = Duration::ZERO;
880888
request_accepted = true;
881889
}
882890
}
@@ -899,9 +907,11 @@ impl Agent {
899907
}
900908
};
901909

902-
match retry_policy.next_backoff() {
903-
Some(duration) => crate::util::sleep(duration).await,
904-
910+
match backoff.next() {
911+
Some(duration) => {
912+
slept += duration;
913+
crate::util::sleep(duration).await;
914+
}
905915
None => return Err(AgentError::TimeoutWaitingForResponse()),
906916
}
907917
}
@@ -926,15 +936,18 @@ impl Agent {
926936
effective_id: EffectiveId,
927937
operation: Option<Operation>,
928938
) -> Result<(Vec<u8>, Certificate), AgentError> {
929-
let mut retry_policy = self.get_retry_policy();
939+
let mut backoff = self.poll_backoff();
940+
let mut slept = Duration::ZERO;
930941

931942
let mut request_accepted = false;
932943
loop {
933944
let (resp, cert) = self.request_status_raw(request_id, effective_id).await?;
934945
match resp {
935946
RequestStatusResponse::Unknown => {
936-
// If status is still `Unknown` after 5 minutes, the ingress message is lost.
937-
if retry_policy.get_elapsed_time() > Duration::from_secs(5 * 60) {
947+
// If the status is still `Unknown` after 5 minutes of cumulative polling
948+
// backoff, the ingress message is presumed lost. `slept` counts backoff
949+
// sleep, not wall-clock, so this excludes per-poll request latency.
950+
if slept > Duration::from_secs(5 * 60) {
938951
return Err(AgentError::TimeoutWaitingForResponse());
939952
}
940953
}
@@ -948,7 +961,8 @@ impl Agent {
948961
// instantaneous. Therefore, once we know the request is accepted,
949962
// we should restart the backoff so the request does not time out.
950963

951-
retry_policy.reset();
964+
backoff = self.poll_backoff();
965+
slept = Duration::ZERO;
952966
request_accepted = true;
953967
}
954968
}
@@ -971,9 +985,11 @@ impl Agent {
971985
}
972986
};
973987

974-
match retry_policy.next_backoff() {
975-
Some(duration) => crate::util::sleep(duration).await,
976-
988+
match backoff.next() {
989+
Some(duration) => {
990+
slept += duration;
991+
crate::util::sleep(duration).await;
992+
}
977993
None => return Err(AgentError::TimeoutWaitingForResponse()),
978994
}
979995
}

0 commit comments

Comments
 (0)