Skip to main content

ursula_config/
config.rs

1use std::path::PathBuf;
2
3use serde::Deserialize;
4use serde::Serialize;
5
6use crate::human::HumanDuration;
7use crate::human::HumanSize;
8
9/// Cold-storage backend selector.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
11#[serde(rename_all = "lowercase")]
12pub enum ColdBackend {
13    #[default]
14    #[serde(alias = "disabled", alias = "off")]
15    None,
16    #[serde(alias = "mem", alias = "inmem")]
17    Memory,
18    S3,
19}
20
21/// Raft WAL persistence backend selector.
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
23#[serde(rename_all = "lowercase")]
24pub enum WalBackend {
25    #[default]
26    Memory,
27    Disk,
28}
29
30/// Raft snapshot store backend selector.
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Deserialize, Serialize)]
32#[serde(rename_all = "lowercase")]
33pub enum RaftSnapshotBackend {
34    #[default]
35    #[serde(alias = "default", alias = "")]
36    Inline,
37    S3,
38}
39
40/// Top-level Ursula server configuration.
41///
42/// Populated from a TOML config file, an optional preset, and CLI overrides.
43#[derive(Debug, Clone, Default, Deserialize, Serialize)]
44#[serde(default, deny_unknown_fields)]
45pub struct UrsulaConfig {
46    pub server: ServerConfig,
47    pub runtime: RuntimeConfig,
48    pub raft: RaftConfig,
49    pub storage: StorageConfig,
50    pub governance: GovernanceConfig,
51    pub observability: ObservabilityConfig,
52}
53
54/// HTTP server binding and admission settings.
55#[derive(Debug, Clone, Deserialize, Serialize)]
56#[serde(default, deny_unknown_fields)]
57pub struct ServerConfig {
58    /// Public HTTP client API bind address.
59    pub listen: String,
60    /// Optional separate bind for the cluster / Raft gRPC plane.
61    /// When omitted, both planes share `listen`.
62    pub cluster_listen: Option<String>,
63    /// Admin-plane bind for mutating operator endpoints (raft ops,
64    /// maintenance drain, cold-flush trigger). Loopback by default so nodes
65    /// expose no cluster-mutation surface on the network; operator tooling
66    /// reaches it through an SSH/SSM/port-forward tunnel.
67    pub admin_listen: String,
68    /// Process-wide cap on accepted write body bytes held by the HTTP layer.
69    pub http_inflight_body_size: HumanSize,
70}
71
72impl Default for ServerConfig {
73    fn default() -> Self {
74        Self {
75            listen: "127.0.0.1:4437".to_string(),
76            cluster_listen: None,
77            admin_listen: "127.0.0.1:4438".to_string(),
78            http_inflight_body_size: HumanSize::mib(256),
79        }
80    }
81}
82
83/// Per-core runtime sizing and admission controls.
84#[derive(Debug, Clone, Deserialize, Serialize)]
85#[serde(default, deny_unknown_fields)]
86pub struct RuntimeConfig {
87    /// Number of CPU cores / tokio worker threads to use.
88    pub core_count: usize,
89    /// Soft RSS cap. When the process RSS exceeds this value, new writes are
90    /// rejected with HTTP 503. `None` disables the monitor.
91    pub node_memory_abort_cap_size: Option<HumanSize>,
92    /// Minimum payload size that triggers external cold-store staging instead
93    /// of inline hot-ring storage. `None` uses the default (1 MiB).
94    pub external_payload_min_size: Option<HumanSize>,
95    /// Max live-read waiters per core. `None` or `0` disables the limit.
96    pub live_read_max_waiters_per_core: Option<usize>,
97}
98
99impl Default for RuntimeConfig {
100    fn default() -> Self {
101        Self {
102            core_count: std::thread::available_parallelism()
103                .map(|n| n.get())
104                .unwrap_or(4),
105            node_memory_abort_cap_size: None,
106            external_payload_min_size: None,
107            live_read_max_waiters_per_core: Some(65_536),
108        }
109    }
110}
111
112/// Raft consensus and static-cluster networking configuration.
113#[derive(Debug, Clone, Deserialize, Serialize)]
114#[serde(default, deny_unknown_fields)]
115pub struct RaftConfig {
116    /// Unique node ID within the static gRPC Raft cluster.
117    /// Must be present in `peers` and must be non-zero.
118    pub node_id: u64,
119    /// Number of Raft groups (shards). Defaults to `core_count * 16`.
120    pub group_count: usize,
121    /// Per-group cap on raft-submitted-but-not-yet-applied payload bytes.
122    /// `None` or `0` disables the admission. Catches raft replication lag before
123    /// in-memory queues grow unbounded.
124    pub max_uncommitted_size_per_group: Option<HumanSize>,
125    /// Bootstrap the initial Raft membership once on startup.
126    pub init_membership: bool,
127    /// Bootstrap per-group Raft membership on startup.
128    pub init_membership_per_group: bool,
129    /// Raft WAL configuration.
130    pub wal: WalConfig,
131    /// Static gRPC Raft peers. Each entry maps a `node_id` to its gRPC URL.
132    pub peers: Vec<RaftPeerConfig>,
133    /// Optional per-group voter assignments.
134    ///
135    /// When empty (the default), every Raft group uses all peers as voters.
136    /// When supplied, every group in `0..group_count` must have an entry and
137    /// each entry's voters must be a non-empty subset of `peers`.
138    #[serde(default)]
139    pub groups: Vec<RaftGroupConfig>,
140    /// How long a restarting node waits to observe an already-established
141    /// (or freshly re-elected) leader before deciding the group is truly new
142    /// and bootstrapping it. Must exceed the election window.
143    pub rejoin_probe: HumanDuration,
144    /// Timeout for probing static peers during bootstrap before logging a
145    /// warning. Continues retrying indefinitely.
146    pub bootstrap_peer_probe: HumanDuration,
147    /// Interval between static-peer reachability probes during bootstrap.
148    pub bootstrap_peer_probe_interval: HumanDuration,
149    /// gRPC connect timeout when probing static peers.
150    pub bootstrap_peer_connect: HumanDuration,
151    /// OpenRaft's `install_snapshot_timeout` covers the whole FullSnapshot RPC.
152    /// The receiver downloads and installs the referenced object before
153    /// replying, so this must be comfortably above the S3 per-attempt timeout
154    /// plus retries.
155    pub install_snapshot_timeout: HumanDuration,
156    /// Directory for memory-bootstrap marker files. When set, each group
157    /// writes a marker after successful membership initialization. On restart,
158    /// a marked memory group rejoins an observed leader or reinitializes
159    /// volatile membership if no leader exists.
160    pub memory_bootstrap_marker_dir: Option<PathBuf>,
161    /// Consecutive gRPC RPC failures before forcing a transport reconnect.
162    pub grpc_reconnect_after_failures: usize,
163    /// Max concurrent snapshot builds across all groups on this node.
164    pub snapshot_build_max_concurrency: usize,
165    /// Max concurrent snapshot installs across all groups on this node.
166    pub snapshot_install_max_concurrency: usize,
167    /// Committed Raft log entries per group between automatic snapshots.
168    /// Larger values reduce full-state snapshot CPU and tail-latency spikes at
169    /// the cost of retaining more log entries for recovery.
170    pub snapshot_logs_since_last: u64,
171    /// Aggregate unpurged Raft log entries on one node that trigger a
172    /// pressure snapshot pass. This bounds memory-WAL growth when traffic is
173    /// spread across many groups and no individual group reaches
174    /// `snapshot_logs_since_last`.
175    pub snapshot_pressure_unpurged_logs: u64,
176    /// Maximum groups snapshotted by one pressure pass.
177    pub snapshot_pressure_max_groups_per_tick: usize,
178    /// Maximum number of payload-bearing Raft log entries retained per group
179    /// after they are covered by a snapshot.
180    pub max_in_snapshot_log_to_keep: u64,
181}
182
183impl Default for RaftConfig {
184    fn default() -> Self {
185        Self {
186            node_id: 0,
187            group_count: std::thread::available_parallelism()
188                .map(|n| n.get().saturating_mul(16).max(1))
189                .unwrap_or(16),
190            max_uncommitted_size_per_group: None,
191            init_membership: false,
192            init_membership_per_group: false,
193            wal: WalConfig::default(),
194            peers: Vec::new(),
195            groups: Vec::new(),
196            rejoin_probe: HumanDuration::sec(6),
197            bootstrap_peer_probe: HumanDuration::sec(60),
198            bootstrap_peer_probe_interval: HumanDuration::milli(250),
199            bootstrap_peer_connect: HumanDuration::milli(500),
200            install_snapshot_timeout: HumanDuration::sec(120),
201            memory_bootstrap_marker_dir: None,
202            grpc_reconnect_after_failures: 8,
203            snapshot_build_max_concurrency: 1,
204            snapshot_install_max_concurrency: 1,
205            snapshot_logs_since_last: 5_000,
206            snapshot_pressure_unpurged_logs: 65_536,
207            snapshot_pressure_max_groups_per_tick: 16,
208            max_in_snapshot_log_to_keep: 64,
209        }
210    }
211}
212
213/// Raft write-ahead log configuration.
214#[derive(Debug, Clone, Deserialize, Serialize)]
215#[serde(default, deny_unknown_fields)]
216pub struct WalConfig {
217    /// WAL persistence backend.
218    pub backend: WalBackend,
219    /// Directory for on-disk WAL files. Required when `backend` is `Disk`.
220    pub path: Option<PathBuf>,
221    /// Reject writes and mark the node unready below this many available bytes.
222    /// Zero disables disk-pressure admission.
223    pub min_available_size: HumanSize,
224    /// Clear disk-pressure admission only after free space reaches this value.
225    /// Must exceed `min_available_size` when the guard is enabled.
226    pub resume_available_size: HumanSize,
227    /// Explicit opt-in for a multi-peer cluster whose Raft log is volatile.
228    pub allow_volatile_multi_peer: bool,
229}
230
231impl WalConfig {
232    /// Resolved on-disk log directory for the Raft WAL.
233    ///
234    /// When `backend` is `Disk` and `path` is set, appends the legacy
235    /// `raft-log` subdirectory so that existing data directories continue
236    /// to work after the config refactor.
237    pub fn resolved_path(&self) -> Option<PathBuf> {
238        match self.backend {
239            WalBackend::Memory => None,
240            WalBackend::Disk => self.path.as_ref().map(|p| p.join("raft-log")),
241        }
242    }
243}
244
245impl Default for WalConfig {
246    fn default() -> Self {
247        Self {
248            backend: WalBackend::Memory,
249            path: None,
250            min_available_size: HumanSize::mib(512),
251            resume_available_size: HumanSize::gib(1),
252            allow_volatile_multi_peer: false,
253        }
254    }
255}
256
257/// A single static gRPC Raft peer.
258#[derive(Debug, Clone, Deserialize, Serialize)]
259#[serde(deny_unknown_fields)]
260pub struct RaftPeerConfig {
261    /// Peer node ID.
262    pub node_id: u64,
263    /// Peer gRPC URL.
264    pub url: String,
265}
266
267/// Per-group voter assignment for heterogeneous static clusters.
268///
269/// When omitted (the default), every group uses all peers as voters.
270#[derive(Debug, Clone, Deserialize, Serialize)]
271#[serde(deny_unknown_fields)]
272pub struct RaftGroupConfig {
273    /// Raft group ID.
274    pub raft_group_id: u32,
275    /// Node IDs that are voters for this group.
276    pub voters: Vec<u64>,
277}
278
279/// Storage tier configuration.
280#[derive(Debug, Clone, Default, Deserialize, Serialize)]
281#[serde(default, deny_unknown_fields)]
282pub struct StorageConfig {
283    /// Cold-tier (opendal-backed object store) configuration.
284    pub cold: ColdConfig,
285    /// Raft snapshot store configuration.
286    pub snapshot: RaftSnapshotConfig,
287}
288
289/// Cold-tier flush, GC, and cache configuration.
290#[derive(Debug, Clone, Deserialize, Serialize)]
291#[serde(default, deny_unknown_fields)]
292pub struct ColdConfig {
293    /// Cold-storage backend.
294    pub backend: ColdBackend,
295    /// Root prefix for cold-storage objects (e.g. S3 prefix or local dir).
296    pub root: Option<String>,
297    /// S3-specific connection and credential settings.
298    /// Required when `backend` is `S3`.
299    pub s3: Option<S3Config>,
300    /// Optional cold-read cache.
301    pub cache: Option<ColdCacheConfig>,
302    /// Interval between periodic cold-flush passes. Must be non-zero.
303    pub flush_interval: HumanDuration,
304    /// Target number of hot bytes to flush per group per pass.
305    pub flush_size: HumanSize,
306    /// Minimum hot bytes a group must have before it is eligible for flush.
307    /// Falls back to [`flush_size`](Self::flush_size) when unset.
308    pub flush_min_hot_size: Option<HumanSize>,
309    /// Aggregate hot bytes across locally led groups that activate a pressure
310    /// flush. A pressure pass allows undersized groups to flush without
311    /// changing their independent state ownership. `0` disables the fallback.
312    pub flush_pressure_hot_size: HumanSize,
313    /// Upper bound on bytes flushed per group per pass.
314    /// Falls back to [`flush_size`](Self::flush_size) when unset.
315    pub flush_max_size: Option<HumanSize>,
316    /// Max groups flushed concurrently.
317    pub flush_max_concurrency: usize,
318    /// Enable background same-stream cold chunk compaction.
319    pub compaction_enabled: bool,
320    /// Interval between cold chunk compaction discovery passes.
321    pub compaction_interval: HumanDuration,
322    /// Preferred compacted object size.
323    pub compaction_target_size: HumanSize,
324    /// Hard maximum compacted object size.
325    pub compaction_max_size: HumanSize,
326    /// Maximum streams compacted per pass.
327    pub compaction_max_streams_per_pass: usize,
328    /// Grace period before compacted input objects are physically deleted.
329    pub compaction_gc_grace: HumanDuration,
330    /// Per-group hot-size cap. When a group's hot bytes exceed this, new
331    /// writes are rejected with HTTP 503. `None` or `0` disables the admission.
332    pub max_hot_size_per_group: Option<HumanSize>,
333    /// Interval between periodic cold-gc passes. Must be non-zero.
334    pub gc_interval: HumanDuration,
335    /// Max GC entries to process per group per pass.
336    pub gc_max_entries: usize,
337}
338
339impl ColdConfig {
340    /// Minimum hot bytes a group must have before it is eligible for flush.
341    ///
342    /// Falls back to [`flush_size`](Self::flush_size) when the user does not
343    /// supply an explicit value.
344    pub fn flush_min_hot_size(&self) -> HumanSize {
345        self.flush_min_hot_size.unwrap_or(self.flush_size)
346    }
347
348    /// Upper bound on bytes flushed per group per pass.
349    ///
350    /// Falls back to [`flush_size`](Self::flush_size) when the user does not
351    /// supply an explicit value.
352    pub fn flush_max_size(&self) -> HumanSize {
353        self.flush_max_size.unwrap_or(self.flush_size)
354    }
355}
356
357impl Default for ColdConfig {
358    fn default() -> Self {
359        Self {
360            backend: ColdBackend::None,
361            root: None,
362            s3: None,
363            cache: None,
364            flush_interval: HumanDuration::sec(1),
365            flush_size: HumanSize::mib(8),
366            flush_min_hot_size: None,
367            flush_pressure_hot_size: HumanSize::mib(128),
368            flush_max_size: None,
369            flush_max_concurrency: 4,
370            compaction_enabled: false,
371            compaction_interval: HumanDuration::sec(30),
372            compaction_target_size: HumanSize::mib(8),
373            compaction_max_size: HumanSize::mib(16),
374            compaction_max_streams_per_pass: 16,
375            compaction_gc_grace: HumanDuration::sec(300),
376            max_hot_size_per_group: Some(HumanSize::mib(64)),
377            gc_interval: HumanDuration::sec(5),
378            gc_max_entries: 256,
379        }
380    }
381}
382
383/// S3 connection and credential settings.
384#[derive(Debug, Clone, Deserialize, Serialize)]
385#[serde(default, deny_unknown_fields)]
386pub struct S3Config {
387    /// S3 bucket name.
388    pub bucket: Option<String>,
389    /// S3 region.
390    pub region: Option<String>,
391    /// Custom S3 endpoint (for MinIO, etc.).
392    pub endpoint: Option<String>,
393    /// S3 access key ID.
394    pub access_key_id: Option<String>,
395    /// S3 secret access key.
396    pub secret_access_key: Option<String>,
397    /// Optional S3 session token.
398    pub session_token: Option<String>,
399    /// Server-side encryption requested on every cold-tier object write.
400    ///
401    /// Defaults to `aes256` (SSE-S3): free on AWS S3 and the baseline for any
402    /// shared deployment. MinIO only honors it when a KMS/KES is configured —
403    /// set `none` explicitly for a MinIO deployment without one. `aws-kms`
404    /// uses the AWS-managed KMS key unless `kms_key_id` names a customer
405    /// managed key.
406    pub server_side_encryption: S3ServerSideEncryption,
407    /// Customer managed KMS key for `server_side_encryption = "aws-kms"`.
408    pub kms_key_id: Option<String>,
409    /// Per-S3-operation timeout.
410    pub timeout: HumanDuration,
411    /// Max retries per S3 operation.
412    pub max_retries: usize,
413    /// Timeout for S3 health probes.
414    pub probe_timeout: HumanDuration,
415    /// Consecutive probe failures before marking S3 unhealthy.
416    pub unhealthy_ticks: usize,
417    /// Consecutive probe successes before marking S3 healthy again.
418    pub heal_ticks: usize,
419}
420
421impl Default for S3Config {
422    fn default() -> Self {
423        Self {
424            bucket: None,
425            region: None,
426            endpoint: None,
427            access_key_id: None,
428            secret_access_key: None,
429            session_token: None,
430            server_side_encryption: S3ServerSideEncryption::default(),
431            kms_key_id: None,
432            timeout: HumanDuration::sec(10),
433            max_retries: 3,
434            probe_timeout: HumanDuration::sec(2),
435            unhealthy_ticks: 1,
436            heal_ticks: 2,
437        }
438    }
439}
440
441/// Server-side encryption mode for cold-tier S3 writes.
442#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
443#[serde(rename_all = "kebab-case")]
444pub enum S3ServerSideEncryption {
445    /// SSE-S3 (`x-amz-server-side-encryption: AES256`). The default.
446    #[default]
447    #[serde(rename = "aes256")]
448    Aes256,
449    /// SSE-KMS; uses the AWS managed key unless `kms_key_id` is set.
450    AwsKms,
451    /// No server-side encryption header. Required for object stores that
452    /// reject the header (for example MinIO without a configured KMS).
453    None,
454}
455
456/// Cold-read cache sizing.
457#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
458#[serde(default, deny_unknown_fields)]
459pub struct ColdCacheConfig {
460    /// Max cache size in bytes.
461    pub max_size: HumanSize,
462    /// Cache block size in bytes.
463    pub block_size: HumanSize,
464    /// Number of blocks to read ahead on cache miss.
465    pub readahead_blocks: usize,
466}
467
468impl Default for ColdCacheConfig {
469    fn default() -> Self {
470        Self {
471            max_size: HumanSize::mib(256),
472            block_size: HumanSize::mib(1),
473            readahead_blocks: 4,
474        }
475    }
476}
477
478/// Raft snapshot store configuration.
479#[derive(Debug, Clone, Deserialize, Serialize)]
480#[serde(default, deny_unknown_fields)]
481pub struct RaftSnapshotConfig {
482    /// Snapshot store backend.
483    pub backend: RaftSnapshotBackend,
484    /// S3 namespace for snapshot objects, relative to the cold-storage root.
485    /// Used only when `backend` is `S3`.
486    pub s3_prefix: Option<String>,
487    /// Interval for the manual snapshot driver.
488    ///
489    /// When omitted, inline snapshot stores keep the manual driver disabled and
490    /// external snapshot stores use a 5s manual-driver default. Explicit `0s`
491    /// disables the manual driver and keeps openraft's default auto-policy.
492    pub drive_interval: Option<HumanDuration>,
493    /// Retained for configuration compatibility. Snapshot driving no longer
494    /// forces cold flushes; the cold worker owns flush concurrency.
495    pub drive_flush_concurrency: usize,
496}
497
498impl Default for RaftSnapshotConfig {
499    fn default() -> Self {
500        Self {
501            backend: RaftSnapshotBackend::Inline,
502            s3_prefix: None,
503            drive_interval: None,
504            drive_flush_concurrency: 4,
505        }
506    }
507}
508
509/// Cluster governance and health-gate configuration.
510#[derive(Debug, Clone, Default, Deserialize, Serialize)]
511#[serde(default, deny_unknown_fields)]
512pub struct GovernanceConfig {
513    /// Leadership balancing configuration.
514    pub leadership_balance: LeadershipBalanceConfig,
515    /// Cluster egress probe configuration.
516    pub cluster_probe: ClusterProbeConfig,
517    /// Commit-stall watchdog configuration.
518    pub commit_stall: CommitStallConfig,
519    /// Cold-storage health gate configuration.
520    pub cold_health: ColdHealthConfig,
521}
522
523/// Leadership balancer tuning.
524#[derive(Debug, Clone, Deserialize, Serialize)]
525#[serde(default, deny_unknown_fields)]
526pub struct LeadershipBalanceConfig {
527    /// Tick interval for the leadership balancer.
528    pub interval: HumanDuration,
529    /// Max leader handoffs to attempt per tick.
530    pub max_per_tick: usize,
531    /// Timeout when querying peer shed state.
532    pub peer_timeout: HumanDuration,
533}
534
535impl Default for LeadershipBalanceConfig {
536    fn default() -> Self {
537        Self {
538            interval: HumanDuration::sec(5),
539            max_per_tick: 4,
540            peer_timeout: HumanDuration::milli(500),
541        }
542    }
543}
544
545/// Cluster egress probe tuning.
546#[derive(Debug, Clone, Deserialize, Serialize)]
547#[serde(default, deny_unknown_fields)]
548pub struct ClusterProbeConfig {
549    /// Tick interval for egress probes.
550    pub interval: HumanDuration,
551    /// Payload size for egress probe messages.
552    pub probe_size: HumanSize,
553    /// Timeout for individual egress probes.
554    pub timeout: HumanDuration,
555    /// Consecutive failed ticks before marking egress unhealthy.
556    pub unhealthy_ticks: usize,
557    /// Consecutive healthy ticks before clearing egress unhealthy.
558    pub heal_ticks: usize,
559}
560
561impl Default for ClusterProbeConfig {
562    fn default() -> Self {
563        Self {
564            interval: HumanDuration::milli(500),
565            probe_size: HumanSize::kib(64),
566            timeout: HumanDuration::milli(200),
567            unhealthy_ticks: 2,
568            heal_ticks: 6,
569        }
570    }
571}
572
573/// Commit-stall watchdog tuning.
574#[derive(Debug, Clone, Deserialize, Serialize)]
575#[serde(default, deny_unknown_fields)]
576pub struct CommitStallConfig {
577    /// Tick interval for the commit-stall watchdog.
578    pub interval: HumanDuration,
579    /// Duration a group must be stalled (`last_log_index > committed_index`)
580    /// before triggering a leader transfer.
581    pub threshold: HumanDuration,
582}
583
584impl Default for CommitStallConfig {
585    fn default() -> Self {
586        Self {
587            interval: HumanDuration::sec(2),
588            threshold: HumanDuration::sec(15),
589        }
590    }
591}
592
593/// Cold-storage health gate tuning.
594#[derive(Debug, Clone, Deserialize, Serialize)]
595#[serde(default, deny_unknown_fields)]
596pub struct ColdHealthConfig {
597    /// Tick interval for the cold-health gate.
598    pub interval: HumanDuration,
599    /// Consecutive unhealthy ticks before shedding leadership.
600    pub unhealthy_ticks: usize,
601    /// Consecutive healthy ticks before re-allowing leadership.
602    pub heal_ticks: usize,
603    /// High watermark for per-group hot bytes. Exceeding this contributes to
604    /// unhealthy.
605    pub hot_size_high: HumanSize,
606    /// Low watermark for per-group hot bytes. Dropping below this contributes
607    /// to healthy.
608    pub hot_size_low: HumanSize,
609    /// Error-count threshold per tick that marks cold as unhealthy.
610    pub errors_per_tick_high: usize,
611}
612
613impl Default for ColdHealthConfig {
614    fn default() -> Self {
615        Self {
616            interval: HumanDuration::sec(2),
617            unhealthy_ticks: 3,
618            heal_ticks: 5,
619            // Leave the normal 8 MiB cold-flush threshold enough room to run.
620            // The old 7 MiB watermark forced leadership shedding before a
621            // group became eligible for its first flush.
622            hot_size_high: HumanSize::mib(48),
623            hot_size_low: HumanSize::mib(32),
624            errors_per_tick_high: 1,
625        }
626    }
627}
628
629/// Observability and debugging features.
630#[derive(Debug, Clone, Default, Deserialize, Serialize)]
631#[serde(default, deny_unknown_fields)]
632pub struct ObservabilityConfig {
633    /// Enable tokio-console integration.
634    pub tokio_console: bool,
635}