livechat: adopt universal clippy/lint baseline; warnings to zero
Renamed the misleadingly-underscored _permit field to permit (used_underscore_binding), by-value self on the Copy RateLimits method (trivially_copy_pass_by_ref), bound the error in a test panic (match_wild_err_arm).
clippy --all-targets clean, cargo fmt clean, tests green (57 passed+3 passed+2 passed+0 passed).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
5 files changed,
+49 insertions,
-19 deletions
| 26 |
26 |
|
[dev-dependencies]
|
| 27 |
27 |
|
serde_json = "1"
|
| 28 |
28 |
|
tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "sync", "time"] }
|
|
29 |
+ |
|
|
30 |
+ |
[lints.rust]
|
|
31 |
+ |
unused = "warn"
|
|
32 |
+ |
unreachable_pub = "warn"
|
|
33 |
+ |
|
|
34 |
+ |
[lints.clippy]
|
|
35 |
+ |
pedantic = { level = "warn", priority = -1 }
|
|
36 |
+ |
# Allow-list tuned from a measured breakdown across server/multithreaded/pter
|
|
37 |
+ |
# (2026-07-22). These are the high-churn / low-signal pedantic lints; everything
|
|
38 |
+ |
# else in `pedantic` stays a warning. Keep this block identical across repos.
|
|
39 |
+ |
module_name_repetitions = "allow"
|
|
40 |
+ |
# Doc lints — no docs-completeness push underway.
|
|
41 |
+ |
missing_errors_doc = "allow"
|
|
42 |
+ |
missing_panics_doc = "allow"
|
|
43 |
+ |
doc_markdown = "allow"
|
|
44 |
+ |
# Numeric casts — endemic and mostly intentional in size/byte math.
|
|
45 |
+ |
cast_possible_truncation = "allow"
|
|
46 |
+ |
cast_sign_loss = "allow"
|
|
47 |
+ |
cast_precision_loss = "allow"
|
|
48 |
+ |
cast_possible_wrap = "allow"
|
|
49 |
+ |
cast_lossless = "allow"
|
|
50 |
+ |
# Subjective structure/style nags — high churn, low signal.
|
|
51 |
+ |
must_use_candidate = "allow"
|
|
52 |
+ |
too_many_lines = "allow"
|
|
53 |
+ |
struct_excessive_bools = "allow"
|
|
54 |
+ |
similar_names = "allow"
|
|
55 |
+ |
items_after_statements = "allow"
|
|
56 |
+ |
single_match_else = "allow"
|
|
57 |
+ |
# Frequent false-positives in TUI/router-heavy code — added from the buckets breakdown.
|
|
58 |
+ |
match_same_arms = "allow"
|
|
59 |
+ |
unnecessary_wraps = "allow"
|
|
60 |
+ |
type_complexity = "allow"
|
| 129 |
129 |
|
.or_insert_with(|| broadcast::channel(self.inner.limits.room_buffer).0)
|
| 130 |
130 |
|
.subscribe();
|
| 131 |
131 |
|
|
| 132 |
|
- |
Ok(Subscription {
|
| 133 |
|
- |
rx,
|
| 134 |
|
- |
room,
|
| 135 |
|
- |
_permit: permit,
|
| 136 |
|
- |
})
|
|
132 |
+ |
Ok(Subscription { rx, room, permit })
|
| 137 |
133 |
|
}
|
| 138 |
134 |
|
|
| 139 |
135 |
|
/// Rooms currently holding a channel. Diagnostics only.
|
| 152 |
148 |
|
self.inner
|
| 153 |
149 |
|
.rooms
|
| 154 |
150 |
|
.get(&room)
|
| 155 |
|
- |
.map(|tx| tx.receiver_count())
|
| 156 |
|
- |
.unwrap_or(0)
|
|
151 |
+ |
.map_or(0, |tx| tx.receiver_count())
|
| 157 |
152 |
|
}
|
| 158 |
153 |
|
}
|
| 159 |
154 |
|
|
| 223 |
218 |
|
pub struct Subscription {
|
| 224 |
219 |
|
rx: broadcast::Receiver<ChatEvent>,
|
| 225 |
220 |
|
room: RoomId,
|
| 226 |
|
- |
_permit: ConnectionPermit,
|
|
221 |
+ |
permit: ConnectionPermit,
|
| 227 |
222 |
|
}
|
| 228 |
223 |
|
|
| 229 |
224 |
|
impl Subscription {
|
| 255 |
250 |
|
// this instant either keeps this channel or creates a fresh one after the
|
| 256 |
251 |
|
// removal. It cannot end up holding a receiver on a sender that has just
|
| 257 |
252 |
|
// been dropped from the map, which is the silent-dead-room failure.
|
| 258 |
|
- |
self._permit
|
|
253 |
+ |
self.permit
|
| 259 |
254 |
|
.inner
|
| 260 |
255 |
|
.rooms
|
| 261 |
256 |
|
.remove_if(&self.room, |_, tx| tx.receiver_count() <= 1);
|
| 48 |
48 |
|
}
|
| 49 |
49 |
|
|
| 50 |
50 |
|
impl RateLimits {
|
| 51 |
|
- |
fn refill_per_sec(&self) -> f64 {
|
|
51 |
+ |
fn refill_per_sec(self) -> f64 {
|
| 52 |
52 |
|
f64::from(self.sustain_per_min) / 60.0
|
| 53 |
53 |
|
}
|
| 54 |
54 |
|
}
|
| 220 |
220 |
|
let (u, r, t0) = (user(), room(), Instant::now());
|
| 221 |
221 |
|
|
| 222 |
222 |
|
assert!(rl.check(u, r, t0).is_allowed());
|
| 223 |
|
- |
let long_later = t0 + Duration::from_secs(3600);
|
|
223 |
+ |
let long_later = t0 + Duration::from_hours(1);
|
| 224 |
224 |
|
|
| 225 |
225 |
|
for i in 0..3 {
|
| 226 |
226 |
|
assert!(rl.check(u, r, long_later).is_allowed(), "message {i}");
|
| 271 |
271 |
|
assert!(rl.check(u, r, t).is_allowed());
|
| 272 |
272 |
|
// Earlier than the bucket's last touch: elapsed saturates to zero rather
|
| 273 |
273 |
|
// than underflowing.
|
| 274 |
|
- |
assert!(rl.check(u, r, t - Duration::from_secs(5)).is_allowed());
|
|
274 |
+ |
assert!(
|
|
275 |
+ |
rl.check(u, r, t.checked_sub(Duration::from_secs(5)).unwrap())
|
|
276 |
+ |
.is_allowed()
|
|
277 |
+ |
);
|
| 275 |
278 |
|
}
|
| 276 |
279 |
|
|
| 277 |
280 |
|
#[test]
|
| 285 |
288 |
|
rl.check(idle, r, t0);
|
| 286 |
289 |
|
assert_eq!(rl.bucket_count(), 2);
|
| 287 |
290 |
|
|
| 288 |
|
- |
let later = t0 + Duration::from_secs(60);
|
|
291 |
+ |
let later = t0 + Duration::from_mins(1);
|
| 289 |
292 |
|
rl.check(busy, r, later);
|
| 290 |
293 |
|
|
| 291 |
294 |
|
assert_eq!(rl.evict_idle(later, Duration::from_secs(30)), 1);
|
| 15 |
15 |
|
use crate::error::ChatError;
|
| 16 |
16 |
|
|
| 17 |
17 |
|
/// Longest retention any room may request.
|
| 18 |
|
- |
pub const MAX_AGE_CEILING: Duration = Duration::from_secs(30 * 24 * 60 * 60);
|
|
18 |
+ |
pub const MAX_AGE_CEILING: Duration = Duration::from_hours(720);
|
| 19 |
19 |
|
|
| 20 |
20 |
|
/// Most messages any room may hold.
|
| 21 |
21 |
|
pub const MAX_MESSAGES_CEILING: usize = 20_000;
|
| 61 |
61 |
|
/// messages.
|
| 62 |
62 |
|
pub fn forum_default() -> Self {
|
| 63 |
63 |
|
Self {
|
| 64 |
|
- |
max_age: Duration::from_secs(7 * 24 * 60 * 60),
|
|
64 |
+ |
max_age: Duration::from_hours(168),
|
| 65 |
65 |
|
max_messages: 5_000,
|
| 66 |
66 |
|
}
|
| 67 |
67 |
|
}
|
| 70 |
70 |
|
/// messages. A creator may raise it, still bounded by the ceilings.
|
| 71 |
71 |
|
pub fn stream_default() -> Self {
|
| 72 |
72 |
|
Self {
|
| 73 |
|
- |
max_age: Duration::from_secs(24 * 60 * 60),
|
|
73 |
+ |
max_age: Duration::from_hours(24),
|
| 74 |
74 |
|
max_messages: 5_000,
|
| 75 |
75 |
|
}
|
| 76 |
76 |
|
}
|
| 105 |
105 |
|
Err(ChatError::RetentionTooLong { .. })
|
| 106 |
106 |
|
));
|
| 107 |
107 |
|
assert!(matches!(
|
| 108 |
|
- |
Retention::new(Duration::from_secs(60), MAX_MESSAGES_CEILING + 1),
|
|
108 |
+ |
Retention::new(Duration::from_mins(1), MAX_MESSAGES_CEILING + 1),
|
| 109 |
109 |
|
Err(ChatError::RetentionTooLong { .. })
|
| 110 |
110 |
|
));
|
| 111 |
111 |
|
}
|
| 122 |
122 |
|
Err(ChatError::RetentionTooShort)
|
| 123 |
123 |
|
));
|
| 124 |
124 |
|
assert!(matches!(
|
| 125 |
|
- |
Retention::new(Duration::from_secs(60), 0),
|
|
125 |
+ |
Retention::new(Duration::from_mins(1), 0),
|
| 126 |
126 |
|
Err(ChatError::RetentionTooShort)
|
| 127 |
127 |
|
));
|
| 128 |
128 |
|
}
|
| 169 |
169 |
|
Ok(Some(ChatEvent::Gap)) => panic!("buffer was sized to make a gap impossible here"),
|
| 170 |
170 |
|
Ok(Some(_)) => received += 1,
|
| 171 |
171 |
|
Ok(None) => panic!("stable listener's room was closed out from under it"),
|
| 172 |
|
- |
Err(_) => panic!("stable listener stopped receiving after {received} events"),
|
|
172 |
+ |
Err(e) => panic!("stable listener stopped receiving after {received} events: {e:?}"),
|
| 173 |
173 |
|
}
|
| 174 |
174 |
|
}
|
| 175 |
175 |
|
|