1use std::{fmt, str::FromStr};
5
6use reifydb_runtime::version_epoch::BUCKET_WIDTH;
7use reifydb_value::value::{Value, duration::Duration, value_type::ValueType};
8
9use crate::{common::CommitVersion, default};
10
11#[derive(Debug, Clone, PartialEq, Eq)]
12pub enum AcceptError {
13 TypeMismatch {
14 expected: Vec<ValueType>,
15 actual: ValueType,
16 },
17
18 InvalidValue(String),
19}
20
21impl fmt::Display for AcceptError {
22 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
23 match self {
24 Self::TypeMismatch {
25 expected,
26 actual,
27 } => {
28 write!(f, "expected one of {:?}, got {:?}", expected, actual)
29 }
30 Self::InvalidValue(reason) => write!(f, "{reason}"),
31 }
32 }
33}
34
35#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
36pub enum ConfigProfile {
37 Production,
38 Testing,
39}
40
41pub fn active_config_profile() -> ConfigProfile {
42 if cfg!(feature = "testing") {
43 ConfigProfile::Testing
44 } else {
45 ConfigProfile::Production
46 }
47}
48
49#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
50pub enum ConfigKey {
51 OracleWindowSize,
52 QueryRowBatchSize,
53 QueryMemoryLimit,
54 RetentionEvictInterval,
55 RetentionEvictBatchSize,
56 RetentionEvictMaxBatchesPerTick,
57 EpochBucketInterval,
58 RetentionStartupGrace,
59 MaxRetentionHorizonFloor,
60 HistoricalGcBatchSize,
61 HistoricalGcInterval,
62 CdcTtlDuration,
63 CdcTtlScanInterval,
64 CdcTtlScanBatchSize,
65 CdcWalAutocheckpoint,
66 CdcCommitBufferBytes,
67 CdcBlockCutBytes,
68 CdcReadBufferBytes,
69 MultiPointBufferShardBytes,
70 MultiRangeBufferShardBytes,
71 OperatorRangeTierBytes,
72 MultiPointBufferShards,
73 MultiRangeBufferShards,
74 MultiFlushInterval,
75 MultiFlushBudgetBytes,
76 MultiWalAutocheckpoint,
77 OperatorResidentBudget,
78 OperatorDirtyBudget,
79 OperatorFlushSlice,
80 OperatorFlushInterval,
81 OperatorWalAutocheckpoint,
82 FlowTick,
83 FlowSampleInterval,
84 FlowBacklogMemoryLimit,
85 FlowPullBatchBytes,
86 FlowLoadBatchBytes,
87 CdcConsumeWaitTimeout,
88 FlowJoinProbeBlockSize,
89 ThreadsAsync,
90 ThreadsCoordination,
91 ThreadsFlow,
92 ThreadsTask,
93 ThreadsCompute,
94 ThreadsMaintenance,
95 SubscriptionWorkerThreads,
96 MetricsFlushInterval,
97 MetricsSampleInterval,
98 MetricsSnapshotInterval,
99 QueueLeaseReapInterval,
100 QueueLeaseReapBatchSize,
101 QueueRetentionInterval,
102 QueueRetentionBatchSize,
103}
104
105impl ConfigKey {
106 pub fn all() -> &'static [Self] {
107 &[
108 Self::OracleWindowSize,
109 Self::QueryRowBatchSize,
110 Self::QueryMemoryLimit,
111 Self::RetentionEvictInterval,
112 Self::RetentionEvictBatchSize,
113 Self::RetentionEvictMaxBatchesPerTick,
114 Self::EpochBucketInterval,
115 Self::RetentionStartupGrace,
116 Self::MaxRetentionHorizonFloor,
117 Self::HistoricalGcBatchSize,
118 Self::HistoricalGcInterval,
119 Self::CdcTtlDuration,
120 Self::CdcTtlScanInterval,
121 Self::CdcTtlScanBatchSize,
122 Self::CdcWalAutocheckpoint,
123 Self::CdcCommitBufferBytes,
124 Self::CdcBlockCutBytes,
125 Self::CdcReadBufferBytes,
126 Self::MultiPointBufferShardBytes,
127 Self::MultiRangeBufferShardBytes,
128 Self::OperatorRangeTierBytes,
129 Self::MultiPointBufferShards,
130 Self::MultiRangeBufferShards,
131 Self::MultiFlushInterval,
132 Self::MultiFlushBudgetBytes,
133 Self::MultiWalAutocheckpoint,
134 Self::OperatorResidentBudget,
135 Self::OperatorDirtyBudget,
136 Self::OperatorFlushSlice,
137 Self::OperatorFlushInterval,
138 Self::OperatorWalAutocheckpoint,
139 Self::FlowTick,
140 Self::FlowSampleInterval,
141 Self::FlowBacklogMemoryLimit,
142 Self::FlowPullBatchBytes,
143 Self::FlowLoadBatchBytes,
144 Self::CdcConsumeWaitTimeout,
145 Self::FlowJoinProbeBlockSize,
146 Self::ThreadsAsync,
147 Self::ThreadsCoordination,
148 Self::ThreadsFlow,
149 Self::ThreadsTask,
150 Self::ThreadsCompute,
151 Self::ThreadsMaintenance,
152 Self::SubscriptionWorkerThreads,
153 Self::MetricsFlushInterval,
154 Self::MetricsSampleInterval,
155 Self::MetricsSnapshotInterval,
156 Self::QueueLeaseReapInterval,
157 Self::QueueLeaseReapBatchSize,
158 Self::QueueRetentionInterval,
159 Self::QueueRetentionBatchSize,
160 ]
161 }
162
163 fn duration_or_none(duration: Option<Duration>) -> Value {
164 match duration {
165 Some(duration) => Value::Duration(duration),
166 None => Value::None {
167 inner: ValueType::Duration,
168 },
169 }
170 }
171
172 pub fn default_value(&self) -> Value {
173 match active_config_profile() {
174 ConfigProfile::Production => self.production_value(),
175 ConfigProfile::Testing => self.testing_value(),
176 }
177 }
178
179 pub fn production_value(&self) -> Value {
180 match self {
181 Self::OracleWindowSize => Value::Uint8(default::query::ORACLE_WINDOW_SIZE),
182 Self::QueryRowBatchSize => Value::Uint2(default::query::ROW_BATCH_SIZE),
183 Self::QueryMemoryLimit => Value::Uint8(default::query::MEMORY_LIMIT.as_bytes()),
184 Self::RetentionEvictInterval => Value::Duration(default::retention::EVICT_INTERVAL),
185 Self::RetentionEvictBatchSize => Value::Uint8(default::retention::EVICT_BATCH_SIZE),
186 Self::RetentionEvictMaxBatchesPerTick => {
187 Value::Uint8(default::retention::EVICT_MAX_BATCHES_PER_TICK)
188 }
189 Self::EpochBucketInterval => Value::Duration(default::retention::EPOCH_BUCKET_INTERVAL),
190 Self::RetentionStartupGrace => Value::Duration(default::retention::STARTUP_GRACE),
191 Self::MaxRetentionHorizonFloor => Value::Duration(default::retention::MAX_HORIZON_FLOOR),
192 Self::HistoricalGcBatchSize => Value::Uint8(default::retention::HISTORICAL_GC_BATCH_SIZE),
193 Self::HistoricalGcInterval => Value::Duration(default::retention::HISTORICAL_GC_INTERVAL),
194 Self::CdcTtlDuration => Self::duration_or_none(default::cdc::TTL),
195 Self::CdcTtlScanInterval => Value::Duration(default::cdc::TTL_SCAN_INTERVAL),
196 Self::CdcTtlScanBatchSize => Value::Uint8(default::cdc::TTL_SCAN_BATCH_SIZE),
197 Self::CdcWalAutocheckpoint => Value::Uint8(default::cdc::WAL_AUTOCHECKPOINT_PAGES),
198 Self::CdcCommitBufferBytes => Value::Uint8(default::cdc::COMMIT_BUFFER.as_bytes()),
199 Self::CdcBlockCutBytes => Value::Uint8(default::cdc::BLOCK_CUT.as_bytes()),
200 Self::CdcReadBufferBytes => Value::Uint8(default::cdc::READ_BUFFER.as_bytes()),
201 Self::MultiPointBufferShardBytes => {
202 Value::Uint8(default::store::MULTI_POINT_BUFFER_SHARD.as_bytes())
203 }
204 Self::MultiRangeBufferShardBytes => {
205 Value::Uint8(default::store::MULTI_RANGE_BUFFER_SHARD.as_bytes())
206 }
207 Self::OperatorRangeTierBytes => Value::Uint8(default::store::OPERATOR_RANGE_TIER.as_bytes()),
208 Self::MultiPointBufferShards => Value::Uint2(default::store::MULTI_POINT_BUFFER_SHARDS),
209 Self::MultiRangeBufferShards => Value::Uint2(default::store::MULTI_RANGE_BUFFER_SHARDS),
210 Self::MultiFlushInterval => Value::Duration(default::store::MULTI_FLUSH_INTERVAL),
211 Self::MultiFlushBudgetBytes => Value::Uint8(default::store::MULTI_FLUSH_BUDGET.as_bytes()),
212 Self::MultiWalAutocheckpoint => Value::Uint8(default::store::MULTI_WAL_AUTOCHECKPOINT_PAGES),
213 Self::OperatorResidentBudget => {
214 Value::Uint8(default::store::OPERATOR_RESIDENT_BUDGET.as_bytes())
215 }
216 Self::OperatorDirtyBudget => Value::Uint8(default::store::OPERATOR_DIRTY_BUDGET.as_bytes()),
217 Self::OperatorFlushSlice => Value::Uint8(default::store::OPERATOR_FLUSH_SLICE.as_bytes()),
218 Self::OperatorFlushInterval => Value::Duration(default::store::OPERATOR_FLUSH_INTERVAL),
219 Self::OperatorWalAutocheckpoint => {
220 Value::Uint8(default::store::OPERATOR_WAL_AUTOCHECKPOINT_PAGES)
221 }
222 Self::FlowTick => Value::Duration(default::flow::TICK),
223 Self::FlowSampleInterval => Value::Duration(default::flow::SAMPLE_INTERVAL),
224 Self::FlowBacklogMemoryLimit => Value::Uint8(default::flow::BACKLOG_MEMORY_LIMIT.as_bytes()),
225 Self::FlowPullBatchBytes => Value::Uint8(default::flow::PULL_BATCH.as_bytes()),
226 Self::FlowLoadBatchBytes => Value::Uint8(default::flow::LOAD_BATCH.as_bytes()),
227 Self::CdcConsumeWaitTimeout => Value::Duration(default::cdc::CONSUME_WAIT_TIMEOUT),
228 Self::FlowJoinProbeBlockSize => Value::Uint8(default::flow::JOIN_PROBE_BLOCK_SIZE),
229 Self::ThreadsAsync => Value::Uint2(default::threads::ASYNC),
230 Self::ThreadsCoordination => Value::Uint2(default::threads::COORDINATION),
231 Self::ThreadsFlow => Value::Uint2(default::threads::FLOW),
232 Self::ThreadsTask => Value::Uint2(default::threads::TASK),
233 Self::ThreadsCompute => Value::Uint2(default::threads::COMPUTE),
234 Self::ThreadsMaintenance => Value::Uint2(default::threads::MAINTENANCE),
235 Self::SubscriptionWorkerThreads => Value::Uint2(default::threads::SUBSCRIPTION_WORKER),
236 Self::MetricsFlushInterval => Value::Duration(default::metrics::FLUSH_INTERVAL),
237 Self::MetricsSampleInterval => Value::Duration(default::metrics::SAMPLE_INTERVAL),
238 Self::MetricsSnapshotInterval => Self::duration_or_none(default::metrics::SNAPSHOT_INTERVAL),
239 Self::QueueLeaseReapInterval => Value::Duration(default::queue::LEASE_REAP_INTERVAL),
240 Self::QueueLeaseReapBatchSize => Value::Uint8(default::queue::LEASE_REAP_BATCH_SIZE),
241 Self::QueueRetentionInterval => Value::Duration(default::queue::RETENTION_INTERVAL),
242 Self::QueueRetentionBatchSize => Value::Uint8(default::queue::RETENTION_BATCH_SIZE),
243 }
244 }
245
246 pub fn testing_value(&self) -> Value {
247 match self {
248 Self::OracleWindowSize => Value::Uint8(default::query::ORACLE_WINDOW_SIZE_TESTING),
249 Self::QueryRowBatchSize => Value::Uint2(default::query::ROW_BATCH_SIZE_TESTING),
250 Self::QueryMemoryLimit => Value::Uint8(default::query::MEMORY_LIMIT_TESTING.as_bytes()),
251 Self::RetentionEvictInterval => Value::Duration(default::retention::EVICT_INTERVAL_TESTING),
252 Self::RetentionEvictBatchSize => Value::Uint8(default::retention::EVICT_BATCH_SIZE_TESTING),
253 Self::RetentionEvictMaxBatchesPerTick => {
254 Value::Uint8(default::retention::EVICT_MAX_BATCHES_PER_TICK_TESTING)
255 }
256 Self::EpochBucketInterval => Value::Duration(default::retention::EPOCH_BUCKET_INTERVAL_TESTING),
257 Self::RetentionStartupGrace => Value::Duration(default::retention::STARTUP_GRACE_TESTING),
258 Self::MaxRetentionHorizonFloor => {
259 Value::Duration(default::retention::MAX_HORIZON_FLOOR_TESTING)
260 }
261 Self::HistoricalGcBatchSize => {
262 Value::Uint8(default::retention::HISTORICAL_GC_BATCH_SIZE_TESTING)
263 }
264 Self::HistoricalGcInterval => {
265 Value::Duration(default::retention::HISTORICAL_GC_INTERVAL_TESTING)
266 }
267 Self::CdcTtlDuration => Self::duration_or_none(default::cdc::TTL_TESTING),
268 Self::CdcTtlScanInterval => Value::Duration(default::cdc::TTL_SCAN_INTERVAL_TESTING),
269 Self::CdcTtlScanBatchSize => Value::Uint8(default::cdc::TTL_SCAN_BATCH_SIZE_TESTING),
270 Self::CdcWalAutocheckpoint => Value::Uint8(default::cdc::WAL_AUTOCHECKPOINT_PAGES_TESTING),
271 Self::CdcCommitBufferBytes => Value::Uint8(default::cdc::COMMIT_BUFFER_TESTING.as_bytes()),
272 Self::CdcBlockCutBytes => Value::Uint8(default::cdc::BLOCK_CUT_TESTING.as_bytes()),
273 Self::CdcReadBufferBytes => Value::Uint8(default::cdc::READ_BUFFER_TESTING.as_bytes()),
274 Self::MultiPointBufferShardBytes => {
275 Value::Uint8(default::store::MULTI_POINT_BUFFER_SHARD_TESTING.as_bytes())
276 }
277 Self::MultiRangeBufferShardBytes => {
278 Value::Uint8(default::store::MULTI_RANGE_BUFFER_SHARD_TESTING.as_bytes())
279 }
280 Self::OperatorRangeTierBytes => {
281 Value::Uint8(default::store::OPERATOR_RANGE_TIER_TESTING.as_bytes())
282 }
283 Self::MultiPointBufferShards => Value::Uint2(default::store::MULTI_POINT_BUFFER_SHARDS_TESTING),
284 Self::MultiRangeBufferShards => Value::Uint2(default::store::MULTI_RANGE_BUFFER_SHARDS_TESTING),
285 Self::MultiFlushInterval => Value::Duration(default::store::MULTI_FLUSH_INTERVAL_TESTING),
286 Self::MultiFlushBudgetBytes => {
287 Value::Uint8(default::store::MULTI_FLUSH_BUDGET_TESTING.as_bytes())
288 }
289 Self::MultiWalAutocheckpoint => {
290 Value::Uint8(default::store::MULTI_WAL_AUTOCHECKPOINT_PAGES_TESTING)
291 }
292 Self::OperatorResidentBudget => {
293 Value::Uint8(default::store::OPERATOR_RESIDENT_BUDGET_TESTING.as_bytes())
294 }
295 Self::OperatorDirtyBudget => {
296 Value::Uint8(default::store::OPERATOR_DIRTY_BUDGET_TESTING.as_bytes())
297 }
298 Self::OperatorFlushSlice => {
299 Value::Uint8(default::store::OPERATOR_FLUSH_SLICE_TESTING.as_bytes())
300 }
301 Self::OperatorFlushInterval => Value::Duration(default::store::OPERATOR_FLUSH_INTERVAL_TESTING),
302 Self::OperatorWalAutocheckpoint => {
303 Value::Uint8(default::store::OPERATOR_WAL_AUTOCHECKPOINT_PAGES_TESTING)
304 }
305 Self::FlowTick => Value::Duration(default::flow::TICK_TESTING),
306 Self::FlowSampleInterval => Value::Duration(default::flow::SAMPLE_INTERVAL_TESTING),
307 Self::FlowBacklogMemoryLimit => {
308 Value::Uint8(default::flow::BACKLOG_MEMORY_LIMIT_TESTING.as_bytes())
309 }
310 Self::FlowPullBatchBytes => Value::Uint8(default::flow::PULL_BATCH_TESTING.as_bytes()),
311 Self::FlowLoadBatchBytes => Value::Uint8(default::flow::LOAD_BATCH_TESTING.as_bytes()),
312 Self::CdcConsumeWaitTimeout => Value::Duration(default::cdc::CONSUME_WAIT_TIMEOUT_TESTING),
313 Self::FlowJoinProbeBlockSize => Value::Uint8(default::flow::JOIN_PROBE_BLOCK_SIZE_TESTING),
314 Self::ThreadsAsync => Value::Uint2(default::threads::ASYNC_TESTING),
315 Self::ThreadsCoordination => Value::Uint2(default::threads::COORDINATION_TESTING),
316 Self::ThreadsFlow => Value::Uint2(default::threads::FLOW_TESTING),
317 Self::ThreadsTask => Value::Uint2(default::threads::TASK_TESTING),
318 Self::ThreadsCompute => Value::Uint2(default::threads::COMPUTE_TESTING),
319 Self::ThreadsMaintenance => Value::Uint2(default::threads::MAINTENANCE_TESTING),
320 Self::SubscriptionWorkerThreads => Value::Uint2(default::threads::SUBSCRIPTION_WORKER_TESTING),
321 Self::MetricsFlushInterval => Value::Duration(default::metrics::FLUSH_INTERVAL_TESTING),
322 Self::MetricsSampleInterval => Value::Duration(default::metrics::SAMPLE_INTERVAL_TESTING),
323 Self::MetricsSnapshotInterval => {
324 Self::duration_or_none(default::metrics::SNAPSHOT_INTERVAL_TESTING)
325 }
326 Self::QueueLeaseReapInterval => Value::Duration(default::queue::LEASE_REAP_INTERVAL_TESTING),
327 Self::QueueLeaseReapBatchSize => Value::Uint8(default::queue::LEASE_REAP_BATCH_SIZE_TESTING),
328 Self::QueueRetentionInterval => Value::Duration(default::queue::RETENTION_INTERVAL_TESTING),
329 Self::QueueRetentionBatchSize => Value::Uint8(default::queue::RETENTION_BATCH_SIZE_TESTING),
330 }
331 }
332
333 pub fn description(&self) -> &'static str {
334 match self {
335 Self::OracleWindowSize => "Number of transactions per conflict-detection window.",
336 Self::QueryRowBatchSize => {
337 "Number of rows produced per batch by query / DML pipeline operators."
338 }
339 Self::QueryMemoryLimit => {
340 "Maximum bytes a single query may buffer in memory across its blocking operators (joins, sort, top k, distinct) and its accumulated result. A query that would exceed this fails with QUERY_006 instead of growing without bound. Read fresh for each query, so changes take effect immediately."
341 }
342 Self::RetentionEvictInterval => {
343 "How often the retention evictor scans objects with a row TTL for expired rows."
344 }
345 Self::RetentionEvictBatchSize => {
346 "Max rows examined (and thus evicted) per transaction during a retention eviction tick."
347 }
348 Self::RetentionEvictMaxBatchesPerTick => {
349 "Upper bound on eviction transactions per retention tick. Caps how long one tick can run when draining a backlog; remaining work resumes on the next tick."
350 }
351 Self::EpochBucketInterval => {
352 "Wall-clock width of one durable version-epoch bucket. The epoch log persists at most one \
353 (bucket, commit version) sample per bucket, and those samples are what let TTLs resolve a \
354 cutoff after a restart. Smaller buckets give finer expiry resolution at the cost of more \
355 persisted samples over the retention horizon."
356 }
357 Self::RetentionStartupGrace => {
358 "How long after startup every retention executor computes cutoffs but deletes nothing. A \
359 process restarted after a long downtime wakes with a large expired backlog; the grace \
360 period plus per-class budgets drain it over many ticks instead of one mass eviction."
361 }
362 Self::MaxRetentionHorizonFloor => {
363 "Lower bound on the retained version-epoch horizon. The horizon is the longest declared \
364 TTL in the catalog, never less than this floor; epoch samples older than the horizon are \
365 pruned. A TTL longer than the horizon could not resolve a cutoff, so it is rejected at \
366 declaration time rather than silently never expiring."
367 }
368 Self::HistoricalGcBatchSize => {
369 "Max historical (key, version) pairs scanned per object per historical GC tick."
370 }
371 Self::HistoricalGcInterval => {
372 "How often the historical-version GC actor sweeps __historical for versions older than the oracle read watermark."
373 }
374 Self::CdcTtlDuration => {
375 "Maximum age of CDC entries before eviction. When unset, CDC is retained forever; \
376 when set, must be > 0 and entries older than this duration are evicted regardless \
377 of consumer state."
378 }
379 Self::CdcTtlScanInterval => {
380 "How often the CDC producer actor scans for and evicts expired CDC entries."
381 }
382 Self::CdcTtlScanBatchSize => {
383 "Max CDC entries deleted per transaction during a CDC TTL eviction tick."
384 }
385 Self::CdcWalAutocheckpoint => {
386 "WAL frame threshold (SQLite wal_autocheckpoint PRAGMA) for the CDC log's SQLite tier. \
387 CDC has no explicit checkpoint of its own, so this is the sole control over how often \
388 cdc.db's WAL is checkpointed into the main file. Higher values checkpoint less often with \
389 a larger WAL; since CDC is written on the commit path, this also bounds how often a commit \
390 pays an inline auto-checkpoint. Read once at boot; changing it requires a restart."
391 }
392 Self::CdcCommitBufferBytes => {
393 "Upper bound on unflushed CDC bytes held in the commit buffer. A writer that would push the \
394 buffer past this stalls until the flusher drains it, so this is the back-pressure point \
395 between the commit path and the persistent tier. Read once at boot."
396 }
397 Self::CdcBlockCutBytes => {
398 "Target size of one CDC block. The commit buffer cuts a block once its pending bytes reach \
399 this, and that block is the unit of flush, of persistent storage, and of read-cache \
400 residency. Larger blocks compress better but coarsen retention, which drops whole blocks. \
401 Read once at boot."
402 }
403 Self::CdcReadBufferBytes => {
404 "Resident byte budget for the CDC read cache of decoded blocks, split evenly across its \
405 shards. None disables the cache outright, so every miss below the commit buffer decodes a \
406 block straight from the persistent tier. Read once at boot."
407 }
408 Self::MultiPointBufferShardBytes => {
409 "Resident byte budget for each shard of the multi-version point cache; total cache memory is \
410 this value times the shard count. None disables the cache outright, so \
411 every point read that misses the commit buffer goes to the persistent tier. Read once at boot; changing it \
412 requires a restart."
413 }
414 Self::MultiRangeBufferShardBytes => {
415 "Resident byte budget for each shard of the multi-version range cache; total cache memory is \
416 this value times the shard count. None disables the cache outright, so \
417 every multi-version range scan goes to the persistent tier. Read once at boot; changing it \
418 requires a restart."
419 }
420 Self::OperatorRangeTierBytes => {
421 "Resident byte budget for one tier of the operator-state range cache. Every cached keyspace \
422 carries its own tier, so total cache memory is this value times the number of cached \
423 keyspaces. None disables the cache outright, so every operator range scan goes to the \
424 persistent tier. Read once at boot; changing it requires a restart."
425 }
426 Self::MultiPointBufferShards => {
427 "Number of lock-striped shards in the multi-version point cache. Each shard carries its own \
428 byte budget, so raising this raises total cache memory proportionally rather \
429 than dividing a fixed pot. Must be >= 1. Read once at boot; changing it \
430 requires a restart."
431 }
432 Self::MultiRangeBufferShards => {
433 "Number of lock-striped shards in the multi-version range cache. Each shard carries its own \
434 byte budget, so raising this raises total cache memory proportionally rather \
435 than dividing a fixed pot. Must be >= 1. Read once at boot; changing it \
436 requires a restart."
437 }
438 Self::MultiFlushInterval => {
439 "How often the persistent-flush actor drains the in-memory commit buffer into the multi \
440 store's SQLite tier. Longer intervals coalesce more writes per flush - a larger WAL - at \
441 the cost of more resident commit-buffer memory and a longer window before data is \
442 materialized in the persistent file. Read once at boot; changing it requires a restart."
443 }
444 Self::MultiFlushBudgetBytes => {
445 "Maximum bytes of buffered entries the persistent-flush class moves from the commit \
446 buffer to the SQLite tier in one slice. Bounds how long a single flush holds the lane, \
447 so a large backlog drains across ticks instead of stalling every other retention class \
448 behind it."
449 }
450 Self::MultiWalAutocheckpoint => {
451 "WAL frame threshold for the multi store's SQLite tier: sets the SQLite \
452 wal_autocheckpoint PRAGMA that governs when SQLite folds the WAL back into the main \
453 database. Higher values checkpoint less often with a larger WAL, reducing checkpoint \
454 I/O; lower values keep the WAL small at the cost of more frequent checkpoints. Read once \
455 at boot; changing it requires a restart."
456 }
457 Self::OperatorResidentBudget => {
458 "Byte ceiling on resident operator state. Eviction returns clean state to this limit \
459 once it is exceeded. Dirty state is never evicted, so a tier holding mostly unwritten \
460 state stays above the limit until a flush turns it clean; OPERATOR_DIRTY_BUDGET bounds \
461 that, and OPERATOR_FLUSH_SLICE sizes the individual commits a flush writes. Read once \
462 at boot; changing it requires a restart."
463 }
464 Self::OperatorDirtyBudget => {
465 "Byte ceiling on unwritten operator state. A flush is triggered once dirty resident state \
466 exceeds this, which bounds both how much memory dirty state may hold and how much one \
467 drain has to write. It defaults to the resident budget, so the flush interval is the \
468 normal trigger and this stays a backstop for a workload that dirties state faster than \
469 the interval anticipates. Read once at boot; changing it requires a restart."
470 }
471 Self::OperatorFlushSlice => {
472 "Byte target for a single operator-state flush transaction. The drain stops taking work at \
473 the first group boundary past this value, so one commit can exceed it by the size of that \
474 group. Larger values mean fewer and longer commits, and every operator-state write blocks \
475 for the length of a commit. Read once at boot; changing it requires a restart."
476 }
477 Self::OperatorFlushInterval => {
478 "How often the operator-state flush actor drains dirty resident state into the operator \
479 store's SQLite tier. Operator state stays resident after a flush and is freed \
480 separately by eviction, so memory pressure alone can leave state unflushed \
481 indefinitely, which holds the durable checkpoint back and with it the CDC pinning \
482 watermark. Read once at boot; changing it requires a restart."
483 }
484 Self::OperatorWalAutocheckpoint => {
485 "WAL frame threshold for the operator store's SQLite tier: sets the SQLite \
486 wal_autocheckpoint PRAGMA that governs when SQLite folds the WAL back into the main \
487 database. Higher values checkpoint less often with a larger WAL, reducing checkpoint \
488 I/O; lower values keep the WAL small at the cost of more frequent checkpoints. Read \
489 once at boot; changing it requires a restart."
490 }
491 Self::FlowTick => {
492 "How often the deferred and transactional flow tick coordinators wake up to dispatch \
493 due flows."
494 }
495 Self::FlowSampleInterval => {
496 "How often each flow actor samples its operators' approximate memory into the \
497 system::metrics::runtime::memory samples (scope operator::N). Runs on the operator's \
498 own thread, off the apply path. When none, operator sampling is disabled entirely; when \
499 set, must be > 0."
500 }
501 Self::FlowBacklogMemoryLimit => {
502 "Byte ceiling of the shared in-memory backlog of decoded CDC entries that feeds flow \
503 consumers. Producer-fed at commit granularity; entries below every flow's cursor are \
504 dropped eagerly and the lowest versions are evicted first once the ceiling is exceeded, \
505 at which point a flow that far behind reloads from disk through the catch-up loader. \
506 Because payload rows are shared, the tally is an upper bound of unique memory."
507 }
508 Self::FlowPullBatchBytes => {
509 "Byte budget a flow actor applies per pull from the CDC backlog. A flow that has fallen \
510 behind receives up to this many bytes of decoded changes in one slice, so catch-up is \
511 vectorized instead of per-version."
512 }
513 Self::FlowLoadBatchBytes => {
514 "Byte budget of one catch-up loader read from the CDC log on behalf of flows that are \
515 behind the in-memory backlog. Identical concurrent requests share a single read."
516 }
517 Self::CdcConsumeWaitTimeout => {
518 "Backstop timeout for the CDC consumer's wait for a consume reply from the downstream \
519 consumer. A lost reply would otherwise wedge the poll loop forever; on timeout the \
520 process aborts with a report naming the consumer, the pending work and the batch. \
521 Must be > 0."
522 }
523 Self::FlowJoinProbeBlockSize => {
524 "Number of opposite-side rows a streaming join pulls per block when probing its stored \
525 state. Bounds resident probe memory without dropping matches; smaller trades fewer \
526 resident rows for more scan round-trips."
527 }
528 Self::ThreadsAsync => {
529 "Number of worker threads for the async runtime. Must be >= 1. \
530 Read at boot before the runtime starts; changes require restart."
531 }
532 Self::ThreadsCoordination => {
533 "Number of worker threads for the coordination group (long-lived actors with \
534 tiny high-frequency handlers and periodic background actors); pinned dispatch. \
535 Must be >= 1. Changes require restart."
536 }
537 Self::ThreadsFlow => {
538 "Number of worker threads for the flow group (long-lived heavy-handler actors: \
539 materialized-view flow execution); pinned dispatch. \
540 Must be >= 1. Changes require restart."
541 }
542 Self::ThreadsTask => {
543 "Number of worker threads for the task pool (short-lived work: per-request \
544 actors and one-shot jobs). Must be >= 1. Changes require restart."
545 }
546 Self::ThreadsCompute => {
547 "Number of worker threads for the compute pool (data-parallel work via install(), \
548 never actors). Must be >= 1. Changes require restart."
549 }
550 Self::ThreadsMaintenance => {
551 "Number of worker threads for the maintenance actor pool (lifecycle tasks, operator range \
552 eviction, filter rebuilds). A long slice on one actor holds a thread, so a count of 1 lets \
553 the slowest task delay every other one. Must be >= 1. Changes require restart."
554 }
555 Self::SubscriptionWorkerThreads => {
556 "Number of subscription worker actors that fan out CDC changes to ephemeral \
557 subscriptions in parallel. 0 means auto (size to the system thread pool). Higher values \
558 raise fan-out parallelism for many concurrent subscriptions. Changes require restart."
559 }
560 Self::MetricsFlushInterval => {
561 "How often the metric collector flushes accumulated storage and CDC accounting into the \
562 system::metrics KV store that backs the storage and cdc views. Must be > 0."
563 }
564 Self::MetricsSampleInterval => {
565 "How often the metrics sampler polls every domain, rolls the window and publishes the \
566 system::metrics ::current and ::total caches. Always on; there is no off value, only a \
567 cadence. Must be > 0. Read once at boot; changing it requires a restart."
568 }
569 Self::MetricsSnapshotInterval => {
570 "How often the published ::current reading of every domain is appended to its ::snapshots \
571 series. When none, no snapshot is ever written; when set, must be > 0 and not shorter than \
572 METRICS_SAMPLE_INTERVAL. Read once at boot; changing it requires a restart."
573 }
574 Self::QueueLeaseReapInterval => {
575 "How often the queue reaper scans for leases whose deadline has passed. A dead worker's item cannot be redelivered sooner than this, so it should stay well below any declared lease ttl."
576 }
577 Self::QueueLeaseReapBatchSize => {
578 "Max queue item-state records one reap slice may scan. Bounds the slice on a deep backlog; the scan resumes from its cursor on the next slice."
579 }
580 Self::QueueRetentionInterval => {
581 "How often the queue retention sweeper deletes finished items whose terminal attempt is older than the queue's declared retention.done, and deduplication records past their own ttl."
582 }
583 Self::QueueRetentionBatchSize => {
584 "Max records one queue retention slice may scan across its item and deduplication sweeps. Remaining work drains on the next slice."
585 }
586 }
587 }
588
589 pub fn requires_restart(&self) -> bool {
590 match self {
591 Self::OracleWindowSize => false,
592 Self::QueryRowBatchSize => false,
593 Self::QueryMemoryLimit => false,
594 Self::RetentionEvictInterval => true,
595 Self::RetentionEvictBatchSize => false,
596 Self::RetentionEvictMaxBatchesPerTick => false,
597 Self::EpochBucketInterval => false,
598 Self::RetentionStartupGrace => false,
599 Self::MaxRetentionHorizonFloor => false,
600 Self::HistoricalGcBatchSize => false,
601 Self::HistoricalGcInterval => false,
602 Self::CdcTtlDuration => false,
603 Self::CdcTtlScanInterval => true,
604 Self::CdcTtlScanBatchSize => false,
605 Self::CdcWalAutocheckpoint => true,
606 Self::CdcCommitBufferBytes => true,
607 Self::CdcBlockCutBytes => true,
608 Self::CdcReadBufferBytes => true,
609 Self::MultiPointBufferShardBytes => true,
610 Self::MultiRangeBufferShardBytes => true,
611 Self::OperatorRangeTierBytes => true,
612 Self::MultiPointBufferShards => true,
613 Self::MultiRangeBufferShards => true,
614 Self::MultiFlushInterval => true,
615 Self::MultiFlushBudgetBytes => false,
616 Self::MultiWalAutocheckpoint => true,
617 Self::OperatorResidentBudget => true,
618 Self::OperatorDirtyBudget => true,
619 Self::OperatorFlushSlice => true,
620 Self::OperatorFlushInterval => true,
621 Self::OperatorWalAutocheckpoint => true,
622 Self::FlowTick => false,
623 Self::FlowSampleInterval => false,
624 Self::FlowBacklogMemoryLimit => true,
625 Self::FlowPullBatchBytes => true,
626 Self::FlowLoadBatchBytes => true,
627 Self::CdcConsumeWaitTimeout => false,
628 Self::FlowJoinProbeBlockSize => false,
629 Self::ThreadsAsync => true,
630 Self::ThreadsCoordination => true,
631 Self::ThreadsFlow => true,
632 Self::ThreadsTask => true,
633 Self::ThreadsCompute => true,
634 Self::ThreadsMaintenance => true,
635 Self::SubscriptionWorkerThreads => true,
636 Self::MetricsFlushInterval => false,
637 Self::MetricsSampleInterval => true,
638 Self::MetricsSnapshotInterval => true,
639 Self::QueueLeaseReapInterval => false,
640 Self::QueueLeaseReapBatchSize => false,
641 Self::QueueRetentionInterval => false,
642 Self::QueueRetentionBatchSize => false,
643 }
644 }
645
646 pub fn expected_types(&self) -> &'static [ValueType] {
647 match self {
648 Self::OracleWindowSize => &[ValueType::Uint8],
649 Self::QueryRowBatchSize => &[ValueType::Uint2],
650 Self::QueryMemoryLimit => &[ValueType::Uint8],
651 Self::RetentionEvictInterval => &[ValueType::Duration],
652 Self::RetentionEvictBatchSize => &[ValueType::Uint8],
653 Self::RetentionEvictMaxBatchesPerTick => &[ValueType::Uint8],
654 Self::EpochBucketInterval => &[ValueType::Duration],
655 Self::RetentionStartupGrace => &[ValueType::Duration],
656 Self::MaxRetentionHorizonFloor => &[ValueType::Duration],
657 Self::HistoricalGcBatchSize => &[ValueType::Uint8],
658 Self::HistoricalGcInterval => &[ValueType::Duration],
659 Self::CdcTtlDuration => &[ValueType::Duration],
660 Self::CdcTtlScanInterval => &[ValueType::Duration],
661 Self::CdcTtlScanBatchSize => &[ValueType::Uint8],
662 Self::CdcWalAutocheckpoint => &[ValueType::Uint8],
663 Self::CdcCommitBufferBytes => &[ValueType::Uint8],
664 Self::CdcBlockCutBytes => &[ValueType::Uint8],
665 Self::CdcReadBufferBytes => &[ValueType::Uint8],
666 Self::MultiPointBufferShardBytes => &[ValueType::Uint8],
667 Self::MultiRangeBufferShardBytes => &[ValueType::Uint8],
668 Self::OperatorRangeTierBytes => &[ValueType::Uint8],
669 Self::MultiPointBufferShards => &[ValueType::Uint2],
670 Self::MultiRangeBufferShards => &[ValueType::Uint2],
671 Self::MultiFlushInterval => &[ValueType::Duration],
672 Self::MultiFlushBudgetBytes => &[ValueType::Uint8],
673 Self::MultiWalAutocheckpoint => &[ValueType::Uint8],
674 Self::OperatorResidentBudget => &[ValueType::Uint8],
675 Self::OperatorDirtyBudget => &[ValueType::Uint8],
676 Self::OperatorFlushSlice => &[ValueType::Uint8],
677 Self::OperatorFlushInterval => &[ValueType::Duration],
678 Self::OperatorWalAutocheckpoint => &[ValueType::Uint8],
679 Self::FlowTick => &[ValueType::Duration],
680 Self::FlowSampleInterval => &[ValueType::Duration],
681 Self::FlowBacklogMemoryLimit => &[ValueType::Uint8],
682 Self::FlowPullBatchBytes => &[ValueType::Uint8],
683 Self::FlowLoadBatchBytes => &[ValueType::Uint8],
684 Self::CdcConsumeWaitTimeout => &[ValueType::Duration],
685 Self::FlowJoinProbeBlockSize => &[ValueType::Uint8],
686 Self::ThreadsAsync => &[ValueType::Uint2],
687 Self::ThreadsCoordination => &[ValueType::Uint2],
688 Self::ThreadsFlow => &[ValueType::Uint2],
689 Self::ThreadsTask => &[ValueType::Uint2],
690 Self::ThreadsCompute => &[ValueType::Uint2],
691 Self::ThreadsMaintenance => &[ValueType::Uint2],
692 Self::SubscriptionWorkerThreads => &[ValueType::Uint2],
693 Self::MetricsFlushInterval => &[ValueType::Duration],
694 Self::MetricsSampleInterval => &[ValueType::Duration],
695 Self::MetricsSnapshotInterval => &[ValueType::Duration],
696 Self::QueueLeaseReapInterval => &[ValueType::Duration],
697 Self::QueueLeaseReapBatchSize => &[ValueType::Uint8],
698 Self::QueueRetentionInterval => &[ValueType::Duration],
699 Self::QueueRetentionBatchSize => &[ValueType::Uint8],
700 }
701 }
702
703 pub fn is_optional(&self) -> bool {
704 match self {
705 Self::OracleWindowSize => false,
706 Self::QueryRowBatchSize => false,
707 Self::QueryMemoryLimit => false,
708 Self::RetentionEvictInterval => false,
709 Self::RetentionEvictBatchSize => false,
710 Self::RetentionEvictMaxBatchesPerTick => false,
711 Self::EpochBucketInterval => false,
712 Self::RetentionStartupGrace => false,
713 Self::MaxRetentionHorizonFloor => false,
714 Self::HistoricalGcBatchSize => false,
715 Self::HistoricalGcInterval => false,
716 Self::CdcTtlDuration => true,
717 Self::CdcTtlScanInterval => false,
718 Self::CdcTtlScanBatchSize => false,
719 Self::CdcWalAutocheckpoint => false,
720 Self::CdcCommitBufferBytes => false,
721 Self::CdcBlockCutBytes => false,
722 Self::CdcReadBufferBytes => true,
723 Self::MultiPointBufferShardBytes => true,
724 Self::MultiRangeBufferShardBytes => true,
725 Self::OperatorRangeTierBytes => true,
726 Self::MultiPointBufferShards => false,
727 Self::MultiRangeBufferShards => false,
728 Self::MultiFlushInterval => false,
729 Self::MultiFlushBudgetBytes => false,
730 Self::MultiWalAutocheckpoint => false,
731 Self::OperatorResidentBudget => false,
732 Self::OperatorDirtyBudget => false,
733 Self::OperatorFlushSlice => false,
734 Self::OperatorFlushInterval => false,
735 Self::OperatorWalAutocheckpoint => false,
736 Self::FlowTick => false,
737 Self::FlowSampleInterval => true,
738 Self::FlowBacklogMemoryLimit => false,
739 Self::FlowPullBatchBytes => false,
740 Self::FlowLoadBatchBytes => false,
741 Self::CdcConsumeWaitTimeout => false,
742 Self::FlowJoinProbeBlockSize => false,
743 Self::ThreadsAsync => false,
744 Self::ThreadsCoordination => false,
745 Self::ThreadsFlow => false,
746 Self::ThreadsTask => false,
747 Self::ThreadsCompute => false,
748 Self::ThreadsMaintenance => false,
749 Self::SubscriptionWorkerThreads => false,
750 Self::MetricsFlushInterval => false,
751 Self::MetricsSampleInterval => false,
752 Self::MetricsSnapshotInterval => true,
753 Self::QueueLeaseReapInterval => false,
754 Self::QueueLeaseReapBatchSize => false,
755 Self::QueueRetentionInterval => false,
756 Self::QueueRetentionBatchSize => false,
757 }
758 }
759
760 fn validate_canonical(&self, value: &Value) -> Result<(), String> {
761 match self {
762 Self::CdcTtlDuration => match value {
763 Value::None {
764 ..
765 } => Ok(()),
766 Value::Duration(d) => {
767 if d.is_positive() {
768 Ok(())
769 } else {
770 Err("CDC_TTL_DURATION must be greater than zero".to_string())
771 }
772 }
773 _ => Ok(()),
774 },
775 Self::EpochBucketInterval => match value {
776 Value::Duration(d) if !d.is_positive() => {
777 Err("EPOCH_BUCKET_INTERVAL must be greater than zero".to_string())
778 }
779 Value::Duration(d) if d.to_std().as_secs() < BUCKET_WIDTH.seconds() => Err(format!(
780 "EPOCH_BUCKET_INTERVAL must be at least {}s: the version epoch resolves cutoffs at \
781 second granularity, so a shorter bucket truncates to zero and silently disables \
782 coarse compaction",
783 BUCKET_WIDTH.seconds()
784 )),
785 _ => Ok(()),
786 },
787 Self::RetentionStartupGrace => match value {
788 Value::Duration(d) if d.is_negative() => {
789 Err("RETENTION_STARTUP_GRACE must not be negative".to_string())
790 }
791 _ => Ok(()),
792 },
793 Self::MaxRetentionHorizonFloor => match value {
794 Value::Duration(d) if !d.is_positive() => {
795 Err("MAX_RETENTION_HORIZON_FLOOR must be greater than zero".to_string())
796 }
797 _ => Ok(()),
798 },
799 Self::QueryRowBatchSize => match value {
800 Value::Uint2(0) => Err("QUERY_ROW_BATCH_SIZE must be greater than zero".to_string()),
801 _ => Ok(()),
802 },
803 Self::QueryMemoryLimit => match value {
804 Value::Uint8(0) => Err("QUERY_MEMORY_LIMIT must be greater than zero".to_string()),
805 _ => Ok(()),
806 },
807 Self::FlowBacklogMemoryLimit => match value {
808 Value::Uint8(0) => {
809 Err("FLOW_BACKLOG_MEMORY_LIMIT must be greater than zero".to_string())
810 }
811 _ => Ok(()),
812 },
813 Self::FlowPullBatchBytes => match value {
814 Value::Uint8(0) => Err("FLOW_PULL_BATCH_BYTES must be greater than zero".to_string()),
815 _ => Ok(()),
816 },
817 Self::FlowLoadBatchBytes => match value {
818 Value::Uint8(0) => Err("FLOW_LOAD_BATCH_BYTES must be greater than zero".to_string()),
819 _ => Ok(()),
820 },
821 Self::MultiPointBufferShardBytes => match value {
822 Value::Uint8(0) => Err(
823 "MULTI_POINT_BUFFER_SHARD_BYTES must be greater than zero; use none to disable the point cache"
824 .to_string(),
825 ),
826 _ => Ok(()),
827 },
828 Self::MultiRangeBufferShardBytes => match value {
829 Value::Uint8(0) => Err(
830 "MULTI_RANGE_BUFFER_SHARD_BYTES must be greater than zero; use none to disable the range cache"
831 .to_string(),
832 ),
833 _ => Ok(()),
834 },
835 Self::OperatorRangeTierBytes => match value {
836 Value::Uint8(0) => Err(
837 "OPERATOR_RANGE_TIER_BYTES must be greater than zero; use none to disable the range cache"
838 .to_string(),
839 ),
840 _ => Ok(()),
841 },
842 Self::MultiPointBufferShards => match value {
843 Value::Uint2(0) => Err("MULTI_POINT_BUFFER_SHARDS must be greater than zero".to_string()),
844 _ => Ok(()),
845 },
846 Self::MultiRangeBufferShards => match value {
847 Value::Uint2(0) => Err("MULTI_RANGE_BUFFER_SHARDS must be greater than zero".to_string()),
848 _ => Ok(()),
849 },
850 Self::CdcCommitBufferBytes => match value {
851 Value::Uint8(0) => Err("CDC_COMMIT_BUFFER_BYTES must be greater than zero".to_string()),
852 _ => Ok(()),
853 },
854 Self::CdcBlockCutBytes => match value {
855 Value::Uint8(0) => Err("CDC_BLOCK_CUT_BYTES must be greater than zero".to_string()),
856 _ => Ok(()),
857 },
858 Self::CdcReadBufferBytes => match value {
859 Value::Uint8(0) => Err(
860 "CDC_READ_BUFFER_BYTES must be greater than zero; use none to disable the block cache"
861 .to_string(),
862 ),
863 _ => Ok(()),
864 },
865 Self::MultiFlushInterval => match value {
866 Value::Duration(d) if d.is_positive() => Ok(()),
867 Value::Duration(_) => Err("MULTI_FLUSH_INTERVAL must be greater than zero".to_string()),
868 _ => Ok(()),
869 },
870 Self::MultiFlushBudgetBytes => match value {
871 Value::Uint8(n) if *n > 0 => Ok(()),
872 Value::Uint8(_) => Err("MULTI_FLUSH_BUDGET_BYTES must be greater than zero".to_string()),
873 _ => Ok(()),
874 },
875 Self::MultiWalAutocheckpoint => match value {
876 Value::Uint8(0) => {
877 Err("MULTI_WAL_AUTOCHECKPOINT must be greater than zero".to_string())
878 }
879 _ => Ok(()),
880 },
881 Self::OperatorResidentBudget => match value {
882 Value::Uint8(n) if *n > 0 => Ok(()),
883 Value::Uint8(_) => {
884 Err("OPERATOR_RESIDENT_BUDGET must be greater than zero".to_string())
885 }
886 _ => Ok(()),
887 },
888 Self::OperatorDirtyBudget => match value {
889 Value::Uint8(n) if *n > 0 => Ok(()),
890 Value::Uint8(_) => Err("OPERATOR_DIRTY_BUDGET must be greater than zero".to_string()),
891 _ => Ok(()),
892 },
893 Self::OperatorFlushSlice => match value {
894 Value::Uint8(n) if *n > 0 => Ok(()),
895 Value::Uint8(_) => Err("OPERATOR_FLUSH_SLICE must be greater than zero".to_string()),
896 _ => Ok(()),
897 },
898 Self::OperatorFlushInterval => match value {
899 Value::Duration(d) if d.is_positive() => Ok(()),
900 Value::Duration(_) => Err("OPERATOR_FLUSH_INTERVAL must be greater than zero".to_string()),
901 _ => Ok(()),
902 },
903 Self::OperatorWalAutocheckpoint => match value {
904 Value::Uint8(0) => {
905 Err("OPERATOR_WAL_AUTOCHECKPOINT must be greater than zero".to_string())
906 }
907 _ => Ok(()),
908 },
909 Self::CdcWalAutocheckpoint => match value {
910 Value::Uint8(0) => Err("CDC_WAL_AUTOCHECKPOINT must be greater than zero".to_string()),
911 _ => Ok(()),
912 },
913 Self::HistoricalGcBatchSize => match value {
914 Value::Uint8(0) => {
915 Err("HISTORICAL_GC_BATCH_SIZE must be greater than zero".to_string())
916 }
917 _ => Ok(()),
918 },
919 Self::HistoricalGcInterval => match value {
920 Value::Duration(d) => {
921 if d.is_positive() {
922 Ok(())
923 } else {
924 Err("HISTORICAL_GC_INTERVAL must be greater than zero".to_string())
925 }
926 }
927 _ => Ok(()),
928 },
929 Self::FlowTick => match value {
930 Value::Duration(d) => {
931 if d.is_positive() {
932 Ok(())
933 } else {
934 Err("FLOW_TICK must be greater than zero".to_string())
935 }
936 }
937 _ => Ok(()),
938 },
939 Self::FlowSampleInterval => match value {
940 Value::None {
941 ..
942 } => Ok(()),
943 Value::Duration(d) => {
944 if d.is_positive() {
945 Ok(())
946 } else {
947 Err("FLOW_SAMPLE_INTERVAL must be greater than zero".to_string())
948 }
949 }
950 _ => Ok(()),
951 },
952 Self::CdcConsumeWaitTimeout => match value {
953 Value::Duration(d) => {
954 if d.is_positive() {
955 Ok(())
956 } else {
957 Err("CDC_CONSUME_WAIT_TIMEOUT must be greater than zero".to_string())
958 }
959 }
960 _ => Ok(()),
961 },
962 Self::FlowJoinProbeBlockSize => match value {
963 Value::Uint8(0) => {
964 Err("FLOW_JOIN_PROBE_BLOCK_SIZE must be greater than zero".to_string())
965 }
966 _ => Ok(()),
967 },
968 Self::ThreadsAsync => match value {
969 Value::Uint2(0) => Err("THREADS_ASYNC must be greater than zero".to_string()),
970 _ => Ok(()),
971 },
972 Self::ThreadsCoordination => match value {
973 Value::Uint2(0) => Err("THREADS_COORDINATION must be greater than zero".to_string()),
974 _ => Ok(()),
975 },
976 Self::ThreadsFlow => match value {
977 Value::Uint2(0) => Err("THREADS_FLOW must be greater than zero".to_string()),
978 _ => Ok(()),
979 },
980 Self::ThreadsTask => match value {
981 Value::Uint2(0) => Err("THREADS_TASK must be greater than zero".to_string()),
982 _ => Ok(()),
983 },
984 Self::ThreadsCompute => match value {
985 Value::Uint2(0) => Err("THREADS_COMPUTE must be greater than zero".to_string()),
986 _ => Ok(()),
987 },
988 Self::ThreadsMaintenance => match value {
989 Value::Uint2(0) => Err("THREADS_MAINTENANCE must be greater than zero".to_string()),
990 _ => Ok(()),
991 },
992 Self::SubscriptionWorkerThreads => Ok(()),
993 Self::MetricsFlushInterval => match value {
994 Value::Duration(d) => {
995 if d.is_positive() {
996 Ok(())
997 } else {
998 Err("METRICS_FLUSH_INTERVAL must be greater than zero".to_string())
999 }
1000 }
1001 _ => Ok(()),
1002 },
1003 Self::MetricsSampleInterval => match value {
1004 Value::Duration(d) => {
1005 if d.is_positive() {
1006 Ok(())
1007 } else {
1008 Err("METRICS_SAMPLE_INTERVAL must be greater than zero".to_string())
1009 }
1010 }
1011 _ => Ok(()),
1012 },
1013 Self::MetricsSnapshotInterval => match value {
1014 Value::None {
1015 ..
1016 } => Ok(()),
1017 Value::Duration(d) => {
1018 if d.is_positive() {
1019 Ok(())
1020 } else {
1021 Err("METRICS_SNAPSHOT_INTERVAL must be greater than zero".to_string())
1022 }
1023 }
1024 _ => Ok(()),
1025 },
1026 _ => Ok(()),
1027 }
1028 }
1029
1030 pub fn accept(&self, value: Value) -> Result<Value, AcceptError> {
1031 if let Value::None {
1032 inner,
1033 } = &value
1034 {
1035 if self.is_optional() && self.expected_types().contains(inner) {
1036 return Ok(value);
1037 }
1038 return Err(AcceptError::TypeMismatch {
1039 expected: self.expected_types().to_vec(),
1040 actual: value.get_type(),
1041 });
1042 }
1043
1044 if !self.expected_types().contains(&value.get_type()) {
1045 return Err(AcceptError::TypeMismatch {
1046 expected: self.expected_types().to_vec(),
1047 actual: value.get_type(),
1048 });
1049 }
1050
1051 self.validate_canonical(&value).map_err(AcceptError::InvalidValue)?;
1052 Ok(value)
1053 }
1054}
1055
1056impl fmt::Display for ConfigKey {
1057 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1058 match self {
1059 Self::OracleWindowSize => write!(f, "ORACLE_WINDOW_SIZE"),
1060 Self::QueryRowBatchSize => write!(f, "QUERY_ROW_BATCH_SIZE"),
1061 Self::QueryMemoryLimit => write!(f, "QUERY_MEMORY_LIMIT"),
1062 Self::RetentionEvictInterval => write!(f, "RETENTION_EVICT_INTERVAL"),
1063 Self::RetentionEvictBatchSize => write!(f, "RETENTION_EVICT_BATCH_SIZE"),
1064 Self::RetentionEvictMaxBatchesPerTick => write!(f, "RETENTION_EVICT_MAX_BATCHES_PER_TICK"),
1065 Self::EpochBucketInterval => write!(f, "EPOCH_BUCKET_INTERVAL"),
1066 Self::RetentionStartupGrace => write!(f, "RETENTION_STARTUP_GRACE"),
1067 Self::MaxRetentionHorizonFloor => write!(f, "MAX_RETENTION_HORIZON_FLOOR"),
1068 Self::HistoricalGcBatchSize => write!(f, "HISTORICAL_GC_BATCH_SIZE"),
1069 Self::HistoricalGcInterval => write!(f, "HISTORICAL_GC_INTERVAL"),
1070 Self::CdcTtlDuration => write!(f, "CDC_TTL_DURATION"),
1071 Self::CdcTtlScanInterval => write!(f, "CDC_TTL_SCAN_INTERVAL"),
1072 Self::CdcTtlScanBatchSize => write!(f, "CDC_TTL_SCAN_BATCH_SIZE"),
1073 Self::CdcWalAutocheckpoint => write!(f, "CDC_WAL_AUTOCHECKPOINT"),
1074 Self::CdcCommitBufferBytes => write!(f, "CDC_COMMIT_BUFFER_BYTES"),
1075 Self::CdcBlockCutBytes => write!(f, "CDC_BLOCK_CUT_BYTES"),
1076 Self::CdcReadBufferBytes => write!(f, "CDC_READ_BUFFER_BYTES"),
1077 Self::MultiPointBufferShardBytes => write!(f, "MULTI_POINT_BUFFER_SHARD_BYTES"),
1078 Self::MultiRangeBufferShardBytes => write!(f, "MULTI_RANGE_BUFFER_SHARD_BYTES"),
1079 Self::OperatorRangeTierBytes => write!(f, "OPERATOR_RANGE_TIER_BYTES"),
1080 Self::MultiPointBufferShards => write!(f, "MULTI_POINT_BUFFER_SHARDS"),
1081 Self::MultiRangeBufferShards => write!(f, "MULTI_RANGE_BUFFER_SHARDS"),
1082 Self::MultiFlushInterval => write!(f, "MULTI_FLUSH_INTERVAL"),
1083 Self::MultiFlushBudgetBytes => write!(f, "MULTI_FLUSH_BUDGET_BYTES"),
1084 Self::MultiWalAutocheckpoint => write!(f, "MULTI_WAL_AUTOCHECKPOINT"),
1085 Self::OperatorResidentBudget => write!(f, "OPERATOR_RESIDENT_BUDGET"),
1086 Self::OperatorDirtyBudget => write!(f, "OPERATOR_DIRTY_BUDGET"),
1087 Self::OperatorFlushSlice => write!(f, "OPERATOR_FLUSH_SLICE"),
1088 Self::OperatorFlushInterval => write!(f, "OPERATOR_FLUSH_INTERVAL"),
1089 Self::OperatorWalAutocheckpoint => write!(f, "OPERATOR_WAL_AUTOCHECKPOINT"),
1090 Self::FlowTick => write!(f, "FLOW_TICK"),
1091 Self::FlowSampleInterval => write!(f, "FLOW_SAMPLE_INTERVAL"),
1092 Self::FlowBacklogMemoryLimit => write!(f, "FLOW_BACKLOG_MEMORY_LIMIT"),
1093 Self::FlowPullBatchBytes => write!(f, "FLOW_PULL_BATCH_BYTES"),
1094 Self::FlowLoadBatchBytes => write!(f, "FLOW_LOAD_BATCH_BYTES"),
1095 Self::CdcConsumeWaitTimeout => write!(f, "CDC_CONSUME_WAIT_TIMEOUT"),
1096 Self::FlowJoinProbeBlockSize => write!(f, "FLOW_JOIN_PROBE_BLOCK_SIZE"),
1097 Self::ThreadsAsync => write!(f, "THREADS_ASYNC"),
1098 Self::ThreadsCoordination => write!(f, "THREADS_COORDINATION"),
1099 Self::ThreadsFlow => write!(f, "THREADS_FLOW"),
1100 Self::ThreadsTask => write!(f, "THREADS_TASK"),
1101 Self::ThreadsCompute => write!(f, "THREADS_COMPUTE"),
1102 Self::ThreadsMaintenance => write!(f, "THREADS_MAINTENANCE"),
1103 Self::SubscriptionWorkerThreads => write!(f, "SUBSCRIPTION_WORKER_THREADS"),
1104 Self::MetricsFlushInterval => write!(f, "METRICS_FLUSH_INTERVAL"),
1105 Self::MetricsSampleInterval => write!(f, "METRICS_SAMPLE_INTERVAL"),
1106 Self::MetricsSnapshotInterval => write!(f, "METRICS_SNAPSHOT_INTERVAL"),
1107 Self::QueueLeaseReapInterval => write!(f, "QUEUE_LEASE_REAP_INTERVAL"),
1108 Self::QueueLeaseReapBatchSize => write!(f, "QUEUE_LEASE_REAP_BATCH_SIZE"),
1109 Self::QueueRetentionInterval => write!(f, "QUEUE_RETENTION_INTERVAL"),
1110 Self::QueueRetentionBatchSize => write!(f, "QUEUE_RETENTION_BATCH_SIZE"),
1111 }
1112 }
1113}
1114
1115impl FromStr for ConfigKey {
1116 type Err = String;
1117
1118 fn from_str(s: &str) -> Result<Self, Self::Err> {
1119 match s {
1120 "ORACLE_WINDOW_SIZE" => Ok(Self::OracleWindowSize),
1121 "QUERY_ROW_BATCH_SIZE" => Ok(Self::QueryRowBatchSize),
1122 "QUERY_MEMORY_LIMIT" => Ok(Self::QueryMemoryLimit),
1123 "RETENTION_EVICT_INTERVAL" => Ok(Self::RetentionEvictInterval),
1124 "RETENTION_EVICT_BATCH_SIZE" => Ok(Self::RetentionEvictBatchSize),
1125 "RETENTION_EVICT_MAX_BATCHES_PER_TICK" => Ok(Self::RetentionEvictMaxBatchesPerTick),
1126 "EPOCH_BUCKET_INTERVAL" => Ok(Self::EpochBucketInterval),
1127 "RETENTION_STARTUP_GRACE" => Ok(Self::RetentionStartupGrace),
1128 "MAX_RETENTION_HORIZON_FLOOR" => Ok(Self::MaxRetentionHorizonFloor),
1129 "HISTORICAL_GC_BATCH_SIZE" => Ok(Self::HistoricalGcBatchSize),
1130 "HISTORICAL_GC_INTERVAL" => Ok(Self::HistoricalGcInterval),
1131 "CDC_TTL_DURATION" => Ok(Self::CdcTtlDuration),
1132 "CDC_TTL_SCAN_INTERVAL" => Ok(Self::CdcTtlScanInterval),
1133 "CDC_TTL_SCAN_BATCH_SIZE" => Ok(Self::CdcTtlScanBatchSize),
1134 "CDC_WAL_AUTOCHECKPOINT" => Ok(Self::CdcWalAutocheckpoint),
1135 "CDC_COMMIT_BUFFER_BYTES" => Ok(Self::CdcCommitBufferBytes),
1136 "CDC_BLOCK_CUT_BYTES" => Ok(Self::CdcBlockCutBytes),
1137 "CDC_READ_BUFFER_BYTES" => Ok(Self::CdcReadBufferBytes),
1138 "MULTI_POINT_BUFFER_SHARD_BYTES" => Ok(Self::MultiPointBufferShardBytes),
1139 "MULTI_RANGE_BUFFER_SHARD_BYTES" => Ok(Self::MultiRangeBufferShardBytes),
1140 "OPERATOR_RANGE_TIER_BYTES" => Ok(Self::OperatorRangeTierBytes),
1141 "MULTI_POINT_BUFFER_SHARDS" => Ok(Self::MultiPointBufferShards),
1142 "MULTI_RANGE_BUFFER_SHARDS" => Ok(Self::MultiRangeBufferShards),
1143 "MULTI_FLUSH_INTERVAL" => Ok(Self::MultiFlushInterval),
1144 "MULTI_FLUSH_BUDGET_BYTES" => Ok(Self::MultiFlushBudgetBytes),
1145 "MULTI_WAL_AUTOCHECKPOINT" => Ok(Self::MultiWalAutocheckpoint),
1146 "OPERATOR_RESIDENT_BUDGET" => Ok(Self::OperatorResidentBudget),
1147 "OPERATOR_DIRTY_BUDGET" => Ok(Self::OperatorDirtyBudget),
1148 "OPERATOR_FLUSH_SLICE" => Ok(Self::OperatorFlushSlice),
1149 "OPERATOR_FLUSH_INTERVAL" => Ok(Self::OperatorFlushInterval),
1150 "OPERATOR_WAL_AUTOCHECKPOINT" => Ok(Self::OperatorWalAutocheckpoint),
1151 "FLOW_TICK" => Ok(Self::FlowTick),
1152 "FLOW_SAMPLE_INTERVAL" => Ok(Self::FlowSampleInterval),
1153 "FLOW_BACKLOG_MEMORY_LIMIT" => Ok(Self::FlowBacklogMemoryLimit),
1154 "FLOW_PULL_BATCH_BYTES" => Ok(Self::FlowPullBatchBytes),
1155 "FLOW_LOAD_BATCH_BYTES" => Ok(Self::FlowLoadBatchBytes),
1156 "CDC_CONSUME_WAIT_TIMEOUT" => Ok(Self::CdcConsumeWaitTimeout),
1157 "FLOW_JOIN_PROBE_BLOCK_SIZE" => Ok(Self::FlowJoinProbeBlockSize),
1158 "THREADS_ASYNC" => Ok(Self::ThreadsAsync),
1159 "THREADS_COORDINATION" => Ok(Self::ThreadsCoordination),
1160 "THREADS_FLOW" => Ok(Self::ThreadsFlow),
1161 "THREADS_TASK" => Ok(Self::ThreadsTask),
1162 "THREADS_COMPUTE" => Ok(Self::ThreadsCompute),
1163 "THREADS_MAINTENANCE" => Ok(Self::ThreadsMaintenance),
1164 "SUBSCRIPTION_WORKER_THREADS" => Ok(Self::SubscriptionWorkerThreads),
1165 "METRICS_FLUSH_INTERVAL" => Ok(Self::MetricsFlushInterval),
1166 "METRICS_SAMPLE_INTERVAL" => Ok(Self::MetricsSampleInterval),
1167 "METRICS_SNAPSHOT_INTERVAL" => Ok(Self::MetricsSnapshotInterval),
1168 "QUEUE_LEASE_REAP_INTERVAL" => Ok(Self::QueueLeaseReapInterval),
1169 "QUEUE_LEASE_REAP_BATCH_SIZE" => Ok(Self::QueueLeaseReapBatchSize),
1170 "QUEUE_RETENTION_INTERVAL" => Ok(Self::QueueRetentionInterval),
1171 "QUEUE_RETENTION_BATCH_SIZE" => Ok(Self::QueueRetentionBatchSize),
1172 _ => Err(format!("Unknown system configuration key: {}", s)),
1173 }
1174 }
1175}
1176
1177#[derive(Debug, Clone)]
1178pub struct Config {
1179 pub key: ConfigKey,
1180
1181 pub value: Value,
1182
1183 pub default_value: Value,
1184
1185 pub description: &'static str,
1186
1187 pub requires_restart: bool,
1188}
1189
1190pub trait GetConfig: Send + Sync {
1191 fn get_config(&self, key: ConfigKey) -> Value;
1192
1193 fn get_config_at(&self, key: ConfigKey, version: CommitVersion) -> Value;
1194
1195 fn get_config_uint8(&self, key: ConfigKey) -> u64 {
1196 let val = self.get_config(key);
1197 match val {
1198 Value::Uint8(v) => v,
1199 v => panic!("config key '{}' expected Uint8, got {:?}", key, v),
1200 }
1201 }
1202
1203 fn get_config_uint1(&self, key: ConfigKey) -> u8 {
1204 let val = self.get_config(key);
1205 match val {
1206 Value::Uint1(v) => v,
1207 v => panic!("config key '{}' expected Uint1, got {:?}", key, v),
1208 }
1209 }
1210
1211 fn get_config_uint2(&self, key: ConfigKey) -> u16 {
1212 let val = self.get_config(key);
1213 match val {
1214 Value::Uint2(v) => v,
1215 v => panic!("config key '{}' expected Uint2, got {:?}", key, v),
1216 }
1217 }
1218
1219 fn get_config_duration(&self, key: ConfigKey) -> Duration {
1220 let val = self.get_config(key);
1221 match val {
1222 Value::Duration(v) => v,
1223 v => panic!("config key '{}' expected Duration, got {:?}", key, v),
1224 }
1225 }
1226
1227 fn get_config_duration_opt(&self, key: ConfigKey) -> Option<Duration> {
1228 match self.get_config(key) {
1229 Value::None {
1230 ..
1231 } => None,
1232 Value::Duration(v) => Some(v),
1233 v => panic!("config key '{}' expected Duration or None, got {:?}", key, v),
1234 }
1235 }
1236
1237 fn get_config_uint8_opt(&self, key: ConfigKey) -> Option<u64> {
1238 match self.get_config(key) {
1239 Value::None {
1240 ..
1241 } => None,
1242 Value::Uint8(v) => Some(v),
1243 v => panic!("config key '{}' expected Uint8 or None, got {:?}", key, v),
1244 }
1245 }
1246}
1247
1248#[cfg(test)]
1249mod tests {
1250 use super::*;
1251
1252 #[test]
1253 fn test_cdc_ttl_default_is_typed_null() {
1254 let default = ConfigKey::CdcTtlDuration.default_value();
1256 assert!(matches!(
1257 default,
1258 Value::None {
1259 inner: ValueType::Duration
1260 }
1261 ));
1262 }
1263
1264 #[test]
1265 fn test_cdc_ttl_accept_passes_typed_null() {
1266 let none = Value::None {
1267 inner: ValueType::Duration,
1268 };
1269 let v = ConfigKey::CdcTtlDuration.accept(none.clone()).unwrap();
1270 assert_eq!(v, none);
1271 }
1272
1273 #[test]
1274 fn test_cdc_ttl_accept_passes_positive_duration() {
1275 let one_sec = Value::duration_seconds(1);
1276 assert_eq!(ConfigKey::CdcTtlDuration.accept(one_sec.clone()).unwrap(), one_sec);
1277
1278 let one_hour = Value::duration_seconds(3600);
1279 assert_eq!(ConfigKey::CdcTtlDuration.accept(one_hour.clone()).unwrap(), one_hour);
1280 }
1281
1282 #[test]
1283 fn test_cdc_ttl_accept_rejects_zero() {
1284 let zero = Value::duration_seconds(0);
1285 match ConfigKey::CdcTtlDuration.accept(zero).unwrap_err() {
1286 AcceptError::InvalidValue(reason) => {
1287 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1288 }
1289 other => panic!("expected InvalidValue, got {other:?}"),
1290 }
1291 }
1292
1293 #[test]
1294 fn test_cdc_ttl_accept_rejects_negative() {
1295 let negative = Value::duration_seconds(-5);
1296 assert!(matches!(ConfigKey::CdcTtlDuration.accept(negative), Err(AcceptError::InvalidValue(_))));
1297 }
1298
1299 #[test]
1300 fn test_other_keys_accept_in_type_values() {
1301 assert!(ConfigKey::OracleWindowSize.accept(Value::Uint8(0)).is_ok());
1302 }
1303
1304 #[test]
1305 fn test_cdc_ttl_round_trips_through_display_and_from_str() {
1306 let key: ConfigKey = "CDC_TTL_DURATION".parse().unwrap();
1307 assert_eq!(key, ConfigKey::CdcTtlDuration);
1308 assert_eq!(format!("{}", ConfigKey::CdcTtlDuration), "CDC_TTL_DURATION");
1309 }
1310
1311 #[test]
1312 fn test_cdc_ttl_in_all() {
1313 assert!(ConfigKey::all().contains(&ConfigKey::CdcTtlDuration));
1314 }
1315
1316 #[test]
1317 fn test_query_memory_limit_defaults_and_round_trips() {
1318 assert_eq!(ConfigKey::QueryMemoryLimit.production_value(), Value::Uint8(1024 * 1024 * 1024));
1319 assert_eq!(ConfigKey::QueryMemoryLimit.expected_types(), &[ValueType::Uint8]);
1320 let key: ConfigKey = "QUERY_MEMORY_LIMIT".parse().unwrap();
1321 assert_eq!(key, ConfigKey::QueryMemoryLimit);
1322 assert_eq!(format!("{}", ConfigKey::QueryMemoryLimit), "QUERY_MEMORY_LIMIT");
1323 }
1324
1325 #[test]
1326 fn test_query_memory_limit_rejects_zero() {
1327 assert!(ConfigKey::QueryMemoryLimit.accept(Value::Uint8(0)).is_err());
1329 assert_eq!(ConfigKey::QueryMemoryLimit.accept(Value::Uint8(1)).unwrap(), Value::Uint8(1));
1330 }
1331
1332 #[test]
1333 fn test_query_memory_limit_requires_restart_and_optional() {
1334 assert!(!ConfigKey::QueryMemoryLimit.requires_restart());
1336 assert!(!ConfigKey::QueryMemoryLimit.is_optional());
1338 }
1339
1340 #[test]
1341 fn test_all_contains_every_compact_key_and_has_expected_len() {
1342 let all = ConfigKey::all();
1343 assert_eq!(all.len(), 52);
1344 assert!(all.contains(&ConfigKey::QueryMemoryLimit));
1345 assert!(all.contains(&ConfigKey::RetentionEvictInterval));
1346 assert!(all.contains(&ConfigKey::RetentionEvictBatchSize));
1347 assert!(all.contains(&ConfigKey::RetentionEvictMaxBatchesPerTick));
1348 assert!(all.contains(&ConfigKey::MultiFlushInterval));
1349 assert!(all.contains(&ConfigKey::MultiWalAutocheckpoint));
1350 assert!(all.contains(&ConfigKey::OperatorResidentBudget));
1351 assert!(all.contains(&ConfigKey::OperatorDirtyBudget));
1352 assert!(all.contains(&ConfigKey::OperatorFlushSlice));
1353 assert!(all.contains(&ConfigKey::OperatorFlushInterval));
1354 assert!(all.contains(&ConfigKey::OperatorWalAutocheckpoint));
1355 assert!(all.contains(&ConfigKey::CdcWalAutocheckpoint));
1356 assert!(all.contains(&ConfigKey::CdcConsumeWaitTimeout));
1357 assert!(all.contains(&ConfigKey::FlowJoinProbeBlockSize));
1358 assert!(all.contains(&ConfigKey::CdcTtlScanInterval));
1359 assert!(all.contains(&ConfigKey::CdcTtlScanBatchSize));
1360 assert!(all.contains(&ConfigKey::MaxRetentionHorizonFloor));
1361 assert!(all.contains(&ConfigKey::FlowLoadBatchBytes));
1362 assert!(all.contains(&ConfigKey::CdcCommitBufferBytes));
1363 assert!(all.contains(&ConfigKey::CdcBlockCutBytes));
1364 assert!(all.contains(&ConfigKey::CdcReadBufferBytes));
1365 assert!(all.contains(&ConfigKey::OperatorRangeTierBytes));
1366 assert!(all.contains(&ConfigKey::OperatorDirtyBudget));
1367 assert!(all.contains(&ConfigKey::OperatorFlushSlice));
1368 assert!(all.contains(&ConfigKey::MultiPointBufferShards));
1369 assert!(all.contains(&ConfigKey::MultiRangeBufferShards));
1370 assert!(all.contains(&ConfigKey::FlowBacklogMemoryLimit));
1371 assert!(all.contains(&ConfigKey::FlowPullBatchBytes));
1372 assert!(all.contains(&ConfigKey::FlowLoadBatchBytes));
1373 assert!(all.contains(&ConfigKey::QueryRowBatchSize));
1374 assert!(all.contains(&ConfigKey::ThreadsAsync));
1375 assert!(all.contains(&ConfigKey::ThreadsCoordination));
1376 assert!(all.contains(&ConfigKey::ThreadsFlow));
1377 assert!(all.contains(&ConfigKey::ThreadsTask));
1378 assert!(all.contains(&ConfigKey::ThreadsCompute));
1379 assert!(all.contains(&ConfigKey::ThreadsMaintenance));
1380 assert!(all.contains(&ConfigKey::MetricsFlushInterval));
1381 assert!(all.contains(&ConfigKey::SubscriptionWorkerThreads));
1382 assert!(all.contains(&ConfigKey::FlowSampleInterval));
1383 assert!(all.contains(&ConfigKey::MetricsSampleInterval));
1384 assert!(all.contains(&ConfigKey::MetricsSnapshotInterval));
1385 assert!(all.contains(&ConfigKey::QueueLeaseReapInterval));
1386 assert!(all.contains(&ConfigKey::QueueLeaseReapBatchSize));
1387 assert!(all.contains(&ConfigKey::QueueRetentionInterval));
1388 assert!(all.contains(&ConfigKey::QueueRetentionBatchSize));
1389 }
1390
1391 #[test]
1392 fn test_metrics_sample_interval_is_always_on() {
1393 assert_eq!(ConfigKey::MetricsSampleInterval.default_value(), Value::duration_seconds(10));
1396 assert_eq!(ConfigKey::MetricsSampleInterval.expected_types(), &[ValueType::Duration]);
1397 assert!(!ConfigKey::MetricsSampleInterval.is_optional(), "there is no off value, only a cadence");
1398 assert!(ConfigKey::MetricsSampleInterval.requires_restart(), "read once at boot");
1399
1400 let ten = Value::duration_seconds(10);
1401 assert_eq!(ConfigKey::MetricsSampleInterval.accept(ten.clone()).unwrap(), ten);
1402 let zero = Value::duration_seconds(0);
1403 assert!(matches!(ConfigKey::MetricsSampleInterval.accept(zero), Err(AcceptError::InvalidValue(_))));
1404 }
1405
1406 #[test]
1407 fn test_metrics_snapshot_interval_accepts_none_and_positive_rejects_zero() {
1408 assert_eq!(
1410 ConfigKey::MetricsSnapshotInterval.default_value(),
1411 Value::None {
1412 inner: ValueType::Duration
1413 },
1414 "snapshotting must be opt-in"
1415 );
1416 assert!(ConfigKey::MetricsSnapshotInterval.is_optional(), "none must stay accepted to turn it off");
1417 assert!(ConfigKey::MetricsSnapshotInterval.requires_restart(), "read once at boot");
1418
1419 let none = Value::None {
1420 inner: ValueType::Duration,
1421 };
1422 assert_eq!(ConfigKey::MetricsSnapshotInterval.accept(none.clone()).unwrap(), none);
1423
1424 let minute = Value::duration_seconds(60);
1425 assert_eq!(ConfigKey::MetricsSnapshotInterval.accept(minute.clone()).unwrap(), minute);
1426
1427 let zero = Value::duration_seconds(0);
1428 match ConfigKey::MetricsSnapshotInterval.accept(zero).unwrap_err() {
1429 AcceptError::InvalidValue(reason) => {
1430 assert!(reason.contains("must be greater than zero"), "unexpected reason: {reason}");
1431 }
1432 other => panic!("expected InvalidValue, got {other:?}"),
1433 }
1434 }
1435
1436 #[test]
1437 fn test_metrics_sampler_keys_round_trip() {
1438 for (key, name) in [
1439 (ConfigKey::MetricsSampleInterval, "METRICS_SAMPLE_INTERVAL"),
1440 (ConfigKey::MetricsSnapshotInterval, "METRICS_SNAPSHOT_INTERVAL"),
1441 ] {
1442 assert_eq!(format!("{key}"), name);
1443 assert_eq!(name.parse::<ConfigKey>().unwrap(), key);
1444 }
1445 }
1446
1447 #[test]
1448 fn test_flow_sample_interval_metadata() {
1449 assert_eq!(ConfigKey::FlowSampleInterval.default_value(), Value::duration_seconds(60));
1452 assert_eq!(ConfigKey::FlowSampleInterval.expected_types(), &[ValueType::Duration]);
1453 assert!(ConfigKey::FlowSampleInterval.is_optional());
1454 }
1455
1456 #[test]
1457 fn test_flow_sample_interval_round_trip() {
1458 assert_eq!("FLOW_SAMPLE_INTERVAL".parse::<ConfigKey>().unwrap(), ConfigKey::FlowSampleInterval);
1459 assert_eq!(format!("{}", ConfigKey::FlowSampleInterval), "FLOW_SAMPLE_INTERVAL");
1460 }
1461
1462 #[test]
1463 fn test_flow_sample_interval_accepts_none_and_positive_rejects_zero() {
1464 let none = Value::None {
1465 inner: ValueType::Duration,
1466 };
1467 assert_eq!(
1468 ConfigKey::FlowSampleInterval.accept(none.clone()).unwrap(),
1469 none,
1470 "none must be accepted so sampling can be turned off"
1471 );
1472
1473 let minute = Value::duration_seconds(60);
1474 assert_eq!(ConfigKey::FlowSampleInterval.accept(minute.clone()).unwrap(), minute);
1475
1476 let zero = Value::duration_seconds(0);
1477 assert!(matches!(ConfigKey::FlowSampleInterval.accept(zero), Err(AcceptError::InvalidValue(_))));
1478 }
1479
1480 #[test]
1481 fn test_metrics_flush_interval_metadata() {
1482 assert_eq!(ConfigKey::MetricsFlushInterval.default_value(), Value::duration_seconds(10));
1484 assert_eq!(ConfigKey::MetricsFlushInterval.expected_types(), &[ValueType::Duration]);
1485 assert!(!ConfigKey::MetricsFlushInterval.is_optional());
1486 assert!(!ConfigKey::MetricsFlushInterval.requires_restart());
1487 }
1488
1489 #[test]
1490 fn test_metrics_flush_interval_round_trip() {
1491 assert_eq!("METRICS_FLUSH_INTERVAL".parse::<ConfigKey>().unwrap(), ConfigKey::MetricsFlushInterval);
1492 assert_eq!(format!("{}", ConfigKey::MetricsFlushInterval), "METRICS_FLUSH_INTERVAL");
1493 }
1494
1495 #[test]
1496 fn test_metrics_flush_interval_accepts_positive_rejects_zero() {
1497 let ten = Value::duration_seconds(10);
1498 assert_eq!(ConfigKey::MetricsFlushInterval.accept(ten.clone()).unwrap(), ten);
1499
1500 let zero = Value::duration_seconds(0);
1501 assert!(matches!(ConfigKey::MetricsFlushInterval.accept(zero), Err(AcceptError::InvalidValue(_))));
1502 }
1503
1504 #[test]
1505 fn test_threads_keys_round_trip() {
1506 assert_eq!("THREADS_ASYNC".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsAsync);
1507 assert_eq!("THREADS_COORDINATION".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsCoordination);
1508 assert_eq!("THREADS_FLOW".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsFlow);
1509 assert_eq!("THREADS_TASK".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsTask);
1510 assert_eq!("THREADS_COMPUTE".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsCompute);
1511 assert_eq!("THREADS_MAINTENANCE".parse::<ConfigKey>().unwrap(), ConfigKey::ThreadsMaintenance);
1512 assert_eq!(format!("{}", ConfigKey::ThreadsAsync), "THREADS_ASYNC");
1513 assert_eq!(format!("{}", ConfigKey::ThreadsCoordination), "THREADS_COORDINATION");
1514 assert_eq!(format!("{}", ConfigKey::ThreadsFlow), "THREADS_FLOW");
1515 assert_eq!(format!("{}", ConfigKey::ThreadsTask), "THREADS_TASK");
1516 assert_eq!(format!("{}", ConfigKey::ThreadsCompute), "THREADS_COMPUTE");
1517 assert_eq!(format!("{}", ConfigKey::ThreadsMaintenance), "THREADS_MAINTENANCE");
1518 }
1519
1520 #[test]
1521 fn test_threads_defaults() {
1522 assert_eq!(ConfigKey::ThreadsAsync.production_value(), Value::Uint2(1));
1523 assert_eq!(ConfigKey::ThreadsCoordination.production_value(), Value::Uint2(2));
1524 assert_eq!(ConfigKey::ThreadsFlow.production_value(), Value::Uint2(2));
1525 assert_eq!(ConfigKey::ThreadsTask.production_value(), Value::Uint2(2));
1526 assert_eq!(ConfigKey::ThreadsCompute.production_value(), Value::Uint2(2));
1527 assert_eq!(ConfigKey::ThreadsMaintenance.production_value(), Value::Uint2(1));
1528 }
1529
1530 #[test]
1531 fn test_threads_reject_zero() {
1532 for key in [
1533 ConfigKey::ThreadsAsync,
1534 ConfigKey::ThreadsCoordination,
1535 ConfigKey::ThreadsFlow,
1536 ConfigKey::ThreadsTask,
1537 ConfigKey::ThreadsCompute,
1538 ConfigKey::ThreadsMaintenance,
1539 ] {
1540 match key.accept(Value::Uint2(0)).unwrap_err() {
1541 AcceptError::InvalidValue(reason) => {
1542 assert!(
1543 reason.contains("greater than zero"),
1544 "{key}: unexpected reason: {reason}"
1545 );
1546 }
1547 other => panic!("{key}: expected InvalidValue, got {other:?}"),
1548 }
1549 }
1550 }
1551
1552 #[test]
1553 fn test_threads_accept_positive() {
1554 assert_eq!(ConfigKey::ThreadsAsync.accept(Value::Uint2(4)).unwrap(), Value::Uint2(4));
1555 assert_eq!(ConfigKey::ThreadsCoordination.accept(Value::Uint2(8)).unwrap(), Value::Uint2(8));
1556 assert_eq!(ConfigKey::ThreadsFlow.accept(Value::Uint2(16)).unwrap(), Value::Uint2(16));
1557 assert_eq!(ConfigKey::ThreadsTask.accept(Value::Uint2(4)).unwrap(), Value::Uint2(4));
1558 assert_eq!(ConfigKey::ThreadsCompute.accept(Value::Uint2(2)).unwrap(), Value::Uint2(2));
1559 assert_eq!(ConfigKey::ThreadsMaintenance.accept(Value::Uint2(4)).unwrap(), Value::Uint2(4));
1560 }
1561
1562 #[test]
1563 fn test_threads_reject_int4_for_uint2_key() {
1564 assert!(matches!(ConfigKey::ThreadsTask.accept(Value::Int4(8)), Err(AcceptError::TypeMismatch { .. })));
1566 }
1567
1568 #[test]
1569 fn test_threads_require_restart() {
1570 assert!(ConfigKey::ThreadsAsync.requires_restart());
1571 assert!(ConfigKey::ThreadsCoordination.requires_restart());
1572 assert!(ConfigKey::ThreadsFlow.requires_restart());
1573 assert!(ConfigKey::ThreadsTask.requires_restart());
1574 assert!(ConfigKey::ThreadsCompute.requires_restart());
1575 assert!(ConfigKey::ThreadsMaintenance.requires_restart());
1576 }
1577
1578 #[test]
1579 fn test_query_row_batch_size_default_is_uint2_128() {
1580 assert_eq!(ConfigKey::QueryRowBatchSize.production_value(), Value::Uint2(128));
1581 }
1582
1583 #[test]
1584 fn test_query_row_batch_size_round_trips_through_display_and_from_str() {
1585 let key: ConfigKey = "QUERY_ROW_BATCH_SIZE".parse().unwrap();
1586 assert_eq!(key, ConfigKey::QueryRowBatchSize);
1587 assert_eq!(format!("{}", ConfigKey::QueryRowBatchSize), "QUERY_ROW_BATCH_SIZE");
1588 }
1589
1590 #[test]
1591 fn test_query_row_batch_size_accept_rejects_zero() {
1592 match ConfigKey::QueryRowBatchSize.accept(Value::Uint2(0)).unwrap_err() {
1593 AcceptError::InvalidValue(reason) => {
1594 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1595 }
1596 other => panic!("expected InvalidValue, got {other:?}"),
1597 }
1598 }
1599
1600 #[test]
1601 fn test_query_row_batch_size_accept_passes_positive() {
1602 assert_eq!(ConfigKey::QueryRowBatchSize.accept(Value::Uint2(1)).unwrap(), Value::Uint2(1));
1603 assert_eq!(ConfigKey::QueryRowBatchSize.accept(Value::Uint2(1024)).unwrap(), Value::Uint2(1024));
1604 }
1605
1606 #[test]
1607 fn test_query_row_batch_size_rejects_mismatched_type() {
1608 assert!(matches!(
1610 ConfigKey::QueryRowBatchSize.accept(Value::Int4(64)),
1611 Err(AcceptError::TypeMismatch { .. })
1612 ));
1613 assert!(matches!(
1614 ConfigKey::QueryRowBatchSize.accept(Value::Int4(0)),
1615 Err(AcceptError::TypeMismatch { .. })
1616 ));
1617 }
1618
1619 #[test]
1620 fn test_accept_rejects_int4_for_uint8_key() {
1621 assert!(matches!(
1623 ConfigKey::FlowLoadBatchBytes.accept(Value::Int4(1024)),
1624 Err(AcceptError::TypeMismatch { .. })
1625 ));
1626 assert!(matches!(
1627 ConfigKey::FlowLoadBatchBytes.accept(Value::Int8(2048)),
1628 Err(AcceptError::TypeMismatch { .. })
1629 ));
1630 }
1631
1632 #[test]
1633 fn test_accept_rejects_zero_of_canonical_type() {
1634 match ConfigKey::FlowLoadBatchBytes.accept(Value::Uint8(0)).unwrap_err() {
1635 AcceptError::InvalidValue(reason) => {
1636 assert!(reason.contains("greater than zero"));
1637 }
1638 other => panic!("expected InvalidValue, got {other:?}"),
1639 }
1640 }
1641
1642 #[test]
1643 fn test_accept_rejects_negative_int_for_uint8_key() {
1644 assert!(matches!(
1647 ConfigKey::FlowLoadBatchBytes.accept(Value::Int4(-1)),
1648 Err(AcceptError::TypeMismatch { .. })
1649 ));
1650 }
1651
1652 #[test]
1653 fn test_accept_rejects_int_for_duration_key() {
1654 assert!(matches!(
1657 ConfigKey::MaxRetentionHorizonFloor.accept(Value::Int4(60)),
1658 Err(AcceptError::TypeMismatch { .. })
1659 ));
1660 }
1661
1662 #[test]
1663 fn test_accept_idempotent_on_canonical_uint8() {
1664 let canonical = Value::Uint8(42);
1665 assert_eq!(ConfigKey::OracleWindowSize.accept(canonical.clone()).unwrap(), canonical);
1666 }
1667
1668 #[test]
1669 fn test_accept_idempotent_on_canonical_duration() {
1670 let canonical = Value::duration_seconds(5);
1671 assert_eq!(ConfigKey::MaxRetentionHorizonFloor.accept(canonical.clone()).unwrap(), canonical);
1672 }
1673
1674 #[test]
1675 fn test_accept_rejects_typed_null_for_non_optional_key() {
1676 let err = ConfigKey::FlowLoadBatchBytes
1677 .accept(Value::None {
1678 inner: ValueType::Uint8,
1679 })
1680 .unwrap_err();
1681 assert!(matches!(err, AcceptError::TypeMismatch { .. }));
1682 }
1683
1684 #[test]
1685 fn test_accept_passes_typed_null_for_optional_key() {
1686 let none = Value::None {
1687 inner: ValueType::Duration,
1688 };
1689 assert_eq!(ConfigKey::CdcTtlDuration.accept(none.clone()).unwrap(), none);
1690 }
1691
1692 #[test]
1693 fn test_accept_rejects_wrong_inner_type_typed_null_for_optional_key() {
1694 let err = ConfigKey::CdcTtlDuration
1696 .accept(Value::None {
1697 inner: ValueType::Uint8,
1698 })
1699 .unwrap_err();
1700 assert!(matches!(err, AcceptError::TypeMismatch { .. }));
1701 }
1702
1703 #[test]
1704 fn test_historical_gc_keys_round_trip() {
1705 assert_eq!("HISTORICAL_GC_BATCH_SIZE".parse::<ConfigKey>().unwrap(), ConfigKey::HistoricalGcBatchSize);
1706 assert_eq!("HISTORICAL_GC_INTERVAL".parse::<ConfigKey>().unwrap(), ConfigKey::HistoricalGcInterval);
1707 assert_eq!(format!("{}", ConfigKey::HistoricalGcBatchSize), "HISTORICAL_GC_BATCH_SIZE");
1708 assert_eq!(format!("{}", ConfigKey::HistoricalGcInterval), "HISTORICAL_GC_INTERVAL");
1709 }
1710
1711 #[test]
1712 fn test_historical_gc_defaults() {
1713 assert_eq!(ConfigKey::HistoricalGcBatchSize.production_value(), Value::Uint8(50_000));
1714 assert!(matches!(ConfigKey::HistoricalGcInterval.production_value(), Value::Duration(_)));
1715 }
1716
1717 #[test]
1718 fn test_historical_gc_batch_size_rejects_zero() {
1719 match ConfigKey::HistoricalGcBatchSize.accept(Value::Uint8(0)).unwrap_err() {
1720 AcceptError::InvalidValue(reason) => {
1721 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1722 }
1723 other => panic!("expected InvalidValue, got {other:?}"),
1724 }
1725 }
1726
1727 #[test]
1728 fn test_historical_gc_interval_rejects_zero() {
1729 let zero = Value::duration_seconds(0);
1730 match ConfigKey::HistoricalGcInterval.accept(zero).unwrap_err() {
1731 AcceptError::InvalidValue(reason) => {
1732 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1733 }
1734 other => panic!("expected InvalidValue, got {other:?}"),
1735 }
1736 }
1737
1738 #[test]
1739 fn test_operator_flush_budget_bytes_metadata() {
1740 assert_eq!(ConfigKey::OperatorResidentBudget.production_value(), Value::Uint8(128 * 1024 * 1024));
1741 assert_eq!(ConfigKey::OperatorResidentBudget.expected_types(), &[ValueType::Uint8]);
1742 assert!(!ConfigKey::OperatorResidentBudget.is_optional());
1743 assert!(
1744 ConfigKey::OperatorResidentBudget.requires_restart(),
1745 "the budget sizes a MemoryBudget built once with the commit tier; declaring it live would \
1746 promise a rewrite that no running store can adopt"
1747 );
1748 }
1749
1750 #[test]
1751 fn test_operator_flush_budget_bytes_rejects_zero() {
1752 match ConfigKey::OperatorResidentBudget.accept(Value::Uint8(0)).unwrap_err() {
1755 AcceptError::InvalidValue(reason) => {
1756 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1757 }
1758 other => panic!("expected InvalidValue, got {other:?}"),
1759 }
1760 assert_eq!(ConfigKey::OperatorResidentBudget.accept(Value::Uint8(1)).unwrap(), Value::Uint8(1));
1761 }
1762
1763 #[test]
1764 fn test_operator_flush_budget_bytes_round_trips_through_display_and_from_str() {
1765 assert_eq!("OPERATOR_RESIDENT_BUDGET".parse::<ConfigKey>().unwrap(), ConfigKey::OperatorResidentBudget);
1766 assert_eq!(format!("{}", ConfigKey::OperatorResidentBudget), "OPERATOR_RESIDENT_BUDGET");
1767 }
1768
1769 #[test]
1770 fn test_operator_wal_autocheckpoint_metadata() {
1771 assert_eq!(ConfigKey::OperatorWalAutocheckpoint.production_value(), Value::Uint8(1000000));
1772 assert_eq!(ConfigKey::OperatorWalAutocheckpoint.expected_types(), &[ValueType::Uint8]);
1773 assert!(!ConfigKey::OperatorWalAutocheckpoint.is_optional());
1774 assert!(ConfigKey::OperatorWalAutocheckpoint.requires_restart());
1775 }
1776
1777 #[test]
1778 fn test_operator_wal_autocheckpoint_rejects_zero() {
1779 match ConfigKey::OperatorWalAutocheckpoint.accept(Value::Uint8(0)).unwrap_err() {
1782 AcceptError::InvalidValue(reason) => {
1783 assert!(reason.contains("greater than zero"), "unexpected reason: {reason}");
1784 }
1785 other => panic!("expected InvalidValue, got {other:?}"),
1786 }
1787 assert_eq!(ConfigKey::OperatorWalAutocheckpoint.accept(Value::Uint8(1)).unwrap(), Value::Uint8(1));
1788 }
1789
1790 #[test]
1791 fn test_operator_wal_autocheckpoint_round_trips_through_display_and_from_str() {
1792 assert_eq!(
1793 "OPERATOR_WAL_AUTOCHECKPOINT".parse::<ConfigKey>().unwrap(),
1794 ConfigKey::OperatorWalAutocheckpoint
1795 );
1796 assert_eq!(format!("{}", ConfigKey::OperatorWalAutocheckpoint), "OPERATOR_WAL_AUTOCHECKPOINT");
1797 }
1798}