krabka_log/config.rs
1//! Tunables for `Log`. Defaults match Apache Kafka 4.2.
2
3use {
4 krabka_compression::CompressionType,
5 krabka_protocol::records::TimestampType,
6 krabka_units::prelude::{
7 ByteSize, Ratio, Time, TimeExt as _, bytes, days, fraction, gibibytes, hours, kibibytes,
8 mebibytes, millis,
9 },
10};
11
12/// Kafka's `segment.bytes` default: roll the active segment at 1 GiB.
13const DEFAULT_SEGMENT_SIZE: ByteSize = gibibytes(1);
14
15/// Kafka's `segment.ms` default: roll the active segment once its first
16/// record is a week old.
17const DEFAULT_SEGMENT_ROLL_INTERVAL: Time = days(7);
18
19/// Kafka's `retention.ms` default: delete sealed segments a week after their
20/// newest record.
21const DEFAULT_RETENTION: Time = days(7);
22
23/// Kafka's `index.interval.bytes` default: one sparse `.index`/`.timeindex`
24/// entry per 4 KiB of `.log`.
25pub const DEFAULT_INDEX_INTERVAL: ByteSize = kibibytes(4);
26
27/// Kafka's `segment.index.bytes` default: each sparse index of a segment
28/// holds up to 10 MiB of entries before the segment rolls.
29const DEFAULT_SEGMENT_INDEX_SIZE: ByteSize = mebibytes(10);
30
31/// Kafka's `max.message.bytes` default: 1 MiB of records plus the 12-byte
32/// `Records.LOG_OVERHEAD` that prefixes every batch on the wire.
33///
34/// Kafka's
35/// broker-wide `message.max.bytes` carries the same number, and a topic that
36/// sets neither inherits it.
37pub const DEFAULT_MAX_MESSAGE_SIZE: ByteSize = bytes(1_048_588);
38
39/// Kafka's `delete.retention.ms` default: a tombstone or transaction marker
40/// stays readable for a day after it first becomes compaction-eligible.
41const DEFAULT_DELETE_RETENTION: Time = hours(24);
42
43/// Default clock-confidence bound for scheduled delivery: the broker treats
44/// its own clock as accurate to within a quarter of a second.
45const DEFAULT_DELIVERY_CLOCK_UNCERTAINTY: Time = millis(250);
46
47/// Default upper bound on a single read's initial allocation.
48pub const DEFAULT_READ_BUFFER_CAP: ByteSize = mebibytes(4);
49
50/// Default cap on how far past its own window a verbatim read asks the
51/// kernel to read ahead.
52pub const DEFAULT_READ_AHEAD_MAX: ByteSize = mebibytes(4);
53
54/// Default for [`LogConfig::tail_cache_size`].
55///
56/// A consumer that falls more than this far behind the producer reads from
57/// the disk, because `O_DIRECT` writes leave nothing in the page cache, and
58/// the disk reads slow it down further. At 1 MiB, one default
59/// `max.partition.fetch.bytes`, a consumer of a single partition taking about
60/// 600 MB/s fell seconds behind (krabka-broker#1370), and 32 MiB kept its
61/// end-to-end p99 under 100 ms. The cache is also most of what a partition
62/// in direct mode holds in memory: a broker with 100 such partitions taking
63/// 1 KiB records used 7–8 GB of anonymous memory at 32 MiB and 3.7 GB at
64/// 8 MiB, with a higher rate (1.22M against 1.09–1.16M records/s) and a lower
65/// end-to-end p99, while the single-partition consumer at 1.3 GB/s measured
66/// the same rate at 8 and 16 MiB and within 5% of 32 MiB.
67pub const DEFAULT_TAIL_CACHE_SIZE: ByteSize = mebibytes(8);
68
69/// Default for [`LogConfig::writeback_window`].
70///
71/// Linux lets a file's dirty pages pile up to `vm.dirty_ratio` of memory
72/// before it pauses the writer, and `balance_dirty_pages` then pauses it for
73/// up to 200 ms at a time; a 1 GiB segment's roll then syncs up to 1 GiB at
74/// once. Three brokers writing 1 KiB records at the disk's rate through the
75/// page cache measured a produce p99 of 0.75–0.9 s, against 0.09 s through
76/// `O_DIRECT`, on the OMB max-rate RF3 workloads. 2 MiB is about two
77/// milliseconds of one partition's writes at that rate, so two of them in
78/// flight bound the wait an append can see to that. Against 8 MiB, 2 MiB
79/// cut the publish p99 of 50K 1 KiB records/s at RF3 from 14–21 ms to 4–6 ms
80/// and raised the 100-partition max rate by a quarter, for 4% of the max
81/// rate of a single partition.
82pub const DEFAULT_WRITEBACK_WINDOW: ByteSize = mebibytes(2);
83
84/// Default byte window for timestamp scans between sparse index entries.
85pub const DEFAULT_TIMESTAMP_SCAN_WINDOW: ByteSize = kibibytes(64);
86
87/// Kafka's `min.cleanable.dirty.ratio` default: half the log has to be
88/// uncleaned before a compaction pass is worth its I/O.
89const DEFAULT_MIN_CLEANABLE_DIRTY_RATIO: Ratio = fraction(0.5);
90
91/// Per-topic policy for what to do with old log segments.
92///
93/// Kafka's `cleanup.policy` is a list, and `LogConfig` derives two independent
94/// booleans from it: `compact` when the list contains `compact` and `delete`
95/// when it contains `delete`. The four sets an operator can write are the
96/// four variants here.
97///
98/// `Delete` is the default. It deletes segments by age or by size in
99/// `crate::retention`. `Compact` does newest-wins dedup by key.
100/// `crate::compact` implements it, and [`crate::Log::compact`] invokes it.
101/// `CompactAndDelete` is Kafka's `compact,delete`: the log cleaner runs over
102/// it *and* retention deletes its old segments. Kafka Streams writes exactly
103/// that value on every windowed-store changelog topic, so a broker that
104/// refuses it cannot host a Streams application with a windowed store.
105/// `NoCleanup` is the empty list, which Kafka accepts and documents as
106/// infinite retention: neither the cleaner nor time and size retention runs.
107///
108/// Ask [`Self::contains_compact`] and [`Self::contains_delete`] rather than
109/// comparing variants: `Compact` and `CompactAndDelete` both run the cleaner.
110///
111/// `as_str` is the `cleanup.policy` value Kafka reports for the policy, which
112/// is what `DescribeConfigs` echoes back.
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, krabka_macros::EnumStr)]
114#[enum_str(case = "lowercase")]
115pub enum CleanupPolicy {
116 #[default]
117 Delete,
118 Compact,
119 #[enum_str(name = "compact,delete")]
120 CompactAndDelete,
121 #[enum_str(name = "")]
122 NoCleanup,
123}
124
125impl CleanupPolicy {
126 /// `true` when the policy list contains `compact`, which is what makes a
127 /// partition the log cleaner's work.
128 #[must_use]
129 pub const fn contains_compact(self) -> bool {
130 matches!(self, Self::Compact | Self::CompactAndDelete)
131 }
132
133 /// `true` when the policy list contains `delete`, which is what makes a
134 /// partition's old segments eligible for time-, size- and
135 /// start-offset-based retention.
136 #[must_use]
137 pub const fn contains_delete(self) -> bool {
138 matches!(self, Self::Delete | Self::CompactAndDelete)
139 }
140}
141
142/// How a new segment's `.log` file gets its disk blocks: Kafka's
143/// `preallocate`.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
145pub enum SegmentAllocation {
146 /// `preallocate=false`, Kafka's default: the file allocates blocks as
147 /// appends grow it.
148 #[default]
149 OnWrite,
150 /// `preallocate=true`: reserve a segment's disk blocks ahead of its
151 /// appends, a quarter of `segment_size` at a time (at least 1 MiB), so
152 /// appends do not allocate as they grow it and a broker with many
153 /// partitions does not commit a whole segment per partition up front,
154 /// and write its batches through `O_DIRECT`. A truncate gives the
155 /// reservation up, and the segment takes it again.
156 ///
157 /// The `O_DIRECT` writes bypass the page cache. An append returns once
158 /// its write is queued, and shared write threads put the queued writes on
159 /// the disk, several at a time, so a produce request is acknowledged
160 /// before its bytes reach the disk, as it is through the page cache. The
161 /// newest [`LogConfig::tail_cache_size`] of the segment, and every byte
162 /// that is not on the disk yet, stays in memory to serve reads. A write
163 /// that fails fails every later append and sync of the segment. A direct
164 /// write covers whole blocks, so the active segment's file runs up to a
165 /// block past its last batch until it is sealed. Where
166 /// the kernel does not report the alignment `O_DIRECT` needs (before Linux
167 /// 6.1, or on tmpfs), the segment writes through the page cache.
168 ///
169 /// Kafka sets the file's length to `segment.bytes` and trims it back when
170 /// the segment closes. krabka reserves the blocks without changing the
171 /// length (`fallocate(FALLOC_FL_KEEP_SIZE)`), so a segment's file is
172 /// never longer than the batches in it, and gives back what is left of
173 /// the reservation when the segment is sealed. The bytes on disk are the
174 /// same either way. Off Linux, or on a filesystem that cannot reserve
175 /// blocks, a segment allocates as it is written.
176 Preallocate,
177}
178
179/// Per-topic policy for when a durable record becomes visible to consumers.
180///
181/// `Immediate` is the default and is every ordinary topic. `Scheduled` gates
182/// visibility on each batch's activation time, so a producer can write a
183/// record now and have it delivered later. `crate::delivery` implements it,
184/// and [`Log::advance_delivery_watermark`](crate::Log::advance_delivery_watermark)
185/// computes the offset that separates the visible prefix from the scheduled
186/// tail.
187#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
188pub enum DeliveryPolicy {
189 /// A batch is visible as soon as it is durable.
190 #[default]
191 Immediate,
192 /// A batch is visible once its activation time has passed. The activation
193 /// time is the batch's `max_timestamp`, the v2 header field, so the
194 /// schedule travels with the records and needs no sidecar.
195 Scheduled,
196}
197
198/// Whether a scheduled partition's delivery times must not run backwards.
199///
200/// KFC-1's `delivery.schedule.monotonic`. It is an enum rather than a `bool`
201/// because it is meaningless outside [`DeliveryPolicy::Scheduled`], and the
202/// two names say at the call site which rule is in force.
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
204pub enum ScheduleOrder {
205 /// Accept any delivery time, so a later batch may come due before an
206 /// earlier one. The default, and the only behaviour Kafka has.
207 Unordered,
208 /// Refuse, with `INVALID_TIMESTAMP`, a batch whose delivery time precedes
209 /// the largest delivery time the partition already holds.
210 Monotonic,
211}
212
213/// KIP-950's two tiered-storage disablement controls.
214///
215/// They are separate from [`LogConfig::remote_storage_enable`] because they
216/// describe how the tier is *left*, not whether it is on: `copy_disable`
217/// freezes the remote copy while reads keep being served from it, and
218/// `delete_on_disable` is the operator's standing consent to erase the remote
219/// copies when tiering is turned off. Kafka refuses the
220/// `remote.storage.enable` `true -> false` flip without the second one.
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
222pub struct RemoteTierFlags {
223 /// `remote.log.copy.disable`: the remote log manager copies nothing new
224 /// for this partition, and retention over what is already there keeps
225 /// running.
226 pub copy_disable: bool,
227 /// `remote.log.delete.on.disable`: turning `remote.storage.enable` off
228 /// erases the partition's remote segments and raises its log start offset
229 /// to the local log start.
230 pub delete_on_disable: bool,
231}
232
233impl RemoteTierFlags {
234 /// Both off, which is Kafka's default: a tiered topic copies, and
235 /// disabling tiering is refused until an operator opts into the delete.
236 pub const DEFAULT: Self = Self {
237 copy_disable: false,
238 delete_on_disable: false,
239 };
240}
241
242/// Tunables for [`Log`](crate::Log) behavior.
243///
244/// Defaults match Apache Kafka 4.2 for `segment.bytes`, `segment.ms`,
245/// `retention.ms`, `index.interval.bytes`, and the other tunables. Start from
246/// the [`Default`](Self::default) impl. Most production deployments override
247/// only [`Self::retention`] and [`Self::retention_size`].
248#[derive(Debug, Clone, PartialEq, krabka_macros::FieldDefaults)]
249pub struct LogConfig {
250 /// Cap the initial allocation used by decoded and raw segment reads.
251 #[default(DEFAULT_READ_BUFFER_CAP)]
252 pub read_buffer_cap: ByteSize,
253
254 /// How far past a verbatim read's own window the read asks the kernel to
255 /// read ahead, as a `POSIX_FADV_WILLNEED` hint, so a consumer that is
256 /// behind finds its next fetch in the page cache. The hint covers the
257 /// window and then up to one more window, capped at this. `0` hints only
258 /// the window itself. The broker gives the hint as one `WILLNEED` call
259 /// for each 128 KiB of the range, because Linux reads only about
260 /// `max(max_sectors_kb, read_ahead_kb)` of one call. The hint is a no-op
261 /// off Linux.
262 #[default(DEFAULT_READ_AHEAD_MAX)]
263 pub read_ahead_max: ByteSize,
264
265 /// Read timestamp searches in windows of this size.
266 #[default(DEFAULT_TIMESTAMP_SCAN_WINDOW)]
267 pub timestamp_scan_window: ByteSize,
268
269 /// Roll the active segment once it grows past this. Kafka's
270 /// `segment.bytes`; default 1 GiB.
271 #[default(DEFAULT_SEGMENT_SIZE)]
272 pub segment_size: ByteSize,
273
274 /// Roll the active segment when its first record is older than this.
275 /// Kafka's `segment.ms`; default 7 days.
276 #[default(DEFAULT_SEGMENT_ROLL_INTERVAL)]
277 pub segment_roll_interval: Time,
278
279 /// Maximum random subtraction from the segment roll interval, sampled per segment.
280 pub segment_jitter: Time,
281
282 /// Delay before retired segment files are unlinked. Readers stop seeing them immediately.
283 #[default(krabka_units::secs(60))]
284 pub file_delete_delay: Time,
285
286 /// How long a log start that only retention moved may wait before it is
287 /// written to `log-start-offset-checkpoint`. Kafka's broker-wide
288 /// `log.flush.start.offset.checkpoint.interval.ms`; default one minute.
289 /// Zero writes the start on the next maintenance pass.
290 /// Retention moves the start to a segment boundary, which the segment
291 /// names already record, so the write can wait: see
292 /// [`Log::checkpoint_log_start_if_due`](crate::Log::checkpoint_log_start_if_due).
293 #[default(krabka_units::secs(60))]
294 pub start_offset_checkpoint_interval: Time,
295
296 /// Offset span between forced syncs. `None` disables the threshold.
297 pub flush_messages: Option<u64>,
298
299 /// Time between forced syncs of dirty data. `None` disables the timer.
300 pub flush_interval: Option<Time>,
301
302 /// Gzip level used when encoding owned batches; -1 selects the codec default.
303 #[default(-1)]
304 pub compression_gzip_level: i32,
305
306 /// Lz4 level used when encoding owned batches.
307 #[default(9)]
308 pub compression_lz4_level: i32,
309
310 /// Zstd level used when encoding owned batches.
311 #[default(3)]
312 pub compression_zstd_level: i32,
313
314 /// Delete sealed segments older than this. `None` = unlimited. Kafka's
315 /// `retention.ms`; default 7 days.
316 #[default(Some(DEFAULT_RETENTION))]
317 pub retention: Option<Time>,
318
319 /// Delete oldest sealed segments until the total `.log` size fits.
320 /// `None` = unlimited. Kafka's `retention.bytes`.
321 pub retention_size: Option<ByteSize>,
322
323 /// Largest single record batch this partition accepts, measured over the
324 /// batch's whole wire encoding including its 61-byte v2 header. Kafka's
325 /// `max.message.bytes`; default 1048588. A `Produce` carrying a larger
326 /// batch is refused with `MESSAGE_TOO_LARGE` (10) rather than truncated,
327 /// because a batch that lands in the log is a batch every consumer has to
328 /// fetch whole.
329 #[default(DEFAULT_MAX_MESSAGE_SIZE)]
330 pub max_message_size: ByteSize,
331
332 /// Kafka trunk's `max.decompressed.message.bytes`: the largest record body a
333 /// compressed batch may hold when the log decompresses it, which it does to
334 /// compact a partition and to answer a by-timestamp `ListOffsets`. A record
335 /// above it fails the lookup or the pass with
336 /// [`LogError::RecordTooLarge`](crate::LogError::RecordTooLarge),
337 /// as `DefaultRecord.readFrom` throws `InvalidRecordException`. An
338 /// uncompressed batch is never held to it. `None` is no limit, which is
339 /// Kafka's default (`Records.SOFT_MAX_ARRAY_LENGTH`) and the only setting
340 /// Kafka 4.3.1 has: the broker sets it only under
341 /// `unstable.api.versions.enable`.
342 pub max_decompressed_record: Option<ByteSize>,
343
344 /// Write one `.index`/`.timeindex` entry per this much `.log`. Kafka's
345 /// `index.interval.bytes`; default 4 KiB.
346 #[default(DEFAULT_INDEX_INTERVAL)]
347 pub index_interval: ByteSize,
348
349 /// Roll the active segment once its `.index` or its `.timeindex` holds
350 /// this many bytes of entries. Kafka's `segment.index.bytes`; default
351 /// 10 MiB. Kafka rounds the size down to whole entries, so the offset
352 /// index holds `size / 8` entries and the time index `size / 12`.
353 #[default(DEFAULT_SEGMENT_INDEX_SIZE)]
354 pub segment_index_size: ByteSize,
355
356 /// fsync after every `append`. Default off. The broker manages fsync
357 /// separately.
358 pub flush_on_append: bool,
359
360 /// Kafka's `preallocate`. Defaults to [`SegmentAllocation::OnWrite`],
361 /// Kafka's `false`. See [`SegmentAllocation`].
362 pub segment_allocation: SegmentAllocation,
363
364 /// How much of the newest data an active segment that writes through
365 /// `O_DIRECT` keeps in memory to serve reads with. Those writes bypass
366 /// the page cache and drop what it held of the range, so without this a
367 /// consumer reading right behind the producer reads from disk. Every
368 /// partition under [`SegmentAllocation::Preallocate`] holds up to this
369 /// much, plus one batch; `0` keeps only the newest batch. Default 8 MiB:
370 /// see [`DEFAULT_TAIL_CACHE_SIZE`].
371 #[default(DEFAULT_TAIL_CACHE_SIZE)]
372 pub tail_cache_size: ByteSize,
373
374 /// In a segment written through the page cache, start a background
375 /// `fdatasync` of the active `.log` file after every this many appended
376 /// bytes, and make an append wait while two such syncs are still running.
377 /// The kernel then holds about twice this much dirty data for the segment
378 /// at most, so neither its dirty-page throttling, which pauses a writer
379 /// for up to 200 ms at a time, nor the sync at segment roll holds an
380 /// append for long. A segment written through `O_DIRECT` needs none of
381 /// this, and `flush_on_append` syncs every append anyway. `0` leaves
382 /// write-back to the kernel. Default 8 MiB: see
383 /// [`DEFAULT_WRITEBACK_WINDOW`].
384 #[default(DEFAULT_WRITEBACK_WINDOW)]
385 pub writeback_window: ByteSize,
386
387 /// The most page cache the segments written through it keep, over every
388 /// log in the process. Each background sync of an active segment records
389 /// the whole pages it covered, and while the recorded bytes exceed this,
390 /// the oldest recorded pages of any log are dropped from the page cache
391 /// (`posix_fadvise(POSIX_FADV_DONTNEED)`), off the appending thread. A
392 /// consumer further behind than this reads from the disk. Without it, a
393 /// container's memory limit is what evicts the pages, and at that limit
394 /// the kernel reclaims a page inside every append that writes one: in
395 /// 10 GiB containers taking 50K 1 KiB records/s at RF3, that doubled the
396 /// publish p50 for minutes at a time. `None` keeps every page until the
397 /// kernel evicts it. Logs with different values each trim the shared
398 /// record to their own. Segments written through `O_DIRECT` keep no page
399 /// cache and are not counted. Default `None`; the broker sets half of
400 /// its memory limit.
401 pub page_cache_budget: Option<ByteSize>,
402
403 /// On open, CRC every batch in the active segment and rebuild its sparse indexes.
404 #[default(true)]
405 pub validate_on_open: bool,
406
407 /// Cleanup policy. Defaults to `Delete`. See [`CleanupPolicy`].
408 #[default(CleanupPolicy::Delete)]
409 pub cleanup_policy: CleanupPolicy,
410
411 /// Kafka's `min.compaction.lag.ms`: a record stays uncompacted for at
412 /// least this long after it is written. The broker's cleaner reads it and
413 /// leaves a partition whose newest dirty record is younger than this
414 /// alone. Default 0, which is Kafka's.
415 #[default(Time::ZERO)]
416 pub min_compaction_lag: Time,
417
418 /// Kafka's `max.compaction.lag.ms`: however clean a partition looks, once
419 /// its oldest dirty record is older than this the cleaner runs anyway.
420 /// `None` is no bound, which is Kafka's default (`Long.MAX_VALUE`).
421 pub max_compaction_lag: Option<Time>,
422
423 /// Kafka's `min.cleanable.dirty.ratio`: the share of a compacted
424 /// partition's log that must be uncleaned before the cleaner spends a pass
425 /// on it. Default 0.5, which is Kafka's.
426 #[default(DEFAULT_MIN_CLEANABLE_DIRTY_RATIO)]
427 pub min_cleanable_dirty_ratio: Ratio,
428
429 /// Kafka's `message.timestamp.type`: whose clock the stored records carry.
430 /// `CreateTime` is the producer's own timestamp and is the default;
431 /// `LogAppendTime` is the broker's clock at append time.
432 #[default(TimestampType::CreateTime)]
433 pub message_timestamp_type: TimestampType,
434
435 /// Broker-side recompression target. `None` is Kafka's
436 /// `compression.type=producer`, which is pass-through: the broker stores
437 /// the batch exactly as the producer sent it. `Some(c)` re-encodes every
438 /// batch the broker accepts on this partition to `c` before the write.
439 /// This matches Kafka's per-topic `compression.type` config. `gzip`,
440 /// `snappy`, `lz4`, `zstd`, and `uncompressed` map to `Some(_)`.
441 /// `producer`, the default, maps to `None`.
442 pub compression_type: Option<CompressionType>,
443
444 /// When `true`, the broker's `RemoteLogManager` may copy this
445 /// partition's sealed segments (KIP-405) to the remote tier. This maps to
446 /// Kafka's per-topic `remote.storage.enable`. Default `false`, which is
447 /// also Kafka's default, because tiered storage is opt-in per topic.
448 pub remote_storage_enable: bool,
449
450 /// KIP-950's two disablement controls, `remote.log.copy.disable` and
451 /// `remote.log.delete.on.disable`. See [`RemoteTierFlags`].
452 // Both KIP-950 controls default off, as in Kafka: a tiered topic
453 // copies, and turning tiering off needs the operator's consent.
454 #[default(RemoteTierFlags::DEFAULT)]
455 pub remote_tier: RemoteTierFlags,
456
457 /// Local-disk time-retention window for tiered partitions (KIP-405).
458 /// `None` inherits [`Self::retention`]. Default `None`.
459 pub local_retention: Option<Time>,
460
461 /// Local-disk size budget for tiered partitions (KIP-405).
462 /// `None` inherits [`Self::retention_size`]. Default `None`.
463 pub local_retention_size: Option<ByteSize>,
464
465 /// KIP-534. After a tombstone or transaction marker first becomes
466 /// compaction-eligible, the log retains it for at least this long before
467 /// deletion. This is the delete-horizon grace window. Default 24h.
468 #[default(DEFAULT_DELETE_RETENTION)]
469 pub delete_retention: Time,
470
471 /// When a durable record becomes visible. Defaults to
472 /// [`DeliveryPolicy::Immediate`]. See [`DeliveryPolicy`].
473 // Scheduled delivery is opt-in per topic; an ordinary topic pays
474 // nothing for it.
475 #[default(DeliveryPolicy::Immediate)]
476 pub delivery_policy: DeliveryPolicy,
477
478 /// KFC-1 `delivery.schedule.monotonic`, read only under
479 /// [`DeliveryPolicy::Scheduled`]. See [`ScheduleOrder`].
480 ///
481 /// The check belongs to the log because only the log serializes appends.
482 /// A partition's schedule is a property of what it already holds, so a
483 /// test that reads it and an append that extends it have to be the same
484 /// critical section: two producers that each pass a check taken before the
485 /// append can still land out of order, and so can two jobs the writer
486 /// batches into one group. Kafka runs its own record-shape rejections in
487 /// the same place, under `UnifiedLog.append`'s lock.
488 // Kafka has no such setting, and a scheduled topic accepts a
489 // schedule that runs backwards unless an operator asks otherwise.
490 #[default(ScheduleOrder::Unordered)]
491 pub schedule_order: ScheduleOrder,
492
493 /// Declared bound on how far this broker's clock can be from true time.
494 /// Default 250 ms. It has an effect only under
495 /// [`DeliveryPolicy::Scheduled`].
496 ///
497 /// A batch is visible once
498 /// `max_timestamp + delivery_clock_uncertainty <= now`. If the clock
499 /// reads `c` while true time is somewhere in `[c - e, c + e]`, then
500 /// `c >= activation + e` proves true time has reached the activation
501 /// instant. Provided the declared bound holds, this test does not deliver
502 /// early. The clock test's additional delay is at most twice the bound;
503 /// polling, unavailable storage and an earlier pending batch can delay
504 /// delivery further.
505 ///
506 /// Changes are prospective for the published delivery prefix: increasing
507 /// this bound or moving the clock backward does not retract offsets the
508 /// in-memory watermark already released. The new bound governs subsequent
509 /// advances and pending batches. A restart rebuilds that watermark from
510 /// retained records using the current bound; the watermark is not durable.
511 #[default(DEFAULT_DELIVERY_CLOCK_UNCERTAINTY)]
512 pub delivery_clock_uncertainty: Time,
513}
514
515impl LogConfig {
516 /// The configured level for a codec that supports compression levels.
517 #[must_use]
518 pub fn compression_level(&self, codec: CompressionType) -> Option<i32> {
519 match codec {
520 CompressionType::Gzip => Some(self.compression_gzip_level),
521 CompressionType::Lz4 => Some(self.compression_lz4_level),
522 CompressionType::Zstd => Some(self.compression_zstd_level),
523 _ => None,
524 }
525 }
526}
527
528#[cfg(test)]
529mod tests {
530
531 use {
532 super::*,
533 krabka_units::prelude::{ByteSizeExt as _, TimeExt, bytes, millis, secs},
534 };
535
536 #[test]
537 fn defaults_match_kafka_4x() {
538 assert2::assert!(
539 LogConfig::default()
540 == LogConfig {
541 read_buffer_cap: mebibytes(4),
542 read_ahead_max: mebibytes(4),
543 timestamp_scan_window: kibibytes(64),
544 segment_size: bytes(1 << 30),
545 segment_roll_interval: days(7),
546 segment_jitter: Time::ZERO,
547 file_delete_delay: secs(60),
548 start_offset_checkpoint_interval: secs(60),
549 flush_messages: None,
550 flush_interval: None,
551 compression_gzip_level: -1,
552 compression_lz4_level: 9,
553 compression_zstd_level: 3,
554 retention: Some(days(7)),
555 retention_size: None,
556 max_message_size: bytes(1_048_588),
557 max_decompressed_record: None,
558 index_interval: bytes(4096),
559 segment_index_size: bytes(10 * 1024 * 1024),
560 flush_on_append: false,
561 segment_allocation: SegmentAllocation::OnWrite,
562 tail_cache_size: mebibytes(8),
563 writeback_window: mebibytes(2),
564 page_cache_budget: None,
565 validate_on_open: true,
566 cleanup_policy: CleanupPolicy::Delete,
567 min_compaction_lag: Time::ZERO,
568 max_compaction_lag: None,
569 min_cleanable_dirty_ratio: fraction(0.5),
570 message_timestamp_type: TimestampType::CreateTime,
571 compression_type: None,
572 remote_storage_enable: false,
573 remote_tier: RemoteTierFlags::DEFAULT,
574 local_retention: None,
575 local_retention_size: None,
576 delete_retention: secs(24 * 60 * 60),
577 delivery_policy: DeliveryPolicy::Immediate,
578 schedule_order: ScheduleOrder::Unordered,
579 delivery_clock_uncertainty: millis(250),
580 }
581 );
582 }
583
584 #[test]
585 fn defaults_cross_the_raw_seams_as_kafkas_documented_numbers() {
586 // The quantities exist to be handed to `.index` sizing, retention
587 // arithmetic, and Kafka config reporting as plain integers; a
588 // scale slip in a constructor would show up here.
589 let c = LogConfig::default();
590 assert2::check!(c.segment_size.bytes_u64() == 1_073_741_824);
591 assert2::check!(c.index_interval.bytes_u64() == 4_096);
592 assert2::check!(c.segment_index_size.bytes_u64() == 10_485_760);
593 assert2::check!(c.max_message_size.bytes_u64() == 1_048_588);
594 assert2::check!(c.segment_roll_interval.millis_i64() == 604_800_000);
595 assert2::check!(c.retention.map(TimeExt::millis_i64) == Some(604_800_000));
596 assert2::check!(c.delete_retention.millis_i64() == 86_400_000);
597 }
598
599 #[test]
600 fn default_cleanup_policy_is_delete() {
601 let c = LogConfig::default();
602 assert2::assert!(c.cleanup_policy == CleanupPolicy::Delete);
603 }
604
605 #[yare::parameterized(
606 delete = { CleanupPolicy::Delete, false, true, "delete" },
607 compact = { CleanupPolicy::Compact, true, false, "compact" },
608 compact_and_delete = { CleanupPolicy::CompactAndDelete, true, true, "compact,delete" },
609 no_cleanup = { CleanupPolicy::NoCleanup, false, false, "" },
610 )]
611 fn a_policy_reports_the_halves_of_kafkas_cleanup_policy_list_it_contains(
612 policy: CleanupPolicy,
613 compact: bool,
614 delete: bool,
615 rendered: &'static str,
616 ) {
617 // `LogConfig` in Kafka derives `compact` and `delete` from the list by
618 // membership, so `compact,delete` is both, and each name alone is one.
619 assert2::check!(policy.contains_compact() == compact, "{policy:?}");
620 assert2::check!(policy.contains_delete() == delete, "{policy:?}");
621 assert2::check!(policy.as_str() == rendered, "{policy:?}");
622 }
623
624 #[test]
625 fn default_compression_is_producer_passthrough() {
626 let c = LogConfig::default();
627 assert2::assert!(c.compression_type == None);
628 }
629
630 #[test]
631 fn delivery_is_immediate_with_a_quarter_second_clock_bound() {
632 let c = LogConfig::default();
633 assert2::check!(c.delivery_policy == DeliveryPolicy::Immediate);
634 assert2::check!(c.delivery_clock_uncertainty.millis_i64() == 250);
635 }
636
637 #[test]
638 fn default_local_retention_is_none() {
639 let c = LogConfig::default();
640 assert2::assert!(c.local_retention == None);
641 assert2::assert!(c.local_retention_size == None);
642 }
643}