1use std::{fmt, str::FromStr};
5
6use reifydb_value::value::{
7 Value, decimal::Decimal, duration::Duration, int::Int, ordered_f32::OrderedF32, ordered_f64::OrderedF64,
8 uint::Uint, value_type::ValueType,
9};
10
11use crate::common::CommitVersion;
12
13#[derive(Debug, Clone, PartialEq, Eq)]
14pub enum AcceptError {
15 TypeMismatch {
16 expected: Vec<ValueType>,
17 actual: ValueType,
18 },
19
20 InvalidValue(String),
21}
22
23impl fmt::Display for AcceptError {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 Self::TypeMismatch {
27 expected,
28 actual,
29 } => {
30 write!(f, "expected one of {:?}, got {:?}", expected, actual)
31 }
32 Self::InvalidValue(reason) => write!(f, "{reason}"),
33 }
34 }
35}
36
37#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
38pub enum ConfigKey {
39 OracleWindowSize,
40 OracleWaterMark,
41 QueryRowBatchSize,
42 RowTtlScanBatchSize,
43 RowTtlScanInterval,
44 OperatorTtlScanBatchSize,
45 OperatorTtlScanInterval,
46 VersionEpochSampleInterval,
47 HistoricalGcBatchSize,
48 HistoricalGcInterval,
49 CdcTtlDuration,
50 CdcTtlScanInterval,
51 CdcTtlScanBatchSize,
52 CdcTtlScanMaxBatchesPerTick,
53 CdcTtlReclaimInterval,
54 CdcCompactInterval,
55 CdcCompactBlockSize,
56 CdcCompactSafetyLag,
57 CdcCompactMaxBlocksPerTick,
58 CdcCompactBlockCacheCapacity,
59 CdcCompactZstdLevel,
60 CdcRecentCacheCapacity,
61 MultiReadBufferPages,
62 MultiReadBufferPageSize,
63 MultiReclaimInterval,
64 FlowTick,
65 CdcWatermarkWaitTimeout,
66 CdcConsumeWaitTimeout,
67 FlowJoinProbeBlockSize,
68 ThreadsAsync,
69 ThreadsSystem,
70 ThreadsQuery,
71 ThreadsCommit,
72 ThreadsBackground,
73 FlowWorkerThreads,
74 SubscriptionWorkerThreads,
75 RuntimeMetricsInterval,
76 MetricFlushInterval,
77 MetricsRuntimeRetention,
78 MetricsProfilerRetention,
79}
80
81impl ConfigKey {
82 pub fn all() -> &'static [Self] {
83 &[
84 Self::OracleWindowSize,
85 Self::OracleWaterMark,
86 Self::QueryRowBatchSize,
87 Self::RowTtlScanBatchSize,
88 Self::RowTtlScanInterval,
89 Self::OperatorTtlScanBatchSize,
90 Self::OperatorTtlScanInterval,
91 Self::VersionEpochSampleInterval,
92 Self::HistoricalGcBatchSize,
93 Self::HistoricalGcInterval,
94 Self::CdcTtlDuration,
95 Self::CdcTtlScanInterval,
96 Self::CdcTtlScanBatchSize,
97 Self::CdcTtlScanMaxBatchesPerTick,
98 Self::CdcTtlReclaimInterval,
99 Self::CdcCompactInterval,
100 Self::CdcCompactBlockSize,
101 Self::CdcCompactSafetyLag,
102 Self::CdcCompactMaxBlocksPerTick,
103 Self::CdcCompactBlockCacheCapacity,
104 Self::CdcCompactZstdLevel,
105 Self::CdcRecentCacheCapacity,
106 Self::MultiReadBufferPages,
107 Self::MultiReadBufferPageSize,
108 Self::MultiReclaimInterval,
109 Self::FlowTick,
110 Self::CdcWatermarkWaitTimeout,
111 Self::CdcConsumeWaitTimeout,
112 Self::FlowJoinProbeBlockSize,
113 Self::ThreadsAsync,
114 Self::ThreadsSystem,
115 Self::ThreadsQuery,
116 Self::ThreadsCommit,
117 Self::ThreadsBackground,
118 Self::FlowWorkerThreads,
119 Self::SubscriptionWorkerThreads,
120 Self::RuntimeMetricsInterval,
121 Self::MetricFlushInterval,
122 Self::MetricsRuntimeRetention,
123 Self::MetricsProfilerRetention,
124 ]
125 }
126
127 pub fn default_value(&self) -> Value {
128 match self {
129 Self::OracleWindowSize => Value::Uint8(500),
130 Self::OracleWaterMark => Value::Uint8(20),
131 Self::QueryRowBatchSize => Value::Uint2(32),
132 Self::RowTtlScanBatchSize => Value::Uint8(10000),
133 Self::RowTtlScanInterval => Value::duration_seconds(60),
134 Self::OperatorTtlScanBatchSize => Value::Uint8(10000),
135 Self::OperatorTtlScanInterval => Value::duration_seconds(60),
136 Self::VersionEpochSampleInterval => Value::duration_seconds(1),
137 Self::HistoricalGcBatchSize => Value::Uint8(50_000),
138 Self::HistoricalGcInterval => Value::duration_seconds(30),
139 Self::CdcTtlDuration => Value::None {
140 inner: ValueType::Duration,
141 },
142 Self::CdcTtlScanInterval => Value::duration_seconds(30),
143 Self::CdcTtlScanBatchSize => Value::Uint8(8192),
144 Self::CdcTtlScanMaxBatchesPerTick => Value::Uint8(32),
145 Self::CdcTtlReclaimInterval => Value::duration_seconds(30),
146 Self::CdcCompactInterval => Value::duration_seconds(60),
147 Self::CdcCompactBlockSize => Value::Uint8(1024),
148 Self::CdcCompactSafetyLag => Value::Uint8(1024),
149 Self::CdcCompactMaxBlocksPerTick => Value::Uint8(16),
150 Self::CdcCompactBlockCacheCapacity => Value::Uint8(8),
151 Self::CdcCompactZstdLevel => Value::Uint1(7),
152 Self::CdcRecentCacheCapacity => Value::Uint8(128),
153 Self::MultiReadBufferPages => Value::Uint8(1024),
154 Self::MultiReadBufferPageSize => Value::Uint8(65536),
155 Self::MultiReclaimInterval => Value::duration_seconds(30),
156 Self::FlowTick => Value::duration_seconds(1),
157 Self::CdcWatermarkWaitTimeout => Value::duration_seconds(1),
158 Self::CdcConsumeWaitTimeout => Value::duration_seconds(30),
159 Self::FlowJoinProbeBlockSize => Value::Uint8(1024),
160 Self::ThreadsAsync => Value::Uint2(1),
161 Self::ThreadsSystem => Value::Uint2(2),
162 Self::ThreadsQuery => Value::Uint2(1),
163 Self::ThreadsCommit => Value::Uint2(2),
164 Self::ThreadsBackground => Value::Uint2(1),
165 Self::FlowWorkerThreads => Value::Uint2(0),
166 Self::SubscriptionWorkerThreads => Value::Uint2(0),
167 Self::RuntimeMetricsInterval => Value::duration_seconds(5),
168 Self::MetricFlushInterval => Value::duration_seconds(10),
169 Self::MetricsRuntimeRetention => Value::duration_seconds(7 * 24 * 3600),
170 Self::MetricsProfilerRetention => Value::duration_seconds(3600),
171 }
172 }
173
174 pub fn description(&self) -> &'static str {
175 match self {
176 Self::OracleWindowSize => "Number of transactions per conflict-detection window.",
177 Self::OracleWaterMark => "Number of conflict windows retained before cleanup is triggered.",
178 Self::QueryRowBatchSize => {
179 "Number of rows produced per batch by query / DML pipeline operators."
180 }
181 Self::RowTtlScanBatchSize => "Max rows to examine per batch during a row TTL scan.",
182 Self::RowTtlScanInterval => "How often the row TTL actor should scan for expired rows.",
183 Self::OperatorTtlScanBatchSize => {
184 "Max rows to examine per batch during an operator-state TTL scan."
185 }
186 Self::OperatorTtlScanInterval => {
187 "How often the operator-state TTL actor should scan for expired rows."
188 }
189 Self::VersionEpochSampleInterval => {
190 "How often the version-epoch sampler records a (wall-clock, commit version) sample used to map a TTL duration to a cutoff version."
191 }
192 Self::HistoricalGcBatchSize => {
193 "Max historical (key, version) pairs scanned per shape per historical GC tick."
194 }
195 Self::HistoricalGcInterval => {
196 "How often the historical-version GC actor sweeps __historical for versions older than the oracle read watermark."
197 }
198 Self::CdcTtlDuration => {
199 "Maximum age of CDC entries before eviction. When unset, CDC is retained forever; \
200 when set, must be > 0 and entries older than this duration are evicted regardless \
201 of consumer state."
202 }
203 Self::CdcTtlScanInterval => {
204 "How often the CDC producer actor scans for and evicts expired CDC entries."
205 }
206 Self::CdcTtlScanBatchSize => {
207 "Max CDC entries deleted per transaction during a CDC TTL eviction tick."
208 }
209 Self::CdcTtlScanMaxBatchesPerTick => {
210 "Upper bound on delete transactions per CDC TTL eviction tick. Caps how long one tick can run when draining a backlog; remaining work continues on the next tick."
211 }
212 Self::CdcTtlReclaimInterval => {
213 "Minimum interval between CDC free-page reclaims (incremental_vacuum + WAL checkpoint) after eviction. Decoupled from the eviction scan so frequent deletes do not trigger frequent heavyweight checkpoints."
214 }
215 Self::CdcCompactInterval => "How often the CDC compaction actor runs.",
216 Self::CdcCompactBlockSize => "Number of CDC entries packed into one compressed block.",
217 Self::CdcCompactSafetyLag => "Versions newer than (max_version - lag) are never compacted.",
218 Self::CdcCompactMaxBlocksPerTick => {
219 "Upper bound on consecutive blocks produced per actor tick."
220 }
221 Self::CdcCompactBlockCacheCapacity => {
222 "Number of decompressed CDC blocks held in the in-memory LRU cache."
223 }
224 Self::CdcCompactZstdLevel => {
225 "Zstd compression level for CDC blocks. Range 1-22; higher means smaller blocks but \
226 slower compression. Decompression cost is independent of level."
227 }
228 Self::CdcRecentCacheCapacity => {
229 "Number of most-recent decoded CDC entries held in memory so a caught-up consumer \
230 is served without re-reading and re-deserializing from the backend."
231 }
232 Self::MultiReadBufferPages => {
233 "Number of pages (contiguous row-number buckets) the multi-version read cache keeps \
234 resident before eviction. Raising it trades RAM for fewer persistent-tier reads."
235 }
236 Self::MultiReadBufferPageSize => {
237 "Number of rows per cached page (bucket) in the multi-version read cache. Must be a \
238 power of two; sets the granularity of whole-page read-ahead and completeness tracking."
239 }
240 Self::MultiReclaimInterval => {
241 "How often the multi store reclaims free pages (incremental_vacuum + WAL truncate) on its persistent SQLite tier, returning space to the OS after evictions. Decoupled from the GC/flush delete cadence."
242 }
243 Self::FlowTick => {
244 "How often the deferred and transactional flow tick coordinators wake up to dispatch \
245 due flows."
246 }
247 Self::CdcWatermarkWaitTimeout => {
248 "Backstop timeout for the CDC consumer's wait for the transaction watermark to reach the \
249 latest commit before consuming; catch-up is event-driven, so this only bounds a missed \
250 wakeup. Must be > 0."
251 }
252 Self::CdcConsumeWaitTimeout => {
253 "Backstop timeout for the CDC consumer's wait for a consume reply from the downstream \
254 consumer. A lost reply would otherwise wedge the poll loop forever; on timeout the batch \
255 is re-dispatched without advancing the checkpoint. Must be > 0."
256 }
257 Self::FlowJoinProbeBlockSize => {
258 "Number of opposite-side rows a streaming join pulls per block when probing its stored \
259 state. Bounds resident probe memory without dropping matches; smaller trades fewer \
260 resident rows for more scan round-trips."
261 }
262 Self::ThreadsAsync => {
263 "Number of worker threads for the async runtime. Must be >= 1. \
264 Read at boot before the runtime starts; changes require restart."
265 }
266 Self::ThreadsSystem => {
267 "Number of worker threads for the system pool (lightweight actors). \
268 Must be >= 1. Changes require restart."
269 }
270 Self::ThreadsQuery => {
271 "Number of worker threads for the query pool (execution-heavy actors). \
272 Must be >= 1. Changes require restart."
273 }
274 Self::ThreadsCommit => {
275 "Number of worker threads for the commit pool (synchronous pre-commit flow execution). \
276 Must be >= 1. Changes require restart."
277 }
278 Self::ThreadsBackground => {
279 "Number of worker threads for the background pool (non-critical cleanup and metrics actors). \
280 Must be >= 1. Changes require restart."
281 }
282 Self::FlowWorkerThreads => {
283 "Number of deferred-flow worker actors that maintain deferred views in parallel. \
284 0 means auto (size to the system thread pool). Higher values raise fan-out parallelism \
285 for many independent views. Changes require restart."
286 }
287 Self::SubscriptionWorkerThreads => {
288 "Number of subscription worker actors that fan out CDC changes to ephemeral \
289 subscriptions in parallel. 0 means auto (size to the system thread pool). Higher values \
290 raise fan-out parallelism for many concurrent subscriptions. Changes require restart."
291 }
292 Self::RuntimeMetricsInterval => {
293 "How often the runtime-metrics sampler records a memory snapshot into \
294 system::metrics::runtime::memory::snapshots. When unset, the history sampler is \
295 dormant and only the live ::current view is available; when set, must be > 0."
296 }
297 Self::MetricFlushInterval => {
298 "How often the metric collector flushes accumulated storage and CDC stats into the \
299 system::metrics views. Must be > 0."
300 }
301 Self::MetricsRuntimeRetention => {
302 "Row TTL applied to the system::metrics::runtime::* snapshot series so old samples are \
303 evicted. Seeded onto each runtime series at bootstrap only when it has no row settings \
304 yet; changing it affects series created after the change, not already-seeded ones. \
305 Must be > 0."
306 }
307 Self::MetricsProfilerRetention => {
308 "Row TTL applied to the system::metrics::profiler::*::snapshots series so old samples are \
309 evicted. Seeded onto each profiler series at bootstrap only when it has no row settings \
310 yet; changing it affects series created after the change, not already-seeded ones. \
311 Must be > 0."
312 }
313 }
314 }
315
316 pub fn requires_restart(&self) -> bool {
317 match self {
318 Self::OracleWindowSize => false,
319 Self::OracleWaterMark => false,
320 Self::QueryRowBatchSize => false,
321 Self::RowTtlScanBatchSize => false,
322 Self::RowTtlScanInterval => false,
323 Self::OperatorTtlScanBatchSize => false,
324 Self::OperatorTtlScanInterval => false,
325 Self::VersionEpochSampleInterval => false,
326 Self::HistoricalGcBatchSize => false,
327 Self::HistoricalGcInterval => false,
328 Self::CdcTtlDuration => false,
329 Self::CdcTtlScanInterval => true,
330 Self::CdcTtlScanBatchSize => false,
331 Self::CdcTtlScanMaxBatchesPerTick => false,
332 Self::CdcTtlReclaimInterval => false,
333 Self::CdcCompactInterval => false,
334 Self::CdcCompactBlockSize => false,
335 Self::CdcCompactSafetyLag => false,
336 Self::CdcCompactMaxBlocksPerTick => false,
337 Self::CdcCompactBlockCacheCapacity => true,
338 Self::CdcCompactZstdLevel => false,
339 Self::CdcRecentCacheCapacity => true,
340 Self::MultiReadBufferPages => true,
341 Self::MultiReadBufferPageSize => true,
342 Self::MultiReclaimInterval => true,
343 Self::FlowTick => false,
344 Self::CdcWatermarkWaitTimeout => false,
345 Self::CdcConsumeWaitTimeout => false,
346 Self::FlowJoinProbeBlockSize => false,
347 Self::ThreadsAsync => true,
348 Self::ThreadsSystem => true,
349 Self::ThreadsQuery => true,
350 Self::ThreadsCommit => true,
351 Self::ThreadsBackground => true,
352 Self::FlowWorkerThreads => true,
353 Self::SubscriptionWorkerThreads => true,
354 Self::RuntimeMetricsInterval => false,
355 Self::MetricFlushInterval => false,
356 Self::MetricsRuntimeRetention => true,
357 Self::MetricsProfilerRetention => true,
358 }
359 }
360
361 pub fn expected_types(&self) -> &'static [ValueType] {
362 match self {
363 Self::OracleWindowSize => &[ValueType::Uint8],
364 Self::OracleWaterMark => &[ValueType::Uint8],
365 Self::QueryRowBatchSize => &[ValueType::Uint2],
366 Self::RowTtlScanBatchSize => &[ValueType::Uint8],
367 Self::RowTtlScanInterval => &[ValueType::Duration],
368 Self::OperatorTtlScanBatchSize => &[ValueType::Uint8],
369 Self::OperatorTtlScanInterval => &[ValueType::Duration],
370 Self::VersionEpochSampleInterval => &[ValueType::Duration],
371 Self::HistoricalGcBatchSize => &[ValueType::Uint8],
372 Self::HistoricalGcInterval => &[ValueType::Duration],
373 Self::CdcTtlDuration => &[ValueType::Duration],
374 Self::CdcTtlScanInterval => &[ValueType::Duration],
375 Self::CdcTtlScanBatchSize => &[ValueType::Uint8],
376 Self::CdcTtlScanMaxBatchesPerTick => &[ValueType::Uint8],
377 Self::CdcTtlReclaimInterval => &[ValueType::Duration],
378 Self::CdcCompactInterval => &[ValueType::Duration],
379 Self::CdcCompactBlockSize => &[ValueType::Uint8],
380 Self::CdcCompactSafetyLag => &[ValueType::Uint8],
381 Self::CdcCompactMaxBlocksPerTick => &[ValueType::Uint8],
382 Self::CdcCompactBlockCacheCapacity => &[ValueType::Uint8],
383 Self::CdcCompactZstdLevel => &[ValueType::Uint1],
384 Self::CdcRecentCacheCapacity => &[ValueType::Uint8],
385 Self::MultiReadBufferPages => &[ValueType::Uint8],
386 Self::MultiReadBufferPageSize => &[ValueType::Uint8],
387 Self::MultiReclaimInterval => &[ValueType::Duration],
388 Self::FlowTick => &[ValueType::Duration],
389 Self::CdcWatermarkWaitTimeout => &[ValueType::Duration],
390 Self::CdcConsumeWaitTimeout => &[ValueType::Duration],
391 Self::FlowJoinProbeBlockSize => &[ValueType::Uint8],
392 Self::ThreadsAsync => &[ValueType::Uint2],
393 Self::ThreadsSystem => &[ValueType::Uint2],
394 Self::ThreadsQuery => &[ValueType::Uint2],
395 Self::ThreadsCommit => &[ValueType::Uint2],
396 Self::ThreadsBackground => &[ValueType::Uint2],
397 Self::FlowWorkerThreads => &[ValueType::Uint2],
398 Self::SubscriptionWorkerThreads => &[ValueType::Uint2],
399 Self::RuntimeMetricsInterval => &[ValueType::Duration],
400 Self::MetricFlushInterval => &[ValueType::Duration],
401 Self::MetricsRuntimeRetention => &[ValueType::Duration],
402 Self::MetricsProfilerRetention => &[ValueType::Duration],
403 }
404 }
405
406 pub fn is_optional(&self) -> bool {
407 match self {
408 Self::OracleWindowSize => false,
409 Self::OracleWaterMark => false,
410 Self::QueryRowBatchSize => false,
411 Self::RowTtlScanBatchSize => false,
412 Self::RowTtlScanInterval => false,
413 Self::OperatorTtlScanBatchSize => false,
414 Self::OperatorTtlScanInterval => false,
415 Self::VersionEpochSampleInterval => false,
416 Self::HistoricalGcBatchSize => false,
417 Self::HistoricalGcInterval => false,
418 Self::CdcTtlDuration => true,
419 Self::CdcTtlScanInterval => false,
420 Self::CdcTtlScanBatchSize => false,
421 Self::CdcTtlScanMaxBatchesPerTick => false,
422 Self::CdcTtlReclaimInterval => false,
423 Self::CdcCompactInterval => false,
424 Self::CdcCompactBlockSize => false,
425 Self::CdcCompactSafetyLag => false,
426 Self::CdcCompactMaxBlocksPerTick => false,
427 Self::CdcCompactBlockCacheCapacity => false,
428 Self::CdcCompactZstdLevel => false,
429 Self::CdcRecentCacheCapacity => false,
430 Self::MultiReadBufferPages => false,
431 Self::MultiReadBufferPageSize => false,
432 Self::MultiReclaimInterval => false,
433 Self::FlowTick => false,
434 Self::CdcWatermarkWaitTimeout => false,
435 Self::CdcConsumeWaitTimeout => false,
436 Self::FlowJoinProbeBlockSize => false,
437 Self::ThreadsAsync => false,
438 Self::ThreadsSystem => false,
439 Self::ThreadsQuery => false,
440 Self::ThreadsCommit => false,
441 Self::ThreadsBackground => false,
442 Self::FlowWorkerThreads => false,
443 Self::SubscriptionWorkerThreads => false,
444 Self::RuntimeMetricsInterval => true,
445 Self::MetricFlushInterval => false,
446 Self::MetricsRuntimeRetention => false,
447 Self::MetricsProfilerRetention => false,
448 }
449 }
450
451 fn validate_canonical(&self, value: &Value) -> Result<(), String> {
452 match self {
453 Self::CdcTtlDuration => match value {
454 Value::None {
455 ..
456 } => Ok(()),
457 Value::Duration(d) => {
458 if d.is_positive() {
459 Ok(())
460 } else {
461 Err("CDC_TTL_DURATION must be greater than zero".to_string())
462 }
463 }
464 _ => Ok(()),
465 },
466 Self::CdcCompactInterval => match value {
467 Value::Duration(d) => {
468 if d.is_positive() {
469 Ok(())
470 } else {
471 Err("CDC_COMPACT_INTERVAL must be greater than zero".to_string())
472 }
473 }
474 _ => Ok(()),
475 },
476 Self::CdcCompactBlockSize => match value {
477 Value::Uint8(0) => Err("CDC_COMPACT_BLOCK_SIZE must be greater than zero".to_string()),
478 _ => Ok(()),
479 },
480 Self::QueryRowBatchSize => match value {
481 Value::Uint2(0) => Err("QUERY_ROW_BATCH_SIZE must be greater than zero".to_string()),
482 _ => Ok(()),
483 },
484 Self::CdcCompactBlockCacheCapacity => match value {
485 Value::Uint8(0) => {
486 Err("CDC_COMPACT_BLOCK_CACHE_CAPACITY must be greater than zero".to_string())
487 }
488 _ => Ok(()),
489 },
490 Self::MultiReadBufferPages => match value {
491 Value::Uint8(0) => Err("MULTI_READ_BUFFER_PAGES must be greater than zero".to_string()),
492 _ => Ok(()),
493 },
494 Self::MultiReadBufferPageSize => match value {
495 Value::Uint8(v) if v.is_power_of_two() => Ok(()),
496 Value::Uint8(_) => {
497 Err("MULTI_READ_BUFFER_PAGE_SIZE must be a power of two".to_string())
498 }
499 _ => Ok(()),
500 },
501 Self::CdcCompactZstdLevel => match value {
502 Value::Uint1(v) if (1..=22).contains(v) => Ok(()),
503 Value::Uint1(_) => Err("CDC_COMPACT_ZSTD_LEVEL must be in [1, 22]".to_string()),
504 _ => Ok(()),
505 },
506 Self::HistoricalGcBatchSize => match value {
507 Value::Uint8(0) => {
508 Err("HISTORICAL_GC_BATCH_SIZE must be greater than zero".to_string())
509 }
510 _ => Ok(()),
511 },
512 Self::HistoricalGcInterval => match value {
513 Value::Duration(d) => {
514 if d.is_positive() {
515 Ok(())
516 } else {
517 Err("HISTORICAL_GC_INTERVAL must be greater than zero".to_string())
518 }
519 }
520 _ => Ok(()),
521 },
522 Self::FlowTick => match value {
523 Value::Duration(d) => {
524 if d.is_positive() {
525 Ok(())
526 } else {
527 Err("FLOW_TICK must be greater than zero".to_string())
528 }
529 }
530 _ => Ok(()),
531 },
532 Self::CdcWatermarkWaitTimeout => match value {
533 Value::Duration(d) => {
534 if d.is_positive() {
535 Ok(())
536 } else {
537 Err("CDC_WATERMARK_WAIT_TIMEOUT must be greater than zero".to_string())
538 }
539 }
540 _ => Ok(()),
541 },
542 Self::CdcConsumeWaitTimeout => match value {
543 Value::Duration(d) => {
544 if d.is_positive() {
545 Ok(())
546 } else {
547 Err("CDC_CONSUME_WAIT_TIMEOUT must be greater than zero".to_string())
548 }
549 }
550 _ => Ok(()),
551 },
552 Self::FlowJoinProbeBlockSize => match value {
553 Value::Uint8(0) => {
554 Err("FLOW_JOIN_PROBE_BLOCK_SIZE must be greater than zero".to_string())
555 }
556 _ => Ok(()),
557 },
558 Self::ThreadsAsync => match value {
559 Value::Uint2(0) => Err("THREADS_ASYNC must be greater than zero".to_string()),
560 _ => Ok(()),
561 },
562 Self::ThreadsSystem => match value {
563 Value::Uint2(0) => Err("THREADS_SYSTEM must be greater than zero".to_string()),
564 _ => Ok(()),
565 },
566 Self::ThreadsQuery => match value {
567 Value::Uint2(0) => Err("THREADS_QUERY must be greater than zero".to_string()),
568 _ => Ok(()),
569 },
570 Self::ThreadsCommit => match value {
571 Value::Uint2(0) => Err("THREADS_COMMIT must be greater than zero".to_string()),
572 _ => Ok(()),
573 },
574 Self::ThreadsBackground => match value {
575 Value::Uint2(0) => Err("THREADS_BACKGROUND must be greater than zero".to_string()),
576 _ => Ok(()),
577 },
578 Self::FlowWorkerThreads => Ok(()),
579 Self::SubscriptionWorkerThreads => Ok(()),
580 Self::RuntimeMetricsInterval => match value {
581 Value::None {
582 ..
583 } => Ok(()),
584 Value::Duration(d) => {
585 if d.is_positive() {
586 Ok(())
587 } else {
588 Err("RUNTIME_METRICS_INTERVAL must be greater than zero".to_string())
589 }
590 }
591 _ => Ok(()),
592 },
593 Self::MetricFlushInterval => match value {
594 Value::Duration(d) => {
595 if d.is_positive() {
596 Ok(())
597 } else {
598 Err("METRIC_FLUSH_INTERVAL must be greater than zero".to_string())
599 }
600 }
601 _ => Ok(()),
602 },
603 Self::MetricsRuntimeRetention => match value {
604 Value::Duration(d) => {
605 if d.is_positive() {
606 Ok(())
607 } else {
608 Err("METRICS_RUNTIME_RETENTION must be greater than zero".to_string())
609 }
610 }
611 _ => Ok(()),
612 },
613 Self::MetricsProfilerRetention => match value {
614 Value::Duration(d) => {
615 if d.is_positive() {
616 Ok(())
617 } else {
618 Err("METRICS_PROFILER_RETENTION must be greater than zero".to_string())
619 }
620 }
621 _ => Ok(()),
622 },
623 _ => Ok(()),
624 }
625 }
626
627 pub fn accept(&self, value: Value) -> Result<Value, AcceptError> {
628 if let Value::None {
629 inner,
630 } = &value
631 {
632 if self.is_optional() && self.expected_types().contains(inner) {
633 return Ok(value);
634 }
635 return Err(AcceptError::TypeMismatch {
636 expected: self.expected_types().to_vec(),
637 actual: value.get_type(),
638 });
639 }
640
641 let canonical = if self.expected_types().contains(&value.get_type()) {
642 value
643 } else {
644 try_coerce_numeric(&value, self.expected_types()).ok_or_else(|| AcceptError::TypeMismatch {
645 expected: self.expected_types().to_vec(),
646 actual: value.get_type(),
647 })?
648 };
649
650 self.validate_canonical(&canonical).map_err(AcceptError::InvalidValue)?;
651 Ok(canonical)
652 }
653}
654
655fn try_coerce_numeric(value: &Value, expected: &[ValueType]) -> Option<Value> {
656 for target in expected {
657 let coerced = match target {
658 ValueType::Uint1 => {
659 value.to_usize().filter(|&v| v <= u8::MAX as usize).map(|v| Value::Uint1(v as u8))
660 }
661 ValueType::Uint2 => {
662 value.to_usize().filter(|&v| v <= u16::MAX as usize).map(|v| Value::Uint2(v as u16))
663 }
664 ValueType::Uint4 => {
665 value.to_usize().filter(|&v| v <= u32::MAX as usize).map(|v| Value::Uint4(v as u32))
666 }
667 ValueType::Uint8 => {
668 value.to_usize().filter(|&v| v <= u64::MAX as usize).map(|v| Value::Uint8(v as u64))
669 }
670 ValueType::Uint16 => value.to_usize().map(|v| Value::Uint16(v as u128)),
671 ValueType::Int1 => {
672 value.to_usize().filter(|&v| v <= i8::MAX as usize).map(|v| Value::Int1(v as i8))
673 }
674 ValueType::Int2 => {
675 value.to_usize().filter(|&v| v <= i16::MAX as usize).map(|v| Value::Int2(v as i16))
676 }
677 ValueType::Int4 => {
678 value.to_usize().filter(|&v| v <= i32::MAX as usize).map(|v| Value::Int4(v as i32))
679 }
680 ValueType::Int8 => {
681 value.to_usize().filter(|&v| v <= i64::MAX as usize).map(|v| Value::Int8(v as i64))
682 }
683 ValueType::Int16 => {
684 value.to_usize().filter(|&v| v <= i128::MAX as usize).map(|v| Value::Int16(v as i128))
685 }
686 ValueType::Uint => value.to_usize().map(|v| Value::Uint(Uint::from_u64(v as u64))),
687 ValueType::Int => value.to_usize().map(|v| Value::Int(Int::from_i64(v as i64))),
688 ValueType::Decimal => value.to_usize().map(|v| Value::Decimal(Decimal::from_i64(v as i64))),
689 ValueType::Float4 => {
690 value.to_usize().and_then(|v| OrderedF32::try_from(v as f32).ok()).map(Value::Float4)
691 }
692 ValueType::Float8 => {
693 value.to_usize().and_then(|v| OrderedF64::try_from(v as f64).ok()).map(Value::Float8)
694 }
695 ValueType::Duration => value
696 .to_usize()
697 .and_then(|v| Duration::from_seconds(v as i64).ok())
698 .map(Value::Duration),
699 _ => None,
700 };
701 if coerced.is_some() {
702 return coerced;
703 }
704 }
705 None
706}
707
708impl fmt::Display for ConfigKey {
709 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
710 match self {
711 Self::OracleWindowSize => write!(f, "ORACLE_WINDOW_SIZE"),
712 Self::OracleWaterMark => write!(f, "ORACLE_WATER_MARK"),
713 Self::QueryRowBatchSize => write!(f, "QUERY_ROW_BATCH_SIZE"),
714 Self::RowTtlScanBatchSize => write!(f, "ROW_TTL_SCAN_BATCH_SIZE"),
715 Self::RowTtlScanInterval => write!(f, "ROW_TTL_SCAN_INTERVAL"),
716 Self::OperatorTtlScanBatchSize => write!(f, "OPERATOR_TTL_SCAN_BATCH_SIZE"),
717 Self::OperatorTtlScanInterval => write!(f, "OPERATOR_TTL_SCAN_INTERVAL"),
718 Self::VersionEpochSampleInterval => write!(f, "VERSION_EPOCH_SAMPLE_INTERVAL"),
719 Self::HistoricalGcBatchSize => write!(f, "HISTORICAL_GC_BATCH_SIZE"),
720 Self::HistoricalGcInterval => write!(f, "HISTORICAL_GC_INTERVAL"),
721 Self::CdcTtlDuration => write!(f, "CDC_TTL_DURATION"),
722 Self::CdcTtlScanInterval => write!(f, "CDC_TTL_SCAN_INTERVAL"),
723 Self::CdcTtlScanBatchSize => write!(f, "CDC_TTL_SCAN_BATCH_SIZE"),
724 Self::CdcTtlScanMaxBatchesPerTick => write!(f, "CDC_TTL_SCAN_MAX_BATCHES_PER_TICK"),
725 Self::CdcTtlReclaimInterval => write!(f, "CDC_TTL_RECLAIM_INTERVAL"),
726 Self::CdcCompactInterval => write!(f, "CDC_COMPACT_INTERVAL"),
727 Self::CdcCompactBlockSize => write!(f, "CDC_COMPACT_BLOCK_SIZE"),
728 Self::CdcCompactSafetyLag => write!(f, "CDC_COMPACT_SAFETY_LAG"),
729 Self::CdcCompactMaxBlocksPerTick => write!(f, "CDC_COMPACT_MAX_BLOCKS_PER_TICK"),
730 Self::CdcCompactBlockCacheCapacity => write!(f, "CDC_COMPACT_BLOCK_CACHE_CAPACITY"),
731 Self::CdcCompactZstdLevel => write!(f, "CDC_COMPACT_ZSTD_LEVEL"),
732 Self::CdcRecentCacheCapacity => write!(f, "CDC_RECENT_CACHE_CAPACITY"),
733 Self::MultiReadBufferPages => write!(f, "MULTI_READ_BUFFER_PAGES"),
734 Self::MultiReadBufferPageSize => write!(f, "MULTI_READ_BUFFER_PAGE_SIZE"),
735 Self::MultiReclaimInterval => write!(f, "MULTI_RECLAIM_INTERVAL"),
736 Self::FlowTick => write!(f, "FLOW_TICK"),
737 Self::CdcWatermarkWaitTimeout => write!(f, "CDC_WATERMARK_WAIT_TIMEOUT"),
738 Self::CdcConsumeWaitTimeout => write!(f, "CDC_CONSUME_WAIT_TIMEOUT"),
739 Self::FlowJoinProbeBlockSize => write!(f, "FLOW_JOIN_PROBE_BLOCK_SIZE"),
740 Self::ThreadsAsync => write!(f, "THREADS_ASYNC"),
741 Self::ThreadsSystem => write!(f, "THREADS_SYSTEM"),
742 Self::ThreadsQuery => write!(f, "THREADS_QUERY"),
743 Self::ThreadsCommit => write!(f, "THREADS_COMMIT"),
744 Self::ThreadsBackground => write!(f, "THREADS_BACKGROUND"),
745 Self::FlowWorkerThreads => write!(f, "FLOW_WORKER_THREADS"),
746 Self::SubscriptionWorkerThreads => write!(f, "SUBSCRIPTION_WORKER_THREADS"),
747 Self::RuntimeMetricsInterval => write!(f, "RUNTIME_METRICS_INTERVAL"),
748 Self::MetricFlushInterval => write!(f, "METRIC_FLUSH_INTERVAL"),
749 Self::MetricsRuntimeRetention => write!(f, "METRICS_RUNTIME_RETENTION"),
750 Self::MetricsProfilerRetention => write!(f, "METRICS_PROFILER_RETENTION"),
751 }
752 }
753}
754
755impl FromStr for ConfigKey {
756 type Err = String;
757
758 fn from_str(s: &str) -> Result<Self, Self::Err> {
759 match s {
760 "ORACLE_WINDOW_SIZE" => Ok(Self::OracleWindowSize),
761 "ORACLE_WATER_MARK" => Ok(Self::OracleWaterMark),
762 "QUERY_ROW_BATCH_SIZE" => Ok(Self::QueryRowBatchSize),
763 "ROW_TTL_SCAN_BATCH_SIZE" => Ok(Self::RowTtlScanBatchSize),
764 "ROW_TTL_SCAN_INTERVAL" => Ok(Self::RowTtlScanInterval),
765 "OPERATOR_TTL_SCAN_BATCH_SIZE" => Ok(Self::OperatorTtlScanBatchSize),
766 "OPERATOR_TTL_SCAN_INTERVAL" => Ok(Self::OperatorTtlScanInterval),
767 "VERSION_EPOCH_SAMPLE_INTERVAL" => Ok(Self::VersionEpochSampleInterval),
768 "HISTORICAL_GC_BATCH_SIZE" => Ok(Self::HistoricalGcBatchSize),
769 "HISTORICAL_GC_INTERVAL" => Ok(Self::HistoricalGcInterval),
770 "CDC_TTL_DURATION" => Ok(Self::CdcTtlDuration),
771 "CDC_TTL_SCAN_INTERVAL" => Ok(Self::CdcTtlScanInterval),
772 "CDC_TTL_SCAN_BATCH_SIZE" => Ok(Self::CdcTtlScanBatchSize),
773 "CDC_TTL_SCAN_MAX_BATCHES_PER_TICK" => Ok(Self::CdcTtlScanMaxBatchesPerTick),
774 "CDC_TTL_RECLAIM_INTERVAL" => Ok(Self::CdcTtlReclaimInterval),
775 "CDC_COMPACT_INTERVAL" => Ok(Self::CdcCompactInterval),
776 "CDC_COMPACT_BLOCK_SIZE" => Ok(Self::CdcCompactBlockSize),
777 "CDC_COMPACT_SAFETY_LAG" => Ok(Self::CdcCompactSafetyLag),
778 "CDC_COMPACT_MAX_BLOCKS_PER_TICK" => Ok(Self::CdcCompactMaxBlocksPerTick),
779 "CDC_COMPACT_BLOCK_CACHE_CAPACITY" => Ok(Self::CdcCompactBlockCacheCapacity),
780 "CDC_COMPACT_ZSTD_LEVEL" => Ok(Self::CdcCompactZstdLevel),
781 "CDC_RECENT_CACHE_CAPACITY" => Ok(Self::CdcRecentCacheCapacity),
782 "MULTI_READ_BUFFER_PAGES" => Ok(Self::MultiReadBufferPages),
783 "MULTI_READ_BUFFER_PAGE_SIZE" => Ok(Self::MultiReadBufferPageSize),
784 "MULTI_RECLAIM_INTERVAL" => Ok(Self::MultiReclaimInterval),
785 "FLOW_TICK" => Ok(Self::FlowTick),
786 "CDC_WATERMARK_WAIT_TIMEOUT" => Ok(Self::CdcWatermarkWaitTimeout),
787 "CDC_CONSUME_WAIT_TIMEOUT" => Ok(Self::CdcConsumeWaitTimeout),
788 "FLOW_JOIN_PROBE_BLOCK_SIZE" => Ok(Self::FlowJoinProbeBlockSize),
789 "THREADS_ASYNC" => Ok(Self::ThreadsAsync),
790 "THREADS_SYSTEM" => Ok(Self::ThreadsSystem),
791 "THREADS_QUERY" => Ok(Self::ThreadsQuery),
792 "THREADS_COMMIT" => Ok(Self::ThreadsCommit),
793 "THREADS_BACKGROUND" => Ok(Self::ThreadsBackground),
794 "FLOW_WORKER_THREADS" => Ok(Self::FlowWorkerThreads),
795 "SUBSCRIPTION_WORKER_THREADS" => Ok(Self::SubscriptionWorkerThreads),
796 "RUNTIME_METRICS_INTERVAL" => Ok(Self::RuntimeMetricsInterval),
797 "METRIC_FLUSH_INTERVAL" => Ok(Self::MetricFlushInterval),
798 "METRICS_RUNTIME_RETENTION" => Ok(Self::MetricsRuntimeRetention),
799 "METRICS_PROFILER_RETENTION" => Ok(Self::MetricsProfilerRetention),
800 _ => Err(format!("Unknown system configuration key: {}", s)),
801 }
802 }
803}
804
805#[derive(Debug, Clone)]
806pub struct Config {
807 pub key: ConfigKey,
808
809 pub value: Value,
810
811 pub default_value: Value,
812
813 pub description: &'static str,
814
815 pub requires_restart: bool,
816}
817
818pub trait GetConfig: Send + Sync {
819 fn get_config(&self, key: ConfigKey) -> Value;
820
821 fn get_config_at(&self, key: ConfigKey, version: CommitVersion) -> Value;
822
823 fn get_config_uint8(&self, key: ConfigKey) -> u64 {
824 let val = self.get_config(key);
825 match val {
826 Value::Uint8(v) => v,
827 v => panic!("config key '{}' expected Uint8, got {:?}", key, v),
828 }
829 }
830
831 fn get_config_uint1(&self, key: ConfigKey) -> u8 {
832 let val = self.get_config(key);
833 match val {
834 Value::Uint1(v) => v,
835 v => panic!("config key '{}' expected Uint1, got {:?}", key, v),
836 }
837 }
838
839 fn get_config_uint2(&self, key: ConfigKey) -> u16 {
840 let val = self.get_config(key);
841 match val {
842 Value::Uint2(v) => v,
843 v => panic!("config key '{}' expected Uint2, got {:?}", key, v),
844 }
845 }
846
847 fn get_config_duration(&self, key: ConfigKey) -> Duration {
848 let val = self.get_config(key);
849 match val {
850 Value::Duration(v) => v,
851 v => panic!("config key '{}' expected Duration, got {:?}", key, v),
852 }
853 }
854
855 fn get_config_duration_opt(&self, key: ConfigKey) -> Option<Duration> {
856 match self.get_config(key) {
857 Value::None {
858 ..
859 } => None,
860 Value::Duration(v) => Some(v),
861 v => panic!("config key '{}' expected Duration or None, got {:?}", key, v),
862 }
863 }
864}
865
866#[cfg(test)]
867mod tests {
868 use super::*;
869
870 #[test]
871 fn test_cdc_ttl_default_is_typed_null() {
872 let default = ConfigKey::CdcTtlDuration.default_value();
874 assert!(matches!(
875 default,
876 Value::None {
877 inner: ValueType::Duration
878 }
879 ));
880 }
881
882 #[test]
883 fn test_cdc_ttl_accept_passes_typed_null() {
884 let none = Value::None {
885 inner: ValueType::Duration,
886 };
887 let v = ConfigKey::CdcTtlDuration.accept(none.clone()).unwrap();
888 assert_eq!(v, none);
889 }
890
891 #[test]
892 fn test_cdc_ttl_accept_passes_positive_duration() {
893 let one_sec = Value::duration_seconds(1);
894 assert_eq!(ConfigKey::CdcTtlDuration.accept(one_sec.clone()).unwrap(), one_sec);
895
896 let one_hour = Value::duration_seconds(3600);
897 assert_eq!(ConfigKey::CdcTtlDuration.accept(one_hour.clone()).unwrap(), one_hour);
898 }
899
900 #[test]
901 fn test_cdc_ttl_accept_rejects_zero() {
902 let zero = Value::duration_seconds(0);
903 match ConfigKey::CdcTtlDuration.accept(zero).unwrap_err() {
904 AcceptError::InvalidValue(reason) => {
905 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
906 }
907 other => panic!("expected InvalidValue, got {other:?}"),
908 }
909 }
910
911 #[test]
912 fn test_cdc_ttl_accept_rejects_negative() {
913 let negative = Value::duration_seconds(-5);
914 assert!(matches!(ConfigKey::CdcTtlDuration.accept(negative), Err(AcceptError::InvalidValue(_))));
915 }
916
917 #[test]
918 fn test_other_keys_accept_in_type_values() {
919 assert!(ConfigKey::OracleWindowSize.accept(Value::Uint8(0)).is_ok());
921 assert!(ConfigKey::RowTtlScanInterval.accept(Value::duration_seconds(0)).is_ok());
922 }
923
924 #[test]
925 fn test_cdc_ttl_round_trips_through_display_and_from_str() {
926 let key: ConfigKey = "CDC_TTL_DURATION".parse().unwrap();
927 assert_eq!(key, ConfigKey::CdcTtlDuration);
928 assert_eq!(format!("{}", ConfigKey::CdcTtlDuration), "CDC_TTL_DURATION");
929 }
930
931 #[test]
932 fn test_cdc_ttl_in_all() {
933 assert!(ConfigKey::all().contains(&ConfigKey::CdcTtlDuration));
934 }
935
936 #[test]
937 fn test_all_contains_every_compact_key_and_has_expected_len() {
938 let all = ConfigKey::all();
939 assert_eq!(all.len(), 40);
940 assert!(all.contains(&ConfigKey::MetricsRuntimeRetention));
941 assert!(all.contains(&ConfigKey::MetricsProfilerRetention));
942 assert!(all.contains(&ConfigKey::VersionEpochSampleInterval));
943 assert!(all.contains(&ConfigKey::CdcWatermarkWaitTimeout));
944 assert!(all.contains(&ConfigKey::CdcConsumeWaitTimeout));
945 assert!(all.contains(&ConfigKey::FlowJoinProbeBlockSize));
946 assert!(all.contains(&ConfigKey::MultiReclaimInterval));
947 assert!(all.contains(&ConfigKey::CdcTtlScanInterval));
948 assert!(all.contains(&ConfigKey::CdcTtlScanBatchSize));
949 assert!(all.contains(&ConfigKey::CdcTtlReclaimInterval));
950 assert!(all.contains(&ConfigKey::CdcTtlScanMaxBatchesPerTick));
951 assert!(all.contains(&ConfigKey::CdcCompactInterval));
952 assert!(all.contains(&ConfigKey::CdcCompactBlockSize));
953 assert!(all.contains(&ConfigKey::CdcCompactSafetyLag));
954 assert!(all.contains(&ConfigKey::CdcCompactMaxBlocksPerTick));
955 assert!(all.contains(&ConfigKey::CdcCompactBlockCacheCapacity));
956 assert!(all.contains(&ConfigKey::CdcCompactZstdLevel));
957 assert!(all.contains(&ConfigKey::CdcRecentCacheCapacity));
958 assert!(all.contains(&ConfigKey::MultiReadBufferPages));
959 assert!(all.contains(&ConfigKey::MultiReadBufferPageSize));
960 assert!(all.contains(&ConfigKey::QueryRowBatchSize));
961 assert!(all.contains(&ConfigKey::ThreadsAsync));
962 assert!(all.contains(&ConfigKey::ThreadsSystem));
963 assert!(all.contains(&ConfigKey::ThreadsQuery));
964 assert!(all.contains(&ConfigKey::ThreadsCommit));
965 assert!(all.contains(&ConfigKey::ThreadsBackground));
966 assert!(all.contains(&ConfigKey::RuntimeMetricsInterval));
967 assert!(all.contains(&ConfigKey::MetricFlushInterval));
968 assert!(all.contains(&ConfigKey::SubscriptionWorkerThreads));
969 }
970
971 #[test]
972 fn test_runtime_metrics_interval_metadata() {
973 assert_eq!(ConfigKey::RuntimeMetricsInterval.default_value(), Value::duration_seconds(5));
975 assert_eq!(ConfigKey::RuntimeMetricsInterval.expected_types(), &[ValueType::Duration]);
976 assert!(ConfigKey::RuntimeMetricsInterval.is_optional());
977 }
978
979 #[test]
980 fn test_runtime_metrics_interval_round_trip() {
981 assert_eq!("RUNTIME_METRICS_INTERVAL".parse::<ConfigKey>().unwrap(), ConfigKey::RuntimeMetricsInterval);
982 assert_eq!(format!("{}", ConfigKey::RuntimeMetricsInterval), "RUNTIME_METRICS_INTERVAL");
983 }
984
985 #[test]
986 fn test_runtime_metrics_interval_accepts_none_and_positive_rejects_zero() {
987 let none = Value::None {
988 inner: ValueType::Duration,
989 };
990 assert_eq!(ConfigKey::RuntimeMetricsInterval.accept(none.clone()).unwrap(), none);
991
992 let five = Value::duration_seconds(5);
993 assert_eq!(ConfigKey::RuntimeMetricsInterval.accept(five.clone()).unwrap(), five);
994
995 let zero = Value::duration_seconds(0);
996 assert!(matches!(ConfigKey::RuntimeMetricsInterval.accept(zero), Err(AcceptError::InvalidValue(_))));
997 }
998
999 #[test]
1000 fn test_metric_flush_interval_metadata() {
1001 assert_eq!(ConfigKey::MetricFlushInterval.default_value(), Value::duration_seconds(10));
1003 assert_eq!(ConfigKey::MetricFlushInterval.expected_types(), &[ValueType::Duration]);
1004 assert!(!ConfigKey::MetricFlushInterval.is_optional());
1005 assert!(!ConfigKey::MetricFlushInterval.requires_restart());
1006 }
1007
1008 #[test]
1009 fn test_metric_flush_interval_round_trip() {
1010 assert_eq!("METRIC_FLUSH_INTERVAL".parse::<ConfigKey>().unwrap(), ConfigKey::MetricFlushInterval);
1011 assert_eq!(format!("{}", ConfigKey::MetricFlushInterval), "METRIC_FLUSH_INTERVAL");
1012 }
1013
1014 #[test]
1015 fn test_metric_flush_interval_accepts_positive_rejects_zero() {
1016 let ten = Value::duration_seconds(10);
1017 assert_eq!(ConfigKey::MetricFlushInterval.accept(ten.clone()).unwrap(), ten);
1018
1019 let zero = Value::duration_seconds(0);
1020 assert!(matches!(ConfigKey::MetricFlushInterval.accept(zero), Err(AcceptError::InvalidValue(_))));
1021 }
1022
1023 #[test]
1024 fn test_cdc_recent_cache_capacity_round_trip() {
1025 assert_eq!(
1026 "CDC_RECENT_CACHE_CAPACITY".parse::<ConfigKey>().unwrap(),
1027 ConfigKey::CdcRecentCacheCapacity
1028 );
1029 assert_eq!(format!("{}", ConfigKey::CdcRecentCacheCapacity), "CDC_RECENT_CACHE_CAPACITY");
1030 }
1031
1032 #[test]
1033 fn test_cdc_recent_cache_capacity_metadata() {
1034 assert_eq!(ConfigKey::CdcRecentCacheCapacity.default_value(), Value::Uint8(128));
1035 assert_eq!(ConfigKey::CdcRecentCacheCapacity.expected_types(), &[ValueType::Uint8]);
1036 assert!(ConfigKey::CdcRecentCacheCapacity.requires_restart());
1037 assert!(!ConfigKey::CdcRecentCacheCapacity.is_optional());
1038 }
1039
1040 #[test]
1041 fn test_multi_read_buffer_pages_round_trip() {
1042 assert_eq!("MULTI_READ_BUFFER_PAGES".parse::<ConfigKey>().unwrap(), ConfigKey::MultiReadBufferPages);
1043 assert_eq!(format!("{}", ConfigKey::MultiReadBufferPages), "MULTI_READ_BUFFER_PAGES");
1044 }
1045
1046 #[test]
1047 fn test_multi_read_buffer_pages_metadata_and_rejects_zero() {
1048 assert_eq!(ConfigKey::MultiReadBufferPages.default_value(), Value::Uint8(1024));
1049 assert_eq!(ConfigKey::MultiReadBufferPages.expected_types(), &[ValueType::Uint8]);
1050 assert!(ConfigKey::MultiReadBufferPages.requires_restart());
1051 assert!(!ConfigKey::MultiReadBufferPages.is_optional());
1052 match ConfigKey::MultiReadBufferPages.accept(Value::Uint8(0)).unwrap_err() {
1053 AcceptError::InvalidValue(reason) => {
1054 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1055 }
1056 other => panic!("expected InvalidValue, got {other:?}"),
1057 }
1058 }
1059
1060 #[test]
1061 fn test_multi_read_buffer_page_size_round_trip() {
1062 assert_eq!(
1063 "MULTI_READ_BUFFER_PAGE_SIZE".parse::<ConfigKey>().unwrap(),
1064 ConfigKey::MultiReadBufferPageSize
1065 );
1066 assert_eq!(format!("{}", ConfigKey::MultiReadBufferPageSize), "MULTI_READ_BUFFER_PAGE_SIZE");
1067 }
1068
1069 #[test]
1070 fn test_multi_read_buffer_page_size_metadata_and_rejects_non_power_of_two() {
1071 assert_eq!(ConfigKey::MultiReadBufferPageSize.default_value(), Value::Uint8(65536));
1074 assert_eq!(ConfigKey::MultiReadBufferPageSize.expected_types(), &[ValueType::Uint8]);
1075 assert!(ConfigKey::MultiReadBufferPageSize.requires_restart());
1076 assert!(!ConfigKey::MultiReadBufferPageSize.is_optional());
1077 assert_eq!(
1078 ConfigKey::MultiReadBufferPageSize.accept(Value::Uint8(4096)).unwrap(),
1079 Value::Uint8(4096),
1080 "a power-of-two page size is accepted"
1081 );
1082 match ConfigKey::MultiReadBufferPageSize.accept(Value::Uint8(1000)).unwrap_err() {
1083 AcceptError::InvalidValue(reason) => {
1084 assert!(reason.contains("power of two"), "unexpected reason: {reason}");
1085 }
1086 other => panic!("expected InvalidValue, got {other:?}"),
1087 }
1088 }
1089
1090 #[test]
1091 fn test_threads_keys_round_trip() {
1092 assert_eq!("THREADS_ASYNC".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsAsync);
1093 assert_eq!("THREADS_SYSTEM".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsSystem);
1094 assert_eq!("THREADS_QUERY".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsQuery);
1095 assert_eq!("THREADS_COMMIT".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsCommit);
1096 assert_eq!("THREADS_BACKGROUND".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsBackground);
1097 assert_eq!(format!("{}", ConfigKey::ThreadsAsync), "THREADS_ASYNC");
1098 assert_eq!(format!("{}", ConfigKey::ThreadsSystem), "THREADS_SYSTEM");
1099 assert_eq!(format!("{}", ConfigKey::ThreadsQuery), "THREADS_QUERY");
1100 assert_eq!(format!("{}", ConfigKey::ThreadsCommit), "THREADS_COMMIT");
1101 assert_eq!(format!("{}", ConfigKey::ThreadsBackground), "THREADS_BACKGROUND");
1102 }
1103
1104 #[test]
1105 fn test_threads_defaults() {
1106 assert_eq!(ConfigKey::ThreadsAsync.default_value(), Value::Uint2(1));
1107 assert_eq!(ConfigKey::ThreadsSystem.default_value(), Value::Uint2(2));
1108 assert_eq!(ConfigKey::ThreadsQuery.default_value(), Value::Uint2(1));
1109 assert_eq!(ConfigKey::ThreadsCommit.default_value(), Value::Uint2(2));
1110 assert_eq!(ConfigKey::ThreadsBackground.default_value(), Value::Uint2(1));
1111 }
1112
1113 #[test]
1114 fn test_threads_reject_zero() {
1115 for key in [
1116 ConfigKey::ThreadsAsync,
1117 ConfigKey::ThreadsSystem,
1118 ConfigKey::ThreadsQuery,
1119 ConfigKey::ThreadsCommit,
1120 ConfigKey::ThreadsBackground,
1121 ] {
1122 match key.accept(Value::Uint2(0)).unwrap_err() {
1123 AcceptError::InvalidValue(reason) => {
1124 assert!(
1125 reason.contains("greater than zero"),
1126 "{key}: unexpected reason: {reason}"
1127 );
1128 }
1129 other => panic!("{key}: expected InvalidValue, got {other:?}"),
1130 }
1131 }
1132 }
1133
1134 #[test]
1135 fn test_threads_accept_positive() {
1136 assert_eq!(ConfigKey::ThreadsAsync.accept(Value::Uint2(4)).unwrap(), Value::Uint2(4));
1137 assert_eq!(ConfigKey::ThreadsSystem.accept(Value::Uint2(8)).unwrap(), Value::Uint2(8));
1138 assert_eq!(ConfigKey::ThreadsQuery.accept(Value::Uint2(16)).unwrap(), Value::Uint2(16));
1139 assert_eq!(ConfigKey::ThreadsCommit.accept(Value::Uint2(4)).unwrap(), Value::Uint2(4));
1140 assert_eq!(ConfigKey::ThreadsBackground.accept(Value::Uint2(2)).unwrap(), Value::Uint2(2));
1141 }
1142
1143 #[test]
1144 fn test_threads_coerce_int4_to_uint2() {
1145 let v = ConfigKey::ThreadsQuery.accept(Value::Int4(8)).unwrap();
1146 assert_eq!(v, Value::Uint2(8));
1147 }
1148
1149 #[test]
1150 fn test_threads_require_restart() {
1151 assert!(ConfigKey::ThreadsAsync.requires_restart());
1152 assert!(ConfigKey::ThreadsSystem.requires_restart());
1153 assert!(ConfigKey::ThreadsQuery.requires_restart());
1154 assert!(ConfigKey::ThreadsCommit.requires_restart());
1155 assert!(ConfigKey::ThreadsBackground.requires_restart());
1156 }
1157
1158 #[test]
1159 fn test_query_row_batch_size_default_is_uint2_32() {
1160 assert_eq!(ConfigKey::QueryRowBatchSize.default_value(), Value::Uint2(32));
1161 }
1162
1163 #[test]
1164 fn test_query_row_batch_size_round_trips_through_display_and_from_str() {
1165 let key: ConfigKey = "QUERY_ROW_BATCH_SIZE".parse().unwrap();
1166 assert_eq!(key, ConfigKey::QueryRowBatchSize);
1167 assert_eq!(format!("{}", ConfigKey::QueryRowBatchSize), "QUERY_ROW_BATCH_SIZE");
1168 }
1169
1170 #[test]
1171 fn test_query_row_batch_size_accept_rejects_zero() {
1172 match ConfigKey::QueryRowBatchSize.accept(Value::Uint2(0)).unwrap_err() {
1173 AcceptError::InvalidValue(reason) => {
1174 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1175 }
1176 other => panic!("expected InvalidValue, got {other:?}"),
1177 }
1178 }
1179
1180 #[test]
1181 fn test_query_row_batch_size_accept_passes_positive() {
1182 assert_eq!(ConfigKey::QueryRowBatchSize.accept(Value::Uint2(1)).unwrap(), Value::Uint2(1));
1183 assert_eq!(ConfigKey::QueryRowBatchSize.accept(Value::Uint2(1024)).unwrap(), Value::Uint2(1024));
1184 }
1185
1186 #[test]
1187 fn test_query_row_batch_size_accept_rejects_zero_after_coercion() {
1188 match ConfigKey::QueryRowBatchSize.accept(Value::Int4(0)).unwrap_err() {
1189 AcceptError::InvalidValue(reason) => {
1190 assert!(reason.contains("greater than zero"));
1191 }
1192 other => panic!("expected InvalidValue, got {other:?}"),
1193 }
1194 }
1195
1196 #[test]
1197 fn test_query_row_batch_size_coerces_int4_to_uint2() {
1198 let v = ConfigKey::QueryRowBatchSize.accept(Value::Int4(64)).unwrap();
1199 assert_eq!(v, Value::Uint2(64));
1200 }
1201
1202 #[test]
1203 fn test_cdc_compact_interval_round_trips_through_display_and_from_str() {
1204 let key: ConfigKey = "CDC_COMPACT_INTERVAL".parse().unwrap();
1205 assert_eq!(key, ConfigKey::CdcCompactInterval);
1206 assert_eq!(format!("{}", ConfigKey::CdcCompactInterval), "CDC_COMPACT_INTERVAL");
1207 }
1208
1209 #[test]
1210 fn test_cdc_compact_block_size_round_trips_through_display_and_from_str() {
1211 let key: ConfigKey = "CDC_COMPACT_BLOCK_SIZE".parse().unwrap();
1212 assert_eq!(key, ConfigKey::CdcCompactBlockSize);
1213 assert_eq!(format!("{}", ConfigKey::CdcCompactBlockSize), "CDC_COMPACT_BLOCK_SIZE");
1214 }
1215
1216 #[test]
1217 fn test_cdc_compact_safety_lag_round_trips_through_display_and_from_str() {
1218 let key: ConfigKey = "CDC_COMPACT_SAFETY_LAG".parse().unwrap();
1219 assert_eq!(key, ConfigKey::CdcCompactSafetyLag);
1220 assert_eq!(format!("{}", ConfigKey::CdcCompactSafetyLag), "CDC_COMPACT_SAFETY_LAG");
1221 }
1222
1223 #[test]
1224 fn test_cdc_compact_max_blocks_per_tick_round_trips_through_display_and_from_str() {
1225 let key: ConfigKey = "CDC_COMPACT_MAX_BLOCKS_PER_TICK".parse().unwrap();
1226 assert_eq!(key, ConfigKey::CdcCompactMaxBlocksPerTick);
1227 assert_eq!(format!("{}", ConfigKey::CdcCompactMaxBlocksPerTick), "CDC_COMPACT_MAX_BLOCKS_PER_TICK");
1228 }
1229
1230 #[test]
1231 fn test_cdc_compact_interval_default_is_duration() {
1232 assert!(matches!(ConfigKey::CdcCompactInterval.default_value(), Value::Duration(_)));
1233 }
1234
1235 #[test]
1236 fn test_cdc_compact_block_size_default_is_uint8_1024() {
1237 assert_eq!(ConfigKey::CdcCompactBlockSize.default_value(), Value::Uint8(1024));
1238 }
1239
1240 #[test]
1241 fn test_cdc_compact_safety_lag_default_is_uint8_1024() {
1242 assert_eq!(ConfigKey::CdcCompactSafetyLag.default_value(), Value::Uint8(1024));
1243 }
1244
1245 #[test]
1246 fn test_cdc_compact_max_blocks_per_tick_default_is_uint8_16() {
1247 assert_eq!(ConfigKey::CdcCompactMaxBlocksPerTick.default_value(), Value::Uint8(16));
1248 }
1249
1250 #[test]
1251 fn test_cdc_compact_interval_accept_passes_positive_duration() {
1252 let one_sec = Value::duration_seconds(1);
1253 assert_eq!(ConfigKey::CdcCompactInterval.accept(one_sec.clone()).unwrap(), one_sec);
1254 }
1255
1256 #[test]
1257 fn test_cdc_compact_interval_accept_rejects_zero() {
1258 let zero = Value::duration_seconds(0);
1259 match ConfigKey::CdcCompactInterval.accept(zero).unwrap_err() {
1260 AcceptError::InvalidValue(reason) => {
1261 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1262 }
1263 other => panic!("expected InvalidValue, got {other:?}"),
1264 }
1265 }
1266
1267 #[test]
1268 fn test_cdc_compact_interval_accept_rejects_negative() {
1269 let negative = Value::duration_seconds(-5);
1270 assert!(matches!(ConfigKey::CdcCompactInterval.accept(negative), Err(AcceptError::InvalidValue(_))));
1271 }
1272
1273 #[test]
1274 fn test_cdc_compact_block_size_accept_rejects_zero() {
1275 match ConfigKey::CdcCompactBlockSize.accept(Value::Uint8(0)).unwrap_err() {
1276 AcceptError::InvalidValue(reason) => {
1277 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1278 }
1279 other => panic!("expected InvalidValue, got {other:?}"),
1280 }
1281 }
1282
1283 #[test]
1284 fn test_cdc_compact_block_size_accept_passes_positive() {
1285 assert_eq!(ConfigKey::CdcCompactBlockSize.accept(Value::Uint8(1)).unwrap(), Value::Uint8(1));
1286 assert_eq!(ConfigKey::CdcCompactBlockSize.accept(Value::Uint8(1024)).unwrap(), Value::Uint8(1024));
1287 }
1288
1289 #[test]
1290 fn test_cdc_compact_safety_lag_and_max_blocks_accept_zero() {
1291 assert_eq!(ConfigKey::CdcCompactSafetyLag.accept(Value::Uint8(0)).unwrap(), Value::Uint8(0));
1292 assert_eq!(ConfigKey::CdcCompactMaxBlocksPerTick.accept(Value::Uint8(0)).unwrap(), Value::Uint8(0));
1293 }
1294
1295 #[test]
1296 fn test_accept_coerces_int4_to_uint8_for_block_size() {
1297 let v = ConfigKey::CdcCompactBlockSize.accept(Value::Int4(1024)).unwrap();
1299 assert_eq!(v, Value::Uint8(1024));
1300 }
1301
1302 #[test]
1303 fn test_accept_coerces_int8_to_uint8_for_block_size() {
1304 let v = ConfigKey::CdcCompactBlockSize.accept(Value::Int8(2048)).unwrap();
1305 assert_eq!(v, Value::Uint8(2048));
1306 }
1307
1308 #[test]
1309 fn test_accept_rejects_zero_after_coercion() {
1310 match ConfigKey::CdcCompactBlockSize.accept(Value::Int4(0)).unwrap_err() {
1312 AcceptError::InvalidValue(reason) => {
1313 assert!(reason.contains("greater than zero"));
1314 }
1315 other => panic!("expected InvalidValue, got {other:?}"),
1316 }
1317 }
1318
1319 #[test]
1320 fn test_accept_rejects_negative_int_for_uint8_key() {
1321 assert!(matches!(
1323 ConfigKey::CdcCompactBlockSize.accept(Value::Int4(-1)),
1324 Err(AcceptError::TypeMismatch { .. })
1325 ));
1326 }
1327
1328 #[test]
1329 fn test_accept_coerces_int_to_duration_via_seconds() {
1330 let v = ConfigKey::CdcCompactInterval.accept(Value::Int4(60)).unwrap();
1332 assert!(matches!(v, Value::Duration(_)));
1333 }
1334
1335 #[test]
1336 fn test_accept_idempotent_on_canonical_uint8() {
1337 let canonical = Value::Uint8(42);
1338 assert_eq!(ConfigKey::OracleWindowSize.accept(canonical.clone()).unwrap(), canonical);
1339 }
1340
1341 #[test]
1342 fn test_accept_idempotent_on_canonical_duration() {
1343 let canonical = Value::duration_seconds(5);
1344 assert_eq!(ConfigKey::CdcCompactInterval.accept(canonical.clone()).unwrap(), canonical);
1345 }
1346
1347 #[test]
1348 fn test_accept_rejects_typed_null_for_non_optional_key() {
1349 let err = ConfigKey::CdcCompactBlockSize
1350 .accept(Value::None {
1351 inner: ValueType::Uint8,
1352 })
1353 .unwrap_err();
1354 assert!(matches!(err, AcceptError::TypeMismatch { .. }));
1355 }
1356
1357 #[test]
1358 fn test_accept_passes_typed_null_for_optional_key() {
1359 let none = Value::None {
1360 inner: ValueType::Duration,
1361 };
1362 assert_eq!(ConfigKey::CdcTtlDuration.accept(none.clone()).unwrap(), none);
1363 }
1364
1365 #[test]
1366 fn test_accept_rejects_wrong_inner_type_typed_null_for_optional_key() {
1367 let err = ConfigKey::CdcTtlDuration
1369 .accept(Value::None {
1370 inner: ValueType::Uint8,
1371 })
1372 .unwrap_err();
1373 assert!(matches!(err, AcceptError::TypeMismatch { .. }));
1374 }
1375
1376 #[test]
1377 fn test_metrics_retention_round_trip() {
1378 assert_eq!(
1379 "METRICS_RUNTIME_RETENTION".parse::<ConfigKey>().unwrap(),
1380 ConfigKey::MetricsRuntimeRetention
1381 );
1382 assert_eq!(
1383 "METRICS_PROFILER_RETENTION".parse::<ConfigKey>().unwrap(),
1384 ConfigKey::MetricsProfilerRetention
1385 );
1386 assert_eq!(format!("{}", ConfigKey::MetricsRuntimeRetention), "METRICS_RUNTIME_RETENTION");
1387 assert_eq!(format!("{}", ConfigKey::MetricsProfilerRetention), "METRICS_PROFILER_RETENTION");
1388 }
1389
1390 #[test]
1391 fn test_metrics_retention_defaults_are_7d_and_1h() {
1392 assert_eq!(ConfigKey::MetricsRuntimeRetention.default_value(), Value::duration_seconds(7 * 24 * 3600));
1395 assert_eq!(ConfigKey::MetricsProfilerRetention.default_value(), Value::duration_seconds(3600));
1396 }
1397
1398 #[test]
1399 fn test_metrics_retention_metadata() {
1400 for key in [ConfigKey::MetricsRuntimeRetention, ConfigKey::MetricsProfilerRetention] {
1401 assert_eq!(key.expected_types(), &[ValueType::Duration], "{key}");
1402 assert!(!key.is_optional(), "{key} is always defaulted, never unset");
1403 }
1404 }
1405
1406 #[test]
1407 fn test_metrics_retention_rejects_zero() {
1408 for key in [ConfigKey::MetricsRuntimeRetention, ConfigKey::MetricsProfilerRetention] {
1411 match key.accept(Value::duration_seconds(0)).unwrap_err() {
1412 AcceptError::InvalidValue(reason) => {
1413 assert!(
1414 reason.contains("greater than zero"),
1415 "{key}: unexpected reason: {reason}"
1416 );
1417 }
1418 other => panic!("{key}: expected InvalidValue, got {other:?}"),
1419 }
1420 }
1421 }
1422
1423 #[test]
1424 fn test_historical_gc_keys_round_trip() {
1425 assert_eq!("HISTORICAL_GC_BATCH_SIZE".parse::<ConfigKey>().unwrap(), ConfigKey::HistoricalGcBatchSize);
1426 assert_eq!("HISTORICAL_GC_INTERVAL".parse::<ConfigKey>().unwrap(), ConfigKey::HistoricalGcInterval);
1427 assert_eq!(format!("{}", ConfigKey::HistoricalGcBatchSize), "HISTORICAL_GC_BATCH_SIZE");
1428 assert_eq!(format!("{}", ConfigKey::HistoricalGcInterval), "HISTORICAL_GC_INTERVAL");
1429 }
1430
1431 #[test]
1432 fn test_historical_gc_defaults() {
1433 assert_eq!(ConfigKey::HistoricalGcBatchSize.default_value(), Value::Uint8(50_000));
1434 assert!(matches!(ConfigKey::HistoricalGcInterval.default_value(), Value::Duration(_)));
1435 }
1436
1437 #[test]
1438 fn test_historical_gc_batch_size_rejects_zero() {
1439 match ConfigKey::HistoricalGcBatchSize.accept(Value::Uint8(0)).unwrap_err() {
1440 AcceptError::InvalidValue(reason) => {
1441 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1442 }
1443 other => panic!("expected InvalidValue, got {other:?}"),
1444 }
1445 }
1446
1447 #[test]
1448 fn test_historical_gc_interval_rejects_zero() {
1449 let zero = Value::duration_seconds(0);
1450 match ConfigKey::HistoricalGcInterval.accept(zero).unwrap_err() {
1451 AcceptError::InvalidValue(reason) => {
1452 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1453 }
1454 other => panic!("expected InvalidValue, got {other:?}"),
1455 }
1456 }
1457}