Skip to content

Commit 18f8e05

Browse files
committed
upkeep(clippy): clean up 100+ clippy lints
I used ``` cargo clippy -- -W clippy::pedantic -W clippy::nursery -W clippy::unwrap_used -A clippy::cast_precision_loss -A clippy::cast_possible_truncation -A clippy::too_many_lines -A clippy::missing_fields_in_debug ``` to reduce the amount of warnings from 201 to 24, and clippy suggestions from 151 to 1. I did this to re-familiarize myself with this code-base, I want to give it another try, our kafka clusters are on the orders of magnitude: - 5+ partitions for biggest topics - 1.5k topics - ~1-3k consumergroups (I am not sure if I'm counting double the ACLs or not, since I know we keep some for rotation purposes per consumer) Thus, when I last tried in february, even after a lot of work on the Rust base (code @ https://github.com/nais/klag-exporter/tree/deploy_our_own_helmchart), we could NOT manage to get `klag-exporter` to scrape lag metrics for an entire cluster reliably within ~20-40 seconds without memory leak. Looking at the code now, I suspect you may have dealt with the librdkafka memory leak I struggled with back then.
1 parent 60c62b6 commit 18f8e05

16 files changed

Lines changed: 318 additions & 252 deletions

File tree

src/cluster/manager.rs

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ use std::time::{Duration, Instant};
1313
use tokio::sync::broadcast;
1414
use tracing::{debug, error, info, instrument, warn};
1515

16-
/// Default timeout for a single collection cycle (should be less than poll_interval)
17-
const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_secs(60);
16+
/// Default timeout for a single collection cycle (should be less than `poll_interval`)
17+
const DEFAULT_COLLECTION_TIMEOUT: Duration = Duration::from_mins(1);
1818

1919
pub struct ClusterManager {
2020
cluster_name: String,
@@ -34,7 +34,7 @@ pub struct ClusterManager {
3434

3535
impl ClusterManager {
3636
pub fn new(
37-
config: ClusterConfig,
37+
config: &ClusterConfig,
3838
registry: Arc<MetricsRegistry>,
3939
exporter_config: &ExporterConfig,
4040
) -> Result<Self> {
@@ -43,11 +43,11 @@ impl ClusterManager {
4343
let filters = config.compile_filters()?;
4444
let performance = exporter_config.performance.clone();
4545

46-
let client = Arc::new(KafkaClient::with_performance(&config, performance.clone())?);
46+
let client = Arc::new(KafkaClient::with_performance(config, performance.clone())?);
4747
let offset_collector = OffsetCollector::with_performance(
4848
Arc::clone(&client),
4949
filters,
50-
performance.clone(),
50+
performance,
5151
exporter_config.granularity,
5252
);
5353

@@ -59,7 +59,7 @@ impl ClusterManager {
5959
// the Tier-3 resident-memory saving for rate-mode users:
6060
// no BaseConsumer pool, no extra librdkafka clients.
6161
let ts_consumer =
62-
TimestampConsumer::with_pool_size(&config, ts_cfg.max_concurrent_fetches)?;
62+
TimestampConsumer::with_pool_size(config, ts_cfg.max_concurrent_fetches)?;
6363
TimestampSampler::new_message(ts_consumer, ts_cfg.cache_ttl)
6464
}
6565
crate::config::TimestampSamplingMode::Rate => {
@@ -101,7 +101,7 @@ impl ClusterManager {
101101
timestamp_sampler,
102102
registry,
103103
poll_interval: exporter_config.poll_interval,
104-
max_backoff: Duration::from_secs(300),
104+
max_backoff: Duration::from_mins(5),
105105
granularity: exporter_config.granularity,
106106
max_concurrent_fetches: exporter_config.timestamp_sampling.max_concurrent_fetches,
107107
cache_cleanup_interval: exporter_config.timestamp_sampling.cache_ttl * 2,
@@ -308,7 +308,7 @@ impl ClusterManager {
308308
// Update registry with granularity and custom labels
309309
self.registry.update_with_options(
310310
&self.cluster_name,
311-
lag_metrics,
311+
&lag_metrics,
312312
self.granularity,
313313
&self.cluster_labels,
314314
);
@@ -320,7 +320,7 @@ impl ClusterManager {
320320
debug!(
321321
cluster = %self.cluster_name,
322322
elapsed_ms = scrape_duration_ms,
323-
timestamp_cache_size = self.timestamp_sampler.as_ref().map(|s| s.cache_size()).unwrap_or(0),
323+
timestamp_cache_size = self.timestamp_sampler.as_ref().map_or(0, super::super::collector::timestamp_sampler::TimestampSampler::cache_size),
324324
"Collection cycle completed"
325325
);
326326

src/collector/lag_calculator.rs

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,11 +31,11 @@ pub struct PartitionLagMetric {
3131
pub lag_seconds: Option<f64>,
3232
/// Whether compaction was detected for this partition's timestamp fetch
3333
pub compaction_detected: bool,
34-
/// Whether data loss occurred (committed_offset < low_watermark)
34+
/// Whether data loss occurred (`committed_offset` < `low_watermark`)
3535
pub data_loss_detected: bool,
36-
/// Number of messages lost to retention (low_watermark - committed_offset when positive)
36+
/// Number of messages lost to retention (`low_watermark` - `committed_offset` when positive)
3737
pub messages_lost: i64,
38-
/// Offset distance to deletion boundary (committed_offset - low_watermark)
38+
/// Offset distance to deletion boundary (`committed_offset` - `low_watermark`)
3939
pub retention_margin: i64,
4040
/// Percentage of retention window occupied by lag (0=caught up, 100=at boundary, >100=data loss)
4141
pub lag_retention_ratio: f64,
@@ -72,10 +72,10 @@ pub struct LagCalculator;
7272

7373
/// Encode consumer group state as an integer.
7474
///
75-
/// The string values come from Kafka's DescribeGroups response and match
75+
/// The string values come from Kafka's `DescribeGroups` response and match
7676
/// `org.apache.kafka.common.ConsumerGroupState` exactly.
7777
///
78-
/// Classic protocol states: PreparingRebalance, CompletingRebalance, Stable, Dead, Empty
78+
/// Classic protocol states: `PreparingRebalance`, `CompletingRebalance`, Stable, Dead, Empty
7979
/// KIP-848 protocol states: Assigning, Reconciling, Stable, Dead, Empty
8080
///
8181
/// Mapping:
@@ -158,16 +158,16 @@ impl LagCalculator {
158158
};
159159

160160
// Look up member info for this partition
161-
let (member_host, consumer_id, client_id) = member_map
162-
.get(tp)
163-
.map(|m| {
161+
let (member_host, consumer_id, client_id) = member_map.get(tp).map_or_else(
162+
|| (String::new(), String::new(), String::new()),
163+
|m| {
164164
(
165165
m.client_host.to_string(),
166166
m.member_id.to_string(),
167167
m.client_id.to_string(),
168168
)
169-
})
170-
.unwrap_or_else(|| (String::new(), String::new(), String::new()));
169+
},
170+
);
171171

172172
// Calculate time lag if timestamp available
173173
let ts_data = timestamps.get(&(group.group_id.clone(), tp.clone()));

src/collector/offset_collector.rs

Lines changed: 55 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use tracing::{debug, instrument, warn};
1313
/// caching it with a long TTL saves a per-cycle `DescribeConfigs` over the
1414
/// monitored topic set. On each cycle the collector asks the cache which
1515
/// topics still need to be queried fresh, then merges the cached-true
16-
/// entries with whatever DescribeConfigs returns.
16+
/// entries with whatever `DescribeConfigs` returns.
1717
struct CompactedTopicsCache {
1818
ttl: Duration,
1919
// (is_compacted, fetched_at). Mutex is fine — accessed only from
@@ -31,10 +31,13 @@ impl CompactedTopicsCache {
3131

3232
/// Split `monitored_topics` into:
3333
/// - `cached_compacted` — topics the cache says are compacted (fresh)
34-
/// - `to_fetch` — topics with no fresh cache entry (need DescribeConfigs)
34+
/// - `to_fetch` — topics with no fresh cache entry (need `DescribeConfigs`)
3535
fn partition<'a>(&self, monitored_topics: &'a [String]) -> (HashSet<String>, Vec<&'a str>) {
3636
let now = Instant::now();
37-
let entries = self.entries.lock().unwrap_or_else(|p| p.into_inner());
37+
let entries = self
38+
.entries
39+
.lock()
40+
.unwrap_or_else(std::sync::PoisonError::into_inner);
3841

3942
let mut cached_compacted = HashSet::new();
4043
let mut to_fetch: Vec<&str> = Vec::new();
@@ -55,7 +58,10 @@ impl CompactedTopicsCache {
5558
/// in `fetched_topics`, record whether it appeared in `compacted_result`.
5659
fn update(&self, fetched_topics: &[&str], compacted_result: &HashSet<String>) {
5760
let now = Instant::now();
58-
let mut entries = self.entries.lock().unwrap_or_else(|p| p.into_inner());
61+
let mut entries = self
62+
.entries
63+
.lock()
64+
.unwrap_or_else(std::sync::PoisonError::into_inner);
5965
for topic in fetched_topics {
6066
let is_compacted = compacted_result.contains(*topic);
6167
entries.insert((*topic).to_string(), (is_compacted, now));
@@ -64,8 +70,14 @@ impl CompactedTopicsCache {
6470

6571
/// Drop cache entries for topics no longer being monitored.
6672
fn prune_to(&self, monitored_topics: &[String]) {
67-
let keep: HashSet<&str> = monitored_topics.iter().map(|s| s.as_str()).collect();
68-
let mut entries = self.entries.lock().unwrap_or_else(|p| p.into_inner());
73+
let keep: HashSet<&str> = monitored_topics
74+
.iter()
75+
.map(std::string::String::as_str)
76+
.collect();
77+
let mut entries = self
78+
.entries
79+
.lock()
80+
.unwrap_or_else(std::sync::PoisonError::into_inner);
6981
entries.retain(|k, _| keep.contains(k.as_str()));
7082
}
7183
}
@@ -84,7 +96,7 @@ struct MetadataCache {
8496
}
8597

8698
impl MetadataCache {
87-
fn new(ttl: Duration) -> Self {
99+
const fn new(ttl: Duration) -> Self {
88100
Self {
89101
ttl,
90102
entry: Mutex::new(None),
@@ -95,7 +107,10 @@ impl MetadataCache {
95107
if self.ttl.is_zero() {
96108
return None;
97109
}
98-
let guard = self.entry.lock().unwrap_or_else(|p| p.into_inner());
110+
let guard = self
111+
.entry
112+
.lock()
113+
.unwrap_or_else(std::sync::PoisonError::into_inner);
99114
let (set, at) = guard.as_ref()?;
100115
if at.elapsed() < self.ttl {
101116
Some((Arc::clone(&set.0), Arc::clone(&set.1)))
@@ -111,7 +126,10 @@ impl MetadataCache {
111126
fn set(&self, partitions: Vec<TopicPartition>, topics: Vec<String>) -> MonitoredSet {
112127
let set: MonitoredSet = (Arc::new(partitions), Arc::new(topics));
113128
if !self.ttl.is_zero() {
114-
let mut guard = self.entry.lock().unwrap_or_else(|p| p.into_inner());
129+
let mut guard = self
130+
.entry
131+
.lock()
132+
.unwrap_or_else(std::sync::PoisonError::into_inner);
115133
*guard = Some(((Arc::clone(&set.0), Arc::clone(&set.1)), Instant::now()));
116134
}
117135
set
@@ -129,7 +147,7 @@ struct ConsumerGroupsCache {
129147
}
130148

131149
impl ConsumerGroupsCache {
132-
fn new(ttl: Duration) -> Self {
150+
const fn new(ttl: Duration) -> Self {
133151
Self {
134152
ttl,
135153
entry: Mutex::new(None),
@@ -140,7 +158,10 @@ impl ConsumerGroupsCache {
140158
if self.ttl.is_zero() {
141159
return None;
142160
}
143-
let guard = self.entry.lock().unwrap_or_else(|p| p.into_inner());
161+
let guard = self
162+
.entry
163+
.lock()
164+
.unwrap_or_else(std::sync::PoisonError::into_inner);
144165
let (groups, at) = guard.as_ref()?;
145166
if at.elapsed() < self.ttl {
146167
Some(Arc::clone(groups))
@@ -152,7 +173,10 @@ impl ConsumerGroupsCache {
152173
fn set(&self, groups: Vec<ConsumerGroupInfo>) -> Arc<Vec<ConsumerGroupInfo>> {
153174
let arc = Arc::new(groups);
154175
if !self.ttl.is_zero() {
155-
let mut guard = self.entry.lock().unwrap_or_else(|p| p.into_inner());
176+
let mut guard = self
177+
.entry
178+
.lock()
179+
.unwrap_or_else(std::sync::PoisonError::into_inner);
156180
*guard = Some((Arc::clone(&arc), Instant::now()));
157181
}
158182
arc
@@ -190,7 +214,7 @@ pub struct OffsetsSnapshot {
190214
/// Topics configured with `cleanup.policy=compact`. Populated by
191215
/// `collect_parallel` for the monitored topic set. Used by the lag
192216
/// calculator to suppress data-loss warnings on compacted topics (where
193-
/// low_watermark ahead of committed_offset is expected, not a loss).
217+
/// `low_watermark` ahead of `committed_offset` is expected, not a loss).
194218
pub compacted_topics: HashSet<String>,
195219
#[allow(dead_code)]
196220
pub timestamp_ms: i64,
@@ -238,15 +262,15 @@ impl OffsetCollector {
238262
/// the watermark + compacted-topic-config path.
239263
///
240264
/// Per-cycle RPC count:
241-
/// - 2 batched ListOffsets calls (EARLIEST + LATEST), routed per leader
265+
/// - 2 batched `ListOffsets` calls (EARLIEST + LATEST), routed per leader
242266
/// broker internally — O(brokers) broker round trips regardless of
243267
/// partition count.
244-
/// - `ceil(groups / 100)` batched DescribeConsumerGroups calls.
245-
/// - 1 ListConsumerGroupOffsets call per group (`PER_CALL_CHUNK = 1`
268+
/// - `ceil(groups / 100)` batched `DescribeConsumerGroups` calls.
269+
/// - 1 `ListConsumerGroupOffsets` call per group (`PER_CALL_CHUNK = 1`
246270
/// in `fetch_all_group_offsets_batched` because librdkafka 2.12
247271
/// rejects multi-group calls at the client layer; fanned out via
248272
/// `max_concurrent_groups`).
249-
/// - 1 DescribeConfigs call restricted to monitored topics.
273+
/// - 1 `DescribeConfigs` call restricted to monitored topics.
250274
#[instrument(skip(self), fields(cluster = %self.client.cluster_name()))]
251275
pub async fn collect_parallel(&self) -> Result<OffsetsSnapshot> {
252276
let start = std::time::Instant::now();
@@ -350,13 +374,21 @@ impl OffsetCollector {
350374
// refresh new topics (or nothing at all in steady state).
351375
let phase_start = std::time::Instant::now();
352376
let (mut compacted_topics, to_fetch) = self.compacted_cache.partition(&monitored_topics);
353-
if !to_fetch.is_empty() {
377+
if to_fetch.is_empty() {
378+
debug!(
379+
cached = compacted_topics.len(),
380+
"Compacted-topic cache fully hit — no DescribeConfigs RPC"
381+
);
382+
} else {
354383
debug!(
355384
to_fetch = to_fetch.len(),
356385
cached = compacted_topics.len(),
357386
"Compacted-topic cache partial miss — refreshing"
358387
);
359-
let to_fetch_owned: Vec<String> = to_fetch.iter().map(|s| s.to_string()).collect();
388+
let to_fetch_owned: Vec<String> = to_fetch
389+
.iter()
390+
.map(std::string::ToString::to_string)
391+
.collect();
360392
match self
361393
.client
362394
.fetch_compacted_topics_for(&to_fetch_owned)
@@ -368,11 +400,6 @@ impl OffsetCollector {
368400
}
369401
Err(e) => warn!(error = %e, "Failed to refresh compacted topics"),
370402
}
371-
} else {
372-
debug!(
373-
cached = compacted_topics.len(),
374-
"Compacted-topic cache fully hit — no DescribeConfigs RPC"
375-
);
376403
}
377404
// Drop cache entries for topics no longer monitored (filter change,
378405
// topic deletion) so memory doesn't grow unboundedly.
@@ -472,9 +499,9 @@ impl OffsetCollector {
472499
/// Fetch offsets for all groups via batched Admin API.
473500
///
474501
/// librdkafka 2.12's `rd_kafka_ListConsumerGroupOffsets` rejects calls with
475-
/// more than one group per call ("Exactly one ListConsumerGroupOffsets must
502+
/// more than one group per call ("Exactly one `ListConsumerGroupOffsets` must
476503
/// be passed") even though the Kafka protocol supports multi-group
477-
/// ListOffsetFetch (KIP-709). We therefore issue one FFI call per group,
504+
/// `ListOffsetFetch` (KIP-709). We therefore issue one FFI call per group,
478505
/// fanned out with bounded concurrency via `max_concurrent_groups`.
479506
///
480507
/// The win vs. the prior path is not "multi-group in one call" but:
@@ -543,10 +570,10 @@ impl OffsetCollector {
543570
match r {
544571
Ok((_gid, Ok(Ok(map)))) => merged.extend(map),
545572
Ok((gid, Ok(Err(e)))) => {
546-
warn!(group = %gid, error = %e, "Group-offset call failed")
573+
warn!(group = %gid, error = %e, "Group-offset call failed");
547574
}
548575
Ok((gid, Err(e))) => {
549-
warn!(group = %gid, error = %e, "Group-offset call task panicked")
576+
warn!(group = %gid, error = %e, "Group-offset call task panicked");
550577
}
551578
Err(e) => warn!(error = %e, "Group-offset join error"),
552579
}

0 commit comments

Comments
 (0)