Skip to content

Commit 137a7c6

Browse files
tradatiouswboayue
andauthored
fix(transport): clamp FibonacciBackoff state at max to avoid u64 overflow (#770)
* fix(transport): clamp FibonacciBackoff state at max to avoid u64 overflow `next_delay()` capped only the returned delay; the internal state kept growing as a raw Fibonacci sequence, which overflows u64 at call 93 (fib(94)). A reconnect loop with a large `max_reconnect_attempts` or `reconnect_forever` reaches that during a long outage. With the 30 s `max` both call sites use, about 45 minutes of continuous outage. In debug builds `next_delay()` would panic with `attempt to add with overflow`, and in release the addition would wrap and the delays would become nonsense. * fix(transport): saturate backoff sum and honor max=0 - saturating_add covers max values above fib(93), where the raw sum overflows before the clamp engages - clamp current at construction so max=0 keeps meaning no delay instead of a fixed 1s --------- Co-authored-by: Wil Boayue <wil.boayue@gmail.com>
1 parent 874aa3e commit 137a7c6

3 files changed

Lines changed: 48 additions & 9 deletions

File tree

CHANGELOG.md

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

4747
### Fixed
4848

49+
- `FibonacciBackoff` no longer overflows `u64` on the 93rd consecutive `next_delay()` call. The backoff state kept growing past `max` (only the returned delay was capped), so a reconnect loop with a large `max_reconnect_attempts` or `reconnect_forever` would panic with `attempt to add with overflow` in debug builds, or wrap to nonsense delays in release, once an outage outlasted ~92 attempts. The state is now clamped at `max`. Returned delays are unchanged.
50+
4951
- Notice 2188 ("Up-to-the-second historical data requires additional subscription for the API.") is classified as a data advisory instead of a hard error. TWS sends it and then delivers the historical bars anyway — the account merely lacks the up-to-the-second tail — but `historical_data()` returned `Err` on the notice and discarded the bars that followed, so accounts without the real-time entitlement for a listing got no history at all for those symbols. `DATA_ADVISORY_CODES` widens from `[i32; 2]` to `[i32; 3]`, which is breaking only for code binding the const with an explicit array type (#765).
5052

5153
- Error 10090 ("Part of requested market data is not subscribed. Subscription-independent ticks are still active") is classified as a data advisory like 10089 and 10167: it is published as a non-terminal notice instead of ending the market-data subscription. TWS sends it on partial entitlement — commonly an options subscription without the underlying — and keeps delivering the ticks the account is entitled to, but the subscription was torn down before they could arrive. `DATA_ADVISORY_CODES` widens again to `[i32; 4]` (#768).

src/transport/common.rs

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -133,21 +133,23 @@ impl FibonacciBackoff {
133133
pub(crate) fn new(max: u64) -> Self {
134134
FibonacciBackoff {
135135
previous: 0,
136-
current: 1,
136+
// Clamped so `current <= max` holds from construction on; keeps
137+
// `max: 0` meaning "no delay" rather than a fixed 1s.
138+
current: 1.min(max),
137139
max,
138140
}
139141
}
140142

141143
pub(crate) fn next_delay(&mut self) -> Duration {
142-
let next = self.previous + self.current;
143-
self.previous = self.current;
144-
self.current = next;
145-
146-
if next > self.max {
147-
Duration::from_secs(self.max)
148-
} else {
149-
Duration::from_secs(next)
144+
// Note: `max` must clamp `previous` and `current` (not just the return value)
145+
// because u64 overflows at fib(94). The saturating_add covers `max` values
146+
// large enough that the sum overflows before the clamp can engage.
147+
if self.current < self.max {
148+
let next = self.previous.saturating_add(self.current).min(self.max);
149+
self.previous = self.current;
150+
self.current = next;
150151
}
152+
Duration::from_secs(self.current)
151153
}
152154
}
153155

src/transport/common_tests.rs

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,3 +137,38 @@ fn test_fibonacci_backoff() {
137137
assert_eq!(backoff.next_delay(), Duration::from_secs(10)); // capped at max
138138
assert_eq!(backoff.next_delay(), Duration::from_secs(10)); // stays at max
139139
}
140+
141+
/// The raw Fibonacci sequence overflows u64 after ~93 steps, which can be
142+
/// reached during a long outage with a large `max_reconnect_attempts`. The
143+
/// internal steps must be clamped at `max` to avoid u64 overflow regardless of
144+
/// the number of calls. Without that, this test will fail in debug builds with
145+
/// `attempt to add with overflow` at call 93.
146+
#[test]
147+
fn test_fibonacci_backoff_never_overflows() {
148+
let mut backoff = FibonacciBackoff::new(30);
149+
for _ in 0..100 {
150+
assert!(backoff.next_delay() <= Duration::from_secs(30));
151+
}
152+
assert_eq!(backoff.next_delay(), Duration::from_secs(30));
153+
}
154+
155+
/// A `max` above fib(93) lets the raw sum overflow before the clamp can
156+
/// engage; `saturating_add` must cover that. Without it, call ~93 panics with
157+
/// `attempt to add with overflow` in debug builds.
158+
#[test]
159+
fn test_fibonacci_backoff_never_overflows_with_huge_max() {
160+
let mut backoff = FibonacciBackoff::new(u64::MAX);
161+
for _ in 0..100 {
162+
backoff.next_delay();
163+
}
164+
assert_eq!(backoff.next_delay(), Duration::from_secs(u64::MAX));
165+
}
166+
167+
/// `max: 0` means no delay, not a fixed 1s: `current` starts clamped at
168+
/// `max`, so the delay must respect `max` from the first call.
169+
#[test]
170+
fn test_fibonacci_backoff_zero_max() {
171+
let mut backoff = FibonacciBackoff::new(0);
172+
assert_eq!(backoff.next_delay(), Duration::ZERO);
173+
assert_eq!(backoff.next_delay(), Duration::ZERO);
174+
}

0 commit comments

Comments
 (0)