Skip to main content

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}