krabka_remote_storage_topic/kafka_log/
config.rs1use {
10 krabka_client_core::{
11 ClientFrameMax, ConnectionDispatchQueueCapacity, security::ClientSecurity,
12 },
13 krabka_macros::RefinedNewtype,
14 krabka_units::prelude::{
15 ByteSize, ByteSizeExt as _, Time, TimeExt as _, mebibytes, millis, secs,
16 },
17 refined_type::rule::{GreaterI32, GreaterUsize},
18 std::time::Duration,
19};
20
21pub const METADATA_TOPIC: &str = "__remote_log_metadata";
23
24pub const DEFAULT_NUM_PARTITIONS: i32 = 50;
27
28pub const DEFAULT_REPLICATION: i32 = 3;
31
32pub const DEFAULT_MIN_ISR: i32 = 2;
36
37pub const DEFAULT_METADATA_TOPIC_CREATE_TIMEOUT: Time = secs(30);
40
41pub const DEFAULT_METADATA_FETCH_MAX_WAIT: Time = millis(500);
45
46pub const DEFAULT_METADATA_FETCH_MAX_BYTES: ByteSize = mebibytes(1);
49
50pub const DEFAULT_METADATA_FETCH_RETRY_BACKOFF: Time = millis(200);
53
54pub const DEFAULT_METADATA_EVENT_QUEUE_CAPACITY: usize = 1024;
56
57#[derive(Clone, Copy, Debug, Eq, PartialEq, RefinedNewtype)]
59#[refined(
60 rule(GreaterUsize<0>),
61 string_error,
62 label = "metadata event queue capacity",
63 getter = capacity,
64 default = DEFAULT_METADATA_EVENT_QUEUE_CAPACITY
65)]
66pub struct MetadataEventQueueCapacity(usize);
67
68#[derive(Debug, Clone)]
71pub struct KafkaMetadataLogConfig {
72 pub bootstrap: String,
75 pub topic: String,
79 pub num_partitions: i32,
84 pub replication: i32,
87 pub min_isr: Option<i32>,
91 pub client_id: String,
93 pub compacted: bool,
95 pub provision_topic: bool,
97 pub security: Option<ClientSecurity>,
101 pub topic_create_timeout: Time,
103 pub fetch_max_wait: Time,
105 pub fetch_max_bytes: ByteSize,
107 pub fetch_retry_backoff: Time,
109 pub event_queue_capacity: MetadataEventQueueCapacity,
111 pub dispatch_queue_capacity: ConnectionDispatchQueueCapacity,
113 pub frame_max: ClientFrameMax,
115}
116
117impl KafkaMetadataLogConfig {
118 #[must_use]
120 pub fn new(bootstrap: impl Into<String>) -> Self {
121 Self {
122 bootstrap: bootstrap.into(),
123 topic: METADATA_TOPIC.to_owned(),
124 num_partitions: DEFAULT_NUM_PARTITIONS,
125 replication: DEFAULT_REPLICATION,
126 min_isr: Some(DEFAULT_MIN_ISR),
127 client_id: "krabka-rlmm".to_owned(),
128 compacted: false,
129 provision_topic: true,
130 security: None,
131 topic_create_timeout: DEFAULT_METADATA_TOPIC_CREATE_TIMEOUT,
132 fetch_max_wait: DEFAULT_METADATA_FETCH_MAX_WAIT,
133 fetch_max_bytes: DEFAULT_METADATA_FETCH_MAX_BYTES,
134 fetch_retry_backoff: DEFAULT_METADATA_FETCH_RETRY_BACKOFF,
135 event_queue_capacity: MetadataEventQueueCapacity::default(),
136 dispatch_queue_capacity: ConnectionDispatchQueueCapacity::default(),
137 frame_max: ClientFrameMax::default(),
138 }
139 }
140
141 pub fn validate(&self) -> Result<(), String> {
149 if let Some(min_isr) = self.min_isr {
150 GreaterI32::<0>::new(min_isr).map_err(|error| format!("min_isr: {error}"))?;
151 }
152 validate_positive_whole_millis_i32("topic_create_timeout", self.topic_create_timeout)?;
153 validate_positive_whole_millis_i32("fetch_max_wait", self.fetch_max_wait)?;
154 validate_positive_whole_bytes_i32("fetch_max_bytes", self.fetch_max_bytes)?;
155 validate_positive_duration("fetch_retry_backoff", self.fetch_retry_backoff)
156 }
157}
158
159fn validate_positive_whole_millis_i32(name: &str, value: Time) -> Result<(), String> {
160 let millis = value.millis_i64();
161 if !value.secs_f64().is_finite() || Time::from_millis(millis) != value {
162 return Err(format!(
163 "{name} must be a positive whole number of milliseconds within 1..=i32::MAX"
164 ));
165 }
166 let millis = i32::try_from(millis).map_err(|_| {
167 format!("{name} must be a positive whole number of milliseconds within 1..=i32::MAX")
168 })?;
169 GreaterI32::<0>::new(millis)
170 .map(|_| ())
171 .map_err(|error| format!("{name}: {error}"))
172}
173
174fn validate_positive_whole_bytes_i32(name: &str, value: ByteSize) -> Result<(), String> {
175 let bytes = value.bytes_i64();
176 if !value.bytes_f64().is_finite() || ByteSize::from_bytes_i64(bytes) != value {
177 return Err(format!(
178 "{name} must be a positive whole number of bytes within 1..=i32::MAX"
179 ));
180 }
181 let bytes = i32::try_from(bytes).map_err(|_| {
182 format!("{name} must be a positive whole number of bytes within 1..=i32::MAX")
183 })?;
184 GreaterI32::<0>::new(bytes)
185 .map(|_| ())
186 .map_err(|error| format!("{name}: {error}"))
187}
188
189fn validate_positive_duration(name: &str, value: Time) -> Result<(), String> {
190 let duration = Duration::try_from_secs_f64(value.secs_f64())
191 .map_err(|error| format!("{name}: {error}"))?;
192 if duration.is_zero() {
193 return Err(format!("{name} must be positive"));
194 }
195 Ok(())
196}
197
198#[cfg(test)]
199mod tests {
200 use {
201 super::*,
202 assert2::{assert, check},
203 std::time::Duration,
204 };
205
206 fn configured(configure: impl FnOnce(&mut KafkaMetadataLogConfig)) -> KafkaMetadataLogConfig {
207 let mut cfg = KafkaMetadataLogConfig::new("127.0.0.1:9092");
208 configure(&mut cfg);
209 cfg
210 }
211
212 #[test]
213 fn config_defaults_match_kafka() {
214 let cfg = KafkaMetadataLogConfig::new("127.0.0.1:9092");
215 check!(cfg.topic == METADATA_TOPIC);
216 check!(cfg.num_partitions == 50);
217 check!(cfg.replication == 3);
218 check!(cfg.min_isr == Some(2));
219 check!(!cfg.compacted);
220 check!(cfg.provision_topic);
221 check!(cfg.bootstrap == "127.0.0.1:9092");
222 check!(cfg.security.is_none());
223 check!(cfg.topic_create_timeout == secs(30));
224 check!(cfg.fetch_max_wait == millis(500));
225 check!(cfg.fetch_max_bytes == mebibytes(1));
226 check!(cfg.fetch_retry_backoff == millis(200));
227 check!(cfg.event_queue_capacity.capacity() == 1024);
228 cfg.validate().unwrap();
229 }
230
231 #[test]
232 fn config_accepts_custom_transport_policy() {
233 let cfg = KafkaMetadataLogConfig {
234 topic_create_timeout: secs(45),
235 fetch_max_wait: millis(750),
236 fetch_max_bytes: mebibytes(2),
237 fetch_retry_backoff: millis(300),
238 event_queue_capacity: MetadataEventQueueCapacity::new(2048).unwrap(),
239 ..KafkaMetadataLogConfig::new("127.0.0.1:9092")
240 };
241
242 cfg.validate().unwrap();
243 check!(cfg.topic_create_timeout == secs(45));
244 check!(cfg.fetch_max_wait == millis(750));
245 check!(cfg.fetch_max_bytes == mebibytes(2));
246 check!(cfg.fetch_retry_backoff == millis(300));
247 check!(cfg.event_queue_capacity.capacity() == 2048);
248 }
249
250 #[yare::parameterized(
251 create_timeout_zero = {
252 "topic_create_timeout",
253 configured(|cfg| cfg.topic_create_timeout = Time::ZERO),
254 },
255 create_timeout_submillisecond = {
256 "topic_create_timeout",
257 configured(|cfg| cfg.topic_create_timeout = Time::from_micros(500)),
258 },
259 create_timeout_infinite = {
260 "topic_create_timeout",
261 configured(|cfg| cfg.topic_create_timeout = Time::from_secs_f64(f64::INFINITY)),
262 },
263 create_timeout_too_large = {
264 "topic_create_timeout",
265 configured(|cfg| cfg.topic_create_timeout = Time::from_millis(i64::from(i32::MAX) + 1)),
266 },
267 fetch_wait_zero = { "fetch_max_wait", configured(|cfg| cfg.fetch_max_wait = Time::ZERO) },
268 fetch_wait_submillisecond = {
269 "fetch_max_wait",
270 configured(|cfg| cfg.fetch_max_wait = Time::from_micros(500)),
271 },
272 fetch_wait_infinite = {
273 "fetch_max_wait",
274 configured(|cfg| cfg.fetch_max_wait = Time::from_secs_f64(f64::INFINITY)),
275 },
276 fetch_wait_too_large = {
277 "fetch_max_wait",
278 configured(|cfg| cfg.fetch_max_wait = Time::from_millis(i64::from(i32::MAX) + 1)),
279 },
280 fetch_bytes_zero = {
281 "fetch_max_bytes",
282 configured(|cfg| cfg.fetch_max_bytes = ByteSize::ZERO),
283 },
284 fetch_bytes_fractional = {
285 "fetch_max_bytes",
286 configured(|cfg| cfg.fetch_max_bytes = ByteSize::from_bytes_f64(0.5)),
287 },
288 fetch_bytes_infinite = {
289 "fetch_max_bytes",
290 configured(|cfg| cfg.fetch_max_bytes = ByteSize::from_bytes_f64(f64::INFINITY)),
291 },
292 fetch_bytes_too_large = {
293 "fetch_max_bytes",
294 configured(|cfg| cfg.fetch_max_bytes = ByteSize::from_bytes_i64(i64::from(i32::MAX) + 1)),
295 },
296 retry_backoff_zero = {
297 "fetch_retry_backoff",
298 configured(|cfg| cfg.fetch_retry_backoff = Time::ZERO),
299 },
300 retry_backoff_infinite = {
301 "fetch_retry_backoff",
302 configured(|cfg| cfg.fetch_retry_backoff = Time::from_secs_f64(f64::INFINITY)),
303 },
304 )]
305 fn config_rejects_invalid_transport_policy(field: &str, cfg: KafkaMetadataLogConfig) {
306 let error = cfg.validate().expect_err("invalid policy must fail");
307 assert2::assert!(error.contains(field), "field={field}, error={error}");
308 }
309
310 #[yare::parameterized(
311 one = { Some(1), true },
312 two = { Some(2), true },
313 zero = { Some(0), false },
314 negative = { Some(-1), false },
315 unset_uses_cluster_default = { None, true },
316 )]
317 fn min_isr_must_be_at_least_one(min_isr: Option<i32>, valid: bool) {
318 let cfg = KafkaMetadataLogConfig {
319 min_isr,
320 ..KafkaMetadataLogConfig::new("127.0.0.1:9092")
321 };
322 let result = cfg.validate();
323 assert2::assert!(result.is_ok() == valid, "min_isr={min_isr:?}: {result:?}");
324 if let Err(error) = result {
325 assert2::assert!(error.contains("min_isr"), "{error}");
326 }
327 }
328
329 #[test]
330 fn metadata_event_queue_capacity_rejects_zero() {
331 assert!(MetadataEventQueueCapacity::new(0).is_err());
332 check!(MetadataEventQueueCapacity::new(1).unwrap().capacity() == 1);
333 }
334
335 #[test]
341 fn client_tunables_convert_to_their_kafka_wire_images() {
342 let cfg = KafkaMetadataLogConfig::new("127.0.0.1:9092");
343 check!(cfg.topic_create_timeout.millis_i32() == 30_000);
344 check!(cfg.fetch_max_wait.millis_i32() == 500);
345 check!(cfg.fetch_max_bytes.bytes_i32() == 1 << 20);
346 check!(cfg.fetch_retry_backoff.to_std() == Duration::from_millis(200));
347 }
348
349 #[test]
350 fn config_carries_client_resource_policy() {
351 let cfg = KafkaMetadataLogConfig {
352 dispatch_queue_capacity: ConnectionDispatchQueueCapacity::new(7).unwrap(),
353 frame_max: ClientFrameMax::try_from(krabka_units::kibibytes(32)).unwrap(),
354 ..KafkaMetadataLogConfig::new("127.0.0.1:9092")
355 };
356 check!(cfg.dispatch_queue_capacity.get() == 7);
357 check!(cfg.frame_max.size() == krabka_units::kibibytes(32));
358 }
359
360 #[test]
361 fn config_carries_security() {
362 use {
363 krabka_client_core::security::{ClientSecurity, SaslCredentials},
364 krabka_security::ListenerProtocol,
365 };
366 let cfg = KafkaMetadataLogConfig {
367 bootstrap: "127.0.0.1:9092".into(),
368 topic: METADATA_TOPIC.into(),
369 num_partitions: 1,
370 replication: 1,
371 client_id: "x".into(),
372 security: Some(ClientSecurity {
373 protocol: ListenerProtocol::SaslPlaintext,
374 tls: None,
375 sasl: Some(SaslCredentials::Plain {
376 username: "u".into(),
377 password: "p".into(),
378 }),
379 sasl_host: None,
380 }),
381 ..KafkaMetadataLogConfig::new("127.0.0.1:9092")
382 };
383 assert!(cfg.security.is_some());
384 }
385}