1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
//! Saturating time / duration arithmetic for request-path modules.
//!
//! `std` and `chrono` both expose *panicking* operators for the everyday
//! "deadline = now + ttl" shape:
//!
//! * `Instant + Duration` panics when the sum is not representable by the
//! platform clock (`Duration::MAX`, `Duration::from_secs(u64::MAX)`, …).
//! * `DateTime<Utc> + TimeDelta` panics via `expect("`DateTime + TimeDelta`
//! overflowed")`.
//! * `TimeDelta::seconds` panics above `i64::MAX / 1_000` seconds.
//!
//! Every one of those inputs is routinely *configuration* (a TTL, a retry
//! window, a poll interval), which means a bad config value crashes a worker
//! or a request thread instead of degrading. The helpers here clamp to a
//! far-future horizon instead, so a pathological TTL yields an effectively
//! non-expiring deadline — the safe direction for expiry, uniqueness holds
//! and maintenance throttles alike.
//!
//! Crate-internal on purpose: these are the framework's own guard rails, not
//! a public time API.
// autumn-panic-gate: request-path module — production code path must be panic-free.
// See CONTRIBUTING.md "Request-path panic gate". Justify exceptions with
// #[allow(clippy::<lint>, reason = "…")] at the narrowest scope.
#![cfg_attr(
not(test),
deny(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::unreachable,
clippy::todo,
clippy::unimplemented,
clippy::indexing_slicing,
clippy::string_slice,
clippy::arithmetic_side_effects,
)
)]
use std::time::{Duration, Instant};
use chrono::{DateTime, TimeDelta, Utc};
/// Clamp horizon (~10 years, `10 * 365 * 24 * 3600`) used when a
/// caller-supplied TTL would overflow `Instant + Duration`. This constant is
/// itself always representable when added to a fresh `Instant`, so it can
/// never re-trigger the overflow.
pub const SATURATING_DEADLINE_HORIZON_SECS: u64 = 315_360_000;
/// Compute an expiry `Instant` for `ttl`, saturating instead of panicking on
/// overflow.
///
/// `now + ttl` panics when the sum is not representable by the platform
/// clock — a pathological `ttl` such as `Duration::MAX` or
/// `Duration::from_secs(u64::MAX)` (entirely config-influenceable) triggers
/// this. Instead of panicking we clamp the deadline to ~10 years out (far
/// enough that the entry is effectively non-expiring), falling back to `now`
/// only in the astronomically unlikely event that even the clamped horizon is
/// not representable.
pub fn saturating_deadline(now: Instant, ttl: Duration) -> Instant {
now.checked_add(ttl).unwrap_or_else(|| {
now.checked_add(Duration::from_secs(SATURATING_DEADLINE_HORIZON_SECS))
.unwrap_or(now)
})
}
/// Compute an expiry [`tokio::time::Instant`] for `ttl`, saturating instead of
/// panicking on overflow.
///
/// The tokio-timeline twin of [`saturating_deadline`], with the identical clamp
/// policy. It exists because a deadline whose counterparty is
/// `tokio::time::sleep` must live on tokio's timeline (and is virtualized for
/// free under a paused runtime), while `tokio::time::Instant` is a distinct type
/// from `std::time::Instant` and cannot reuse the same function.
#[must_use]
// Currently reached only from the `redis` job worker's maintenance throttle, so
// a default-feature build has no caller. Kept ungated (and not `#[cfg(feature =
// "redis")]`) because it is a general helper: the moment any other tokio-timeline
// deadline needs clamping it should reach for this, not re-derive the policy.
#[cfg_attr(not(feature = "redis"), allow(dead_code))]
pub fn saturating_tokio_deadline(now: tokio::time::Instant, ttl: Duration) -> tokio::time::Instant {
now.checked_add(ttl).unwrap_or_else(|| {
now.checked_add(Duration::from_secs(SATURATING_DEADLINE_HORIZON_SECS))
.unwrap_or(now)
})
}
/// Add `delta` to `now`, clamping to chrono's representable range instead of
/// panicking.
///
/// `DateTime<Utc> + TimeDelta` is `checked_add_signed(..).expect(..)`. When
/// the sum leaves `[DateTime::MIN_UTC, DateTime::MAX_UTC]` we saturate *in the
/// direction of the delta's sign*, so a huge positive TTL becomes "never
/// expires" and a huge negative offset becomes "the beginning of time" —
/// both of which preserve the comparison the caller was about to make.
pub fn saturating_dt_add(now: DateTime<Utc>, delta: TimeDelta) -> DateTime<Utc> {
now.checked_add_signed(delta).unwrap_or_else(|| {
if delta < TimeDelta::zero() {
DateTime::<Utc>::MIN_UTC
} else {
DateTime::<Utc>::MAX_UTC
}
})
}
/// Build a [`TimeDelta`] from a `u64` second count, clamping to
/// [`TimeDelta::MAX`] instead of panicking.
///
/// `TimeDelta::seconds` panics above `i64::MAX / 1_000` seconds, so the
/// common `TimeDelta::seconds(i64::try_from(ttl_secs).unwrap_or(i64::MAX))`
/// idiom panics *precisely* on the saturating branch it was written to
/// protect. Pair this with [`saturating_dt_add`] at the addition site.
pub fn saturating_time_delta_secs(secs: u64) -> TimeDelta {
i64::try_from(secs)
.ok()
.and_then(TimeDelta::try_seconds)
.unwrap_or(TimeDelta::MAX)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn saturating_deadline_is_exact_for_ordinary_ttls() {
let now = Instant::now();
assert_eq!(
saturating_deadline(now, Duration::from_secs(60)),
now + Duration::from_secs(60)
);
assert_eq!(saturating_deadline(now, Duration::ZERO), now);
}
#[test]
fn saturating_tokio_deadline_matches_the_std_clamp_policy() {
// The tokio-timeline twin must behave identically: exact for ordinary
// TTLs, clamped (never panicking) for a pathological one. The redis
// maintenance throttle derives its interval from app config, so an
// absurd value must make the sweep one-shot rather than crash the
// worker (issue #1611's regression, now on tokio's timeline).
let now = tokio::time::Instant::now();
assert_eq!(
saturating_tokio_deadline(now, Duration::from_secs(60)),
now + Duration::from_secs(60)
);
assert_eq!(saturating_tokio_deadline(now, Duration::ZERO), now);
for extreme in [Duration::MAX, Duration::from_secs(u64::MAX)] {
let deadline = saturating_tokio_deadline(now, extreme);
assert!(
deadline > now + Duration::from_secs(3_600),
"a clamped deadline must still be far in the future"
);
}
}
#[test]
fn saturating_deadline_clamps_instead_of_panicking() {
let now = Instant::now();
for extreme in [Duration::MAX, Duration::from_secs(u64::MAX)] {
let deadline = saturating_deadline(now, extreme);
assert!(
deadline > now,
"a clamped deadline must still be in the future"
);
}
}
#[test]
fn saturating_dt_add_is_exact_for_ordinary_deltas() {
let now = DateTime::<Utc>::UNIX_EPOCH;
assert_eq!(
saturating_dt_add(now, TimeDelta::seconds(86_400)),
now + TimeDelta::seconds(86_400)
);
assert_eq!(
saturating_dt_add(now, TimeDelta::seconds(-86_400)),
now - TimeDelta::seconds(86_400)
);
}
#[test]
fn saturating_dt_add_clamps_in_the_direction_of_the_delta() {
let now = DateTime::<Utc>::UNIX_EPOCH;
assert_eq!(
saturating_dt_add(now, TimeDelta::MAX),
DateTime::<Utc>::MAX_UTC
);
assert_eq!(
saturating_dt_add(now, TimeDelta::MIN),
DateTime::<Utc>::MIN_UTC
);
assert_eq!(
saturating_dt_add(DateTime::<Utc>::MAX_UTC, TimeDelta::seconds(1)),
DateTime::<Utc>::MAX_UTC
);
assert_eq!(
saturating_dt_add(DateTime::<Utc>::MIN_UTC, TimeDelta::seconds(-1)),
DateTime::<Utc>::MIN_UTC
);
}
#[test]
fn saturating_time_delta_secs_is_exact_below_the_ceiling() {
assert_eq!(saturating_time_delta_secs(0), TimeDelta::zero());
assert_eq!(
saturating_time_delta_secs(86_400),
TimeDelta::seconds(86_400)
);
// The largest exactly-representable second count.
let ceiling = i64::MAX / 1_000;
assert_eq!(
saturating_time_delta_secs(
u64::try_from(ceiling).expect("the ceiling is a positive i64")
),
TimeDelta::seconds(ceiling)
);
}
#[test]
fn saturating_time_delta_secs_clamps_instead_of_panicking() {
for extreme in [
u64::MAX,
u64::try_from(i64::MAX).expect("i64::MAX fits in u64"),
// One second past the `i64::MAX / 1_000` ceiling.
u64::try_from(i64::MAX / 1_000).expect("positive") + 1,
] {
assert_eq!(saturating_time_delta_secs(extreme), TimeDelta::MAX);
}
}
}