feldera_types/config.rs
1//! Controller configuration.
2//!
3//! This module defines the controller configuration structure. The leaves of
4//! this structure are individual transport-specific and data-format-specific
5//! endpoint configs. We represent these configs as opaque JSON values, so
6//! that the entire configuration tree can be deserialized from a JSON file.
7
8use crate::postprocess::PostprocessorConfig;
9use crate::preprocess::PreprocessorConfig;
10use crate::secret_resolver::default_secrets_directory;
11use crate::transport::adhoc::AdHocInputConfig;
12use crate::transport::clock::{ClockConfig, ClockTimezoneOffset};
13use crate::transport::datagen::DatagenInputConfig;
14use crate::transport::delta_table::{DeltaTableReaderConfig, DeltaTableWriterConfig};
15use crate::transport::dynamodb::DynamoDBWriterConfig;
16use crate::transport::file::{FileInputConfig, FileOutputConfig};
17use crate::transport::http::{HttpInputConfig, HttpOutputConfig};
18use crate::transport::iceberg::IcebergReaderConfig;
19use crate::transport::kafka::{KafkaInputConfig, KafkaOutputConfig};
20use crate::transport::nats::NatsInputConfig;
21use crate::transport::nexmark::NexmarkInputConfig;
22use crate::transport::postgres::{
23 PostgresCdcReaderConfig, PostgresReaderConfig, PostgresWriterConfig,
24};
25use crate::transport::pubsub::PubSubInputConfig;
26use crate::transport::redis::RedisOutputConfig;
27use crate::transport::s3::S3InputConfig;
28use crate::transport::url::UrlInputConfig;
29use core::fmt;
30use feldera_ir::{MirNode, MirNodeId};
31use serde::de::{self, MapAccess, Visitor};
32use serde::{Deserialize, Deserializer, Serialize};
33use serde_json::Value as JsonValue;
34use std::collections::HashMap;
35use std::fmt::Display;
36use std::path::Path;
37use std::str::FromStr;
38use std::time::Duration;
39use std::{borrow::Cow, cmp::max, collections::BTreeMap};
40use utoipa::ToSchema;
41use utoipa::openapi::{ObjectBuilder, OneOfBuilder, Ref, RefOr, Schema, SchemaType};
42
43pub mod dev_tweaks;
44pub use dev_tweaks::DevTweaks;
45
46const DEFAULT_MAX_PARALLEL_CONNECTOR_INIT: u64 = 10;
47
48/// Default maximum number of updates to be kept in the output buffer.
49const DEFAULT_MAX_OUTPUT_BUFFER_SIZE_RECORDS: usize = 10_000_000;
50
51/// Default value of `ConnectorConfig::max_queued_records`.
52pub const fn default_max_queued_records() -> u64 {
53 1_000_000
54}
55
56pub const DEFAULT_MAX_WORKER_BATCH_SIZE: u64 = 10_000;
57
58pub const DEFAULT_CLOCK_RESOLUTION_USECS: u64 = 1_000_000;
59
60/// Program information included in the pipeline configuration.
61#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq, Eq)]
62pub struct ProgramIr {
63 /// The MIR of the program.
64 pub mir: HashMap<MirNodeId, MirNode>,
65 /// Program schema.
66 pub program_schema: serde_json::Value,
67}
68
69/// Identity of a pipeline, used to record and check ownership of an S3
70/// checkpoint bucket.
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
72pub struct PipelineIdentity {
73 /// System-generated name of the pipeline (format `pipeline-<uuid>`).
74 pub name: String,
75 /// Name given to the pipeline by the tenant.
76 pub given_name: Option<String>,
77}
78
79impl Display for PipelineIdentity {
80 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81 match &self.given_name {
82 Some(given_name) => write!(f, "{given_name} (id: {})", self.name),
83 None => write!(f, "{}", self.name),
84 }
85 }
86}
87
88/// Pipeline deployment configuration.
89/// It represents configuration entries directly provided by the user
90/// (e.g., runtime configuration) and entries derived from the schema
91/// of the compiled program (e.g., connectors). Storage configuration,
92/// if applicable, is set by the runner.
93#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)]
94pub struct PipelineConfig {
95 /// Global controller configuration.
96 #[serde(flatten)]
97 #[schema(inline)]
98 pub global: RuntimeConfig,
99
100 /// Configuration for multihost pipelines.
101 ///
102 /// The presence of this field indicates that the pipeline is running in
103 /// multihost mode. In the pod with ordinal 0, this triggers starting the
104 /// coordinator process. In all pods, this tells the pipeline process to
105 /// await a connection from the coordinator instead of initializing the
106 /// pipeline immediately.
107 pub multihost: Option<MultihostConfig>,
108
109 /// Unique system-generated name of the pipeline (format: `pipeline-<uuid>`).
110 /// It is unique across all tenants and cannot be changed.
111 ///
112 /// The `<uuid>` is also used in the naming of various resources that back the pipeline,
113 /// and as such this name is useful to find/identify corresponding resources.
114 pub name: Option<String>,
115
116 /// Name given by the tenant to the pipeline. It is only unique within the same tenant, and can
117 /// be changed by the tenant when the pipeline is stopped.
118 ///
119 /// Given a specific tenant, it can be used to find/identify a specific pipeline of theirs.
120 pub given_name: Option<String>,
121
122 /// Configuration for persistent storage
123 ///
124 /// If `global.storage` is `Some(_)`, this field must be set to some
125 /// [`StorageConfig`]. If `global.storage` is `None``, the pipeline ignores
126 /// this field.
127 #[serde(default)]
128 pub storage_config: Option<StorageConfig>,
129
130 /// Directory containing values of secrets.
131 ///
132 /// If this is not set, a default directory is used.
133 pub secrets_dir: Option<String>,
134
135 /// Input endpoint configuration.
136 #[serde(default)]
137 pub inputs: BTreeMap<Cow<'static, str>, InputEndpointConfig>,
138
139 /// Output endpoint configuration.
140 #[serde(default)]
141 pub outputs: BTreeMap<Cow<'static, str>, OutputEndpointConfig>,
142
143 /// Program information.
144 #[serde(default)]
145 pub program_ir: Option<ProgramIr>,
146}
147
148impl PipelineConfig {
149 pub fn max_parallel_connector_init(&self) -> u64 {
150 max(
151 self.global
152 .max_parallel_connector_init
153 .unwrap_or(DEFAULT_MAX_PARALLEL_CONNECTOR_INIT),
154 1,
155 )
156 }
157
158 /// Extract the compiler-generated subset of the configuration (connectors
159 /// and program IR) used to compute pipeline diffs.
160 pub fn program_info_subset(&self) -> PipelineConfigProgramInfo {
161 PipelineConfigProgramInfo {
162 inputs: self.inputs.clone(),
163 outputs: self.outputs.clone(),
164 program_ir: self.program_ir.clone(),
165 }
166 }
167
168 pub fn with_storage(self, storage: Option<(StorageConfig, StorageOptions)>) -> Self {
169 let (storage_config, storage_options) = storage.unzip();
170 Self {
171 global: RuntimeConfig {
172 storage: storage_options,
173 ..self.global
174 },
175 storage_config,
176 ..self
177 }
178 }
179
180 pub fn storage(&self) -> Option<(&StorageConfig, &StorageOptions)> {
181 let storage_options = self.global.storage.as_ref();
182 let storage_config = self.storage_config.as_ref();
183 storage_config.zip(storage_options)
184 }
185
186 /// Returns this pipeline's [`PipelineIdentity`], used to identify the
187 /// pipeline that owns an S3 checkpoint bucket.
188 ///
189 /// Returns `None` when the pipeline has no system-generated [`name`](Self::name).
190 pub fn pipeline_identity(&self) -> Option<PipelineIdentity> {
191 self.name.as_ref().map(|name| PipelineIdentity {
192 name: name.clone(),
193 given_name: self.given_name.clone(),
194 })
195 }
196
197 /// Returns `self.secrets_dir`, or the default secrets directory if it isn't
198 /// set.
199 pub fn secrets_dir(&self) -> &Path {
200 match &self.secrets_dir {
201 Some(dir) => Path::new(dir.as_str()),
202 None => default_secrets_directory(),
203 }
204 }
205
206 /// Abbreviated config that can be printed in the log on pipeline startup.
207 pub fn display_summary(&self) -> String {
208 // TODO: we may want to further abbreviate connector config.
209 let summary = serde_json::json!({
210 "name": self.name,
211 "given_name": self.given_name,
212 "global": self.global,
213 "storage_config": self.storage_config,
214 "secrets_dir": self.secrets_dir,
215 "inputs": self.inputs,
216 "outputs": self.outputs
217 });
218
219 serde_json::to_string_pretty(&summary).unwrap_or_else(|_| "{}".to_string())
220 }
221}
222
223/// A subset of fields in `PipelineConfig` that are generated by the compiler.
224/// These fields are shipped to the pipeline by the compilation server along with
225/// the program binary.
226// Note: An alternative would be to embed these fields in the program binary itself
227// as static strings. This would work well for program IR, but it would require recompiling
228// the program anytime a connector config changes, whereas today connector changes
229// do not require recompilation.
230#[derive(Default, Deserialize, Serialize, Eq, PartialEq, Debug, Clone)]
231pub struct PipelineConfigProgramInfo {
232 /// Input endpoint configuration.
233 pub inputs: BTreeMap<Cow<'static, str>, InputEndpointConfig>,
234
235 /// Output endpoint configuration.
236 #[serde(default)]
237 pub outputs: BTreeMap<Cow<'static, str>, OutputEndpointConfig>,
238
239 /// Program information.
240 #[serde(default)]
241 pub program_ir: Option<ProgramIr>,
242}
243
244/// Configuration for a multihost Feldera pipeline.
245///
246/// This configuration is primarily for the coordinator.
247#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
248pub struct MultihostConfig {
249 /// Number of hosts to launch.
250 ///
251 /// For the configuration to be truly multihost, this should be at least 2.
252 /// A value of 1 still runs the multihost coordinator but it only
253 /// coordinates a single host.
254 pub hosts: usize,
255}
256
257impl Default for MultihostConfig {
258 fn default() -> Self {
259 Self { hosts: 1 }
260 }
261}
262
263/// Configuration for persistent storage in a [`PipelineConfig`].
264#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
265pub struct StorageConfig {
266 /// A directory to keep pipeline state, as a path on the filesystem of the
267 /// machine or container where the pipeline will run.
268 ///
269 /// When storage is enabled, this directory stores the data for
270 /// [StorageBackendConfig::Default].
271 ///
272 /// When fault tolerance is enabled, this directory stores checkpoints and
273 /// the log.
274 pub path: String,
275
276 /// How to cache access to storage in this pipeline.
277 #[serde(default)]
278 pub cache: StorageCacheConfig,
279}
280
281impl StorageConfig {
282 pub fn path(&self) -> &Path {
283 Path::new(&self.path)
284 }
285}
286
287/// How to cache access to storage within a Feldera pipeline.
288#[derive(Copy, Clone, Default, Deserialize, Serialize, Debug, PartialEq, Eq, ToSchema)]
289#[serde(rename_all = "snake_case")]
290pub enum StorageCacheConfig {
291 /// Use the operating system's page cache as the primary storage cache.
292 ///
293 /// This is the default because it currently performs better than
294 /// `FelderaCache`.
295 #[default]
296 PageCache,
297
298 /// Use Feldera's internal cache implementation.
299 ///
300 /// This is under development. It will become the default when its
301 /// performance exceeds that of `PageCache`.
302 FelderaCache,
303}
304
305impl StorageCacheConfig {
306 #[cfg(unix)]
307 pub fn to_custom_open_flags(&self) -> i32 {
308 match self {
309 StorageCacheConfig::PageCache => (),
310 StorageCacheConfig::FelderaCache => {
311 #[cfg(target_os = "linux")]
312 return libc::O_DIRECT;
313 }
314 }
315 0
316 }
317}
318
319/// Storage configuration for a pipeline.
320#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
321#[serde(default)]
322pub struct StorageOptions {
323 /// How to connect to the underlying storage.
324 pub backend: StorageBackendConfig,
325
326 /// For a batch of data maintained as part of a persistent index during a
327 /// pipeline run, the minimum estimated number of bytes to write it to
328 /// storage.
329 ///
330 /// This is provided for debugging and fine-tuning and should ordinarily be
331 /// left unset.
332 ///
333 /// A value of 0 will write even empty batches to storage, and nonzero
334 /// values provide a threshold. `usize::MAX` would effectively disable
335 /// storage for such batches. The default is 10,048,576 (10 MiB).
336 pub min_storage_bytes: Option<usize>,
337
338 /// For a batch of data passed through the pipeline during a single step,
339 /// the minimum estimated number of bytes to write it to storage.
340 ///
341 /// This is provided for debugging and fine-tuning and should ordinarily be
342 /// left unset. A value of 0 will write even empty batches to storage, and
343 /// nonzero values provide a threshold. `usize::MAX`, the default,
344 /// effectively disables storage for such batches. If it is set to another
345 /// value, it should ordinarily be greater than or equal to
346 /// `min_storage_bytes`.
347 pub min_step_storage_bytes: Option<usize>,
348
349 /// The form of compression to use in data batches.
350 ///
351 /// Compression has a CPU cost but it can take better advantage of limited
352 /// NVMe and network bandwidth, which means that it can increase overall
353 /// performance.
354 pub compression: StorageCompression,
355
356 /// The maximum size of the in-memory storage cache, in MiB.
357 ///
358 /// If set, the specified cache size is spread across all the foreground and
359 /// background threads. If unset, each foreground or background thread cache
360 /// is limited to 256 MiB.
361 pub cache_mib: Option<usize>,
362}
363
364/// Backend storage configuration.
365#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
366#[serde(tag = "name", content = "config", rename_all = "snake_case")]
367pub enum StorageBackendConfig {
368 /// Use the default storage configuration.
369 ///
370 /// This currently uses the local file system.
371 #[default]
372 Default,
373
374 /// Use the local file system.
375 ///
376 /// This uses ordinary system file operations.
377 File(Box<FileBackendConfig>),
378
379 /// Object storage.
380 Object(ObjectStorageConfig),
381}
382
383impl Display for StorageBackendConfig {
384 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
385 match self {
386 StorageBackendConfig::Default => write!(f, "default"),
387 StorageBackendConfig::File(_) => write!(f, "file"),
388 StorageBackendConfig::Object(_) => write!(f, "object"),
389 }
390 }
391}
392
393/// Storage compression algorithm.
394#[derive(Debug, Copy, Clone, Default, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
395#[serde(rename_all = "snake_case")]
396pub enum StorageCompression {
397 /// Use Feldera's default compression algorithm.
398 ///
399 /// The default may change as Feldera's performance is tuned and new
400 /// algorithms are introduced.
401 #[default]
402 Default,
403
404 /// Do not compress.
405 None,
406
407 /// Use [Snappy](https://en.wikipedia.org/wiki/Snappy_(compression)) compression.
408 Snappy,
409}
410
411#[derive(Debug, Clone, Eq, PartialEq)]
412pub enum StartFromCheckpoint {
413 Latest,
414 Uuid(uuid::Uuid),
415}
416
417impl ToSchema<'_> for StartFromCheckpoint {
418 fn schema() -> (
419 &'static str,
420 utoipa::openapi::RefOr<utoipa::openapi::schema::Schema>,
421 ) {
422 (
423 "StartFromCheckpoint",
424 utoipa::openapi::RefOr::T(Schema::OneOf(
425 OneOfBuilder::new()
426 .item(
427 ObjectBuilder::new()
428 .schema_type(SchemaType::String)
429 .enum_values(Some(["latest"].into_iter()))
430 .build(),
431 )
432 .item(
433 ObjectBuilder::new()
434 .schema_type(SchemaType::String)
435 .format(Some(utoipa::openapi::SchemaFormat::KnownFormat(
436 utoipa::openapi::KnownFormat::Uuid,
437 )))
438 .build(),
439 )
440 .nullable(true)
441 .build(),
442 )),
443 )
444 }
445}
446
447impl<'de> Deserialize<'de> for StartFromCheckpoint {
448 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
449 where
450 D: Deserializer<'de>,
451 {
452 struct StartFromCheckpointVisitor;
453
454 impl<'de> Visitor<'de> for StartFromCheckpointVisitor {
455 type Value = StartFromCheckpoint;
456
457 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
458 formatter.write_str("a UUID string or the string \"latest\"")
459 }
460
461 fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
462 where
463 E: de::Error,
464 {
465 if value == "latest" {
466 Ok(StartFromCheckpoint::Latest)
467 } else {
468 uuid::Uuid::parse_str(value)
469 .map(StartFromCheckpoint::Uuid)
470 .map_err(|_| E::invalid_value(serde::de::Unexpected::Str(value), &self))
471 }
472 }
473 }
474
475 deserializer.deserialize_str(StartFromCheckpointVisitor)
476 }
477}
478
479impl Serialize for StartFromCheckpoint {
480 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
481 where
482 S: serde::Serializer,
483 {
484 match self {
485 StartFromCheckpoint::Latest => serializer.serialize_str("latest"),
486 StartFromCheckpoint::Uuid(uuid) => serializer.serialize_str(&uuid.to_string()),
487 }
488 }
489}
490
491#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
492pub struct SyncConfig {
493 /// The endpoint URL for the storage service.
494 ///
495 /// This is typically required for custom or local S3-compatible storage providers like MinIO.
496 /// Example: `http://localhost:9000`
497 ///
498 /// Relevant rclone config key: [`endpoint`](https://rclone.org/s3/#s3-endpoint)
499 pub endpoint: Option<String>,
500
501 /// The name of the storage bucket.
502 ///
503 /// This may include a path to a folder inside the bucket (e.g., `my-bucket/data`).
504 pub bucket: String,
505
506 /// The region that this bucket is in.
507 ///
508 /// Leave empty for Minio or the default region (`us-east-1` for AWS).
509 pub region: Option<String>,
510
511 /// The name of the cloud storage provider (e.g., `"AWS"`, `"Minio"`).
512 ///
513 /// Used for provider-specific behavior in rclone.
514 /// If omitted, defaults to `"Other"`.
515 ///
516 /// See [rclone S3 provider documentation](https://rclone.org/s3/#s3-provider)
517 pub provider: Option<String>,
518
519 /// The access key used to authenticate with the storage provider.
520 ///
521 /// If not provided, rclone will fall back to environment-based credentials, such as
522 /// `RCLONE_S3_ACCESS_KEY_ID`. In Kubernetes environments using IRSA (IAM Roles for Service Accounts),
523 /// this can be left empty to allow automatic authentication via the pod's service account.
524 pub access_key: Option<String>,
525
526 /// The secret key used together with the access key for authentication.
527 ///
528 /// If not provided, rclone will fall back to environment-based credentials, such as
529 /// `RCLONE_S3_SECRET_ACCESS_KEY`. In Kubernetes environments using IRSA (IAM Roles for Service Accounts),
530 /// this can be left empty to allow automatic authentication via the pod's service account.
531 pub secret_key: Option<String>,
532
533 /// When set, the pipeline will try fetch the specified checkpoint from the
534 /// object store.
535 ///
536 /// If `fail_if_no_checkpoint` is `true`, the pipeline will fail to initialize.
537 pub start_from_checkpoint: Option<StartFromCheckpoint>,
538
539 /// When true, the pipeline will fail to initialize if fetching the
540 /// specified checkpoint fails (missing, download error).
541 /// When false, the pipeline will start from scratch instead.
542 ///
543 /// False by default.
544 #[schema(default = std::primitive::bool::default)]
545 #[serde(default)]
546 pub fail_if_no_checkpoint: bool,
547
548 /// The number of file transfers to run in parallel.
549 /// Default: 20
550 pub transfers: Option<u8>,
551
552 /// The number of checkers to run in parallel.
553 /// Default: 20
554 pub checkers: Option<u8>,
555
556 /// Set to skip post copy check of checksums, and only check the file sizes.
557 /// This can significantly improve the throughput.
558 /// Defualt: false
559 pub ignore_checksum: Option<bool>,
560
561 /// Number of streams to use for multi-thread downloads.
562 /// Default: 10
563 pub multi_thread_streams: Option<u8>,
564
565 /// Use multi-thread download for files above this size.
566 /// Format: `[size][Suffix]` (Example: 1G, 500M)
567 /// Supported suffixes: k|M|G|T
568 /// Default: 100M
569 pub multi_thread_cutoff: Option<String>,
570
571 /// When true, checkpoint downloads use the maximum resources available on
572 /// the host: `transfers` and `checkers` are scaled to the number of CPUs,
573 /// and the download buffer is allowed to grow up to most of the available
574 /// memory. This maximizes download throughput at the cost of higher CPU and
575 /// memory usage during a pull.
576 ///
577 /// When false, downloads use the values configured via `transfers`,
578 /// `checkers`, and the rclone defaults instead.
579 ///
580 /// Default: true
581 #[schema(default = default_optimize_download_resources)]
582 #[serde(default = "default_optimize_download_resources")]
583 pub optimize_download_resources: bool,
584
585 /// The number of chunks of the same file that are uploaded for multipart uploads.
586 /// Default: 10
587 pub upload_concurrency: Option<u8>,
588
589 /// **Deprecated.** Use `initial=standby` when starting the pipeline instead.
590 #[deprecated(note = "Use `initial=standby` when starting the pipeline instead.")]
591 #[schema(default = std::primitive::bool::default)]
592 #[serde(default)]
593 pub standby: bool,
594
595 /// The interval (in seconds) between each attempt to fetch the latest
596 /// checkpoint from object store while in standby mode.
597 ///
598 /// Applies only when `start_from_checkpoint` is set to `latest`.
599 ///
600 /// Default: 10 seconds
601 #[schema(default = default_pull_interval)]
602 #[serde(default = "default_pull_interval")]
603 pub pull_interval: u64,
604
605 /// The interval (in seconds) between each push of checkpoints to object store.
606 ///
607 /// Default: disabled (no periodic push).
608 #[serde(default)]
609 pub push_interval: Option<u64>,
610
611 /// Extra flags to pass to `rclone`.
612 ///
613 /// WARNING: Supplying incorrect or conflicting flags can break `rclone`.
614 /// Use with caution.
615 ///
616 /// Refer to the docs to see the supported flags:
617 /// - [Global flags](https://rclone.org/flags/)
618 /// - [S3 specific flags](https://rclone.org/s3/)
619 pub flags: Option<Vec<String>>,
620
621 /// The minimum number of checkpoints to retain in object store.
622 /// No checkpoints will be deleted if the total count is below this threshold.
623 ///
624 /// Default: 10
625 #[schema(default = default_retention_min_count)]
626 #[serde(default = "default_retention_min_count")]
627 pub retention_min_count: u32,
628
629 /// The minimum age (in days) a checkpoint must reach before it becomes
630 /// eligible for deletion. All younger checkpoints will be preserved.
631 ///
632 /// Default: 30
633 #[schema(default = default_retention_min_age)]
634 #[serde(default = "default_retention_min_age")]
635 pub retention_min_age: u32,
636
637 /// A read-only bucket used as a fallback checkpoint source.
638 ///
639 /// When the pipeline has no local checkpoint and `bucket` contains no
640 /// checkpoint either, it will attempt to fetch the checkpoint from this
641 /// location instead. All connection settings (`endpoint`, `region`,
642 /// `provider`, `access_key`, `secret_key`) are shared with `bucket`.
643 ///
644 /// The pipeline **never writes** to `read_bucket`.
645 ///
646 /// Must point to a different location than `bucket`.
647 #[serde(default)]
648 pub read_bucket: Option<String>,
649}
650
651fn default_pull_interval() -> u64 {
652 10
653}
654
655fn default_retention_min_count() -> u32 {
656 10
657}
658
659fn default_retention_min_age() -> u32 {
660 30
661}
662
663fn default_optimize_download_resources() -> bool {
664 true
665}
666
667impl SyncConfig {
668 pub fn validate(&self) -> Result<(), String> {
669 #[allow(deprecated)]
670 if self.standby {
671 return Err(
672 "The `standby` config field has been deprecated. Use `initial=standby` when starting the pipeline instead.".to_owned()
673 );
674 }
675
676 if let Some(ref rb) = self.read_bucket
677 && rb == &self.bucket
678 {
679 return Err(
680 "invalid sync config: `read_bucket` and `bucket` must point to different locations"
681 .to_owned(),
682 );
683 }
684
685 Ok(())
686 }
687}
688
689/// Configuration for supplying a custom pipeline StatefulSet template via a Kubernetes ConfigMap.
690///
691/// Operators can provide a custom StatefulSet YAML that the Kubernetes runner will use when
692/// creating pipeline StatefulSets for a pipeline. The custom template must be stored as the
693/// value of a key in a ConfigMap in the same namespace as the pipeline; set `name` to the
694/// ConfigMap name and `key` to the entry that contains the template.
695///
696/// Recommendations and requirements:
697/// - **Start from the default template and modify it as needed.** The default template is present
698/// in ConfigMap named as `<release-name>-pipeline-template`, with key `pipelineTemplate` in the release
699/// namespace and should be used as a reference.
700/// - The template must contain a valid Kubernetes `StatefulSet` manifest in YAML form. The
701/// runner substitutes variables in the template before parsing; therefore the final YAML
702/// must be syntactically valid.
703/// - The runner performs simple string substitution for the following placeholders. Please ensure these
704/// placeholders are placed at appropriate location for their semantics:
705/// - `{id}`: pipeline Kubernetes name (used for object names and labels)
706/// - `{namespace}`: Kubernetes namespace where the pipeline runs
707/// - `{pipeline_executor_image}`: container image used to run the pipeline executor
708/// - `{binary_ref}`: program binary reference passed as an argument
709/// - `{program_info_ref}`: program info reference passed as an argument
710/// - `{pipeline_storage_path}`: mount path for persistent pipeline storage
711/// - `{storage_class_name}`: storage class name to use for PVCs (if applicable)
712/// - `{deployment_id}`: UUID identifying the deployment instance
713/// - `{deployment_initial}`: initial desired runtime status (e.g., `provisioning`)
714/// - `{bootstrap_policy}`: bootstrap policy value when applicable
715#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
716pub struct PipelineTemplateConfig {
717 /// Name of the ConfigMap containing the pipeline template.
718 pub name: String,
719 /// Key in the ConfigMap containing the pipeline template.
720 ///
721 /// If not set, defaults to `pipelineTemplate`.
722 #[schema(default = default_pipeline_template_key)]
723 #[serde(default = "default_pipeline_template_key")]
724 pub key: String,
725}
726
727fn default_pipeline_template_key() -> String {
728 "pipelineTemplate".to_string()
729}
730
731#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
732pub struct ObjectStorageConfig {
733 /// URL.
734 ///
735 /// The following URL schemes are supported:
736 ///
737 /// * S3:
738 /// - `s3://<bucket>/<path>`
739 /// - `s3a://<bucket>/<path>`
740 /// - `https://s3.<region>.amazonaws.com/<bucket>`
741 /// - `https://<bucket>.s3.<region>.amazonaws.com`
742 /// - `https://ACCOUNT_ID.r2.cloudflarestorage.com/bucket`
743 /// * Google Cloud Storage:
744 /// - `gs://<bucket>/<path>`
745 /// * Microsoft Azure Blob Storage:
746 /// - `abfs[s]://<container>/<path>` (according to [fsspec](https://github.com/fsspec/adlfs))
747 /// - `abfs[s]://<file_system>@<account_name>.dfs.core.windows.net/<path>`
748 /// - `abfs[s]://<file_system>@<account_name>.dfs.fabric.microsoft.com/<path>`
749 /// - `az://<container>/<path>` (according to [fsspec](https://github.com/fsspec/adlfs))
750 /// - `adl://<container>/<path>` (according to [fsspec](https://github.com/fsspec/adlfs))
751 /// - `azure://<container>/<path>` (custom)
752 /// - `https://<account>.dfs.core.windows.net`
753 /// - `https://<account>.blob.core.windows.net`
754 /// - `https://<account>.blob.core.windows.net/<container>`
755 /// - `https://<account>.dfs.fabric.microsoft.com`
756 /// - `https://<account>.dfs.fabric.microsoft.com/<container>`
757 /// - `https://<account>.blob.fabric.microsoft.com`
758 /// - `https://<account>.blob.fabric.microsoft.com/<container>`
759 ///
760 /// Settings derived from the URL will override other settings.
761 pub url: String,
762
763 /// Additional options as key-value pairs.
764 ///
765 /// The following keys are supported:
766 ///
767 /// * S3:
768 /// - `access_key_id`: AWS Access Key.
769 /// - `secret_access_key`: AWS Secret Access Key.
770 /// - `region`: Region.
771 /// - `default_region`: Default region.
772 /// - `endpoint`: Custom endpoint for communicating with S3,
773 /// e.g. `https://localhost:4566` for testing against a localstack
774 /// instance.
775 /// - `token`: Token to use for requests (passed to underlying provider).
776 /// - [Other keys](https://docs.rs/object_store/latest/object_store/aws/enum.AmazonS3ConfigKey.html#variants).
777 /// * Google Cloud Storage:
778 /// - `service_account`: Path to the service account file.
779 /// - `service_account_key`: The serialized service account key.
780 /// - `google_application_credentials`: Application credentials path.
781 /// - [Other keys](https://docs.rs/object_store/latest/object_store/gcp/enum.GoogleConfigKey.html).
782 /// * Microsoft Azure Blob Storage:
783 /// - `access_key`: Azure Access Key.
784 /// - `container_name`: Azure Container Name.
785 /// - `account`: Azure Account.
786 /// - `bearer_token_authorization`: Static bearer token for authorizing requests.
787 /// - `client_id`: Client ID for use in client secret or Kubernetes federated credential flow.
788 /// - `client_secret`: Client secret for use in client secret flow.
789 /// - `tenant_id`: Tenant ID for use in client secret or Kubernetes federated credential flow.
790 /// - `endpoint`: Override the endpoint for communicating with blob storage.
791 /// - [Other keys](https://docs.rs/object_store/latest/object_store/azure/enum.AzureConfigKey.html#variants).
792 ///
793 /// Options set through the URL take precedence over those set with these
794 /// options.
795 #[serde(flatten)]
796 pub other_options: BTreeMap<String, String>,
797}
798
799/// Configuration for local file system access.
800#[derive(Debug, Clone, Default, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
801#[serde(default)]
802pub struct FileBackendConfig {
803 /// Whether to use background threads for file I/O.
804 ///
805 /// Background threads should improve performance, but they can reduce
806 /// performance if too few cores are available. This is provided for
807 /// debugging and fine-tuning and should ordinarily be left unset.
808 pub async_threads: Option<bool>,
809
810 /// Per-I/O operation sleep duration, in milliseconds.
811 ///
812 /// This is for simulating slow storage devices. Do not use this in
813 /// production.
814 pub ioop_delay: Option<u64>,
815
816 /// Configuration to synchronize checkpoints to object store.
817 pub sync: Option<SyncConfig>,
818}
819
820/// Global pipeline configuration settings. This is the publicly
821/// exposed type for users to configure pipelines.
822#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, ToSchema)]
823#[serde(default)]
824pub struct RuntimeConfig {
825 /// Number of DBSP worker threads.
826 ///
827 /// Each DBSP "foreground" worker thread is paired with a "background"
828 /// thread for LSM merging, making the total number of threads twice the
829 /// specified number.
830 ///
831 /// The typical sweet spot for the number of workers is between 4 and 16.
832 /// Each worker increases overall memory consumption for data structures
833 /// used during a step.
834 pub workers: u16,
835
836 /// The maximum amount of memory, in Megabytes, that the pipeline is allowed to use
837 /// on each host.
838 ///
839 /// Setting this property activates memory pressure monitoring and backpressure
840 /// mechanisms. The pipeline will track the amount of remaining memory and
841 /// report the memory pressure level via the `memory_pressure` metric.
842 ///
843 /// As the memory pressure increases, the system will apply increasing backpressure
844 /// to push state cached in memory to storage, preventing the pipeline from running
845 /// out of memory at the cost of some performance degradation.
846 ///
847 /// It is strongly recommended to set this property to prevent the pipeline from
848 /// running out of memory. The setting should not exceed the memory limit of the pipeline
849 /// instance.
850 ///
851 /// When `max_rss_mb` is not specified but `resources.memory_mb_max` is set, the
852 /// latter is used as the effective memory cap for the pipeline.
853 ///
854 /// See [documentation on the pipeline's memory usage](https://docs.feldera.com/operations/memory)
855 /// for more details.
856 pub max_rss_mb: Option<u64>,
857
858 /// DataFusion memory pool size, in MB, shared by the ad-hoc query
859 /// engine and the Delta Lake / Iceberg connectors.
860 ///
861 /// Carved out of `max_rss_mb` (falling back to
862 /// `resources.memory_mb_max`); the remainder goes to the DBSP circuit,
863 /// so the two do not double-book RAM.
864 ///
865 /// Unset: defaults to 5% of the effective budget, capped at 2 GB.
866 /// Pipelines that don't run heavy ad-hoc / Delta / Iceberg workloads
867 /// can leave this unset.
868 ///
869 /// Sort/aggregate-heavy ad-hoc queries (especially at high `workers`
870 /// counts) should set this explicitly. An under-sized pool surfaces as
871 /// `ResourcesExhausted` on the failing query — the pipeline keeps
872 /// running and only that query fails.
873 ///
874 /// No pool limit applied if no overall budget is configured.
875 ///
876 /// See [documentation on the pipeline's memory usage](https://docs.feldera.com/operations/memory)
877 /// for more details.
878 pub datafusion_memory_mb: Option<u64>,
879
880 /// Number of DBSP hosts.
881 ///
882 /// The worker threads are evenly divided among the hosts. For single-host
883 /// deployments, this should be 1 (the default).
884 ///
885 /// Multihost pipelines are an enterprise-only preview feature.
886 pub hosts: usize,
887
888 /// Storage configuration.
889 ///
890 /// - If this is `None`, the default, the pipeline's state is kept in
891 /// in-memory data-structures. This is useful if the pipeline's state
892 /// will fit in memory and if the pipeline is ephemeral and does not need
893 /// to be recovered after a restart. The pipeline will most likely run
894 /// faster since it does not need to access storage.
895 ///
896 /// - If set, the pipeline's state is kept on storage. This allows the
897 /// pipeline to work with state that will not fit into memory. It also
898 /// allows the state to be checkpointed and recovered across restarts.
899 #[serde(deserialize_with = "deserialize_storage_options")]
900 pub storage: Option<StorageOptions>,
901
902 /// Fault tolerance configuration.
903 #[serde(deserialize_with = "deserialize_fault_tolerance")]
904 pub fault_tolerance: FtConfig,
905
906 /// Enable CPU profiler.
907 ///
908 /// The default value is `true`.
909 pub cpu_profiler: bool,
910
911 /// Enable pipeline tracing.
912 pub tracing: bool,
913
914 /// Jaeger tracing endpoint to send tracing information to.
915 pub tracing_endpoint_jaeger: String,
916
917 /// Minimal input batch size.
918 ///
919 /// The controller delays pushing input records to the circuit until at
920 /// least `min_batch_size_records` records have been received (total
921 /// across all endpoints) or `max_buffering_delay_usecs` microseconds
922 /// have passed since at least one input records has been buffered.
923 /// Defaults to 0.
924 pub min_batch_size_records: u64,
925
926 /// Maximal delay in microseconds to wait for `min_batch_size_records` to
927 /// get buffered by the controller, defaults to 0.
928 pub max_buffering_delay_usecs: u64,
929
930 /// Resource reservations and limits. This is enforced
931 /// only in Feldera Cloud.
932 pub resources: ResourceConfig,
933
934 /// Real-time clock resolution in microseconds.
935 ///
936 /// This parameter controls the execution of queries that use the `NOW()` function. The output of such
937 /// queries depends on the real-time clock and can change over time without any external
938 /// inputs. If the query uses `NOW()`, the pipeline will update the clock value and trigger incremental
939 /// recomputation at most each `clock_resolution_usecs` microseconds. If the query does not use
940 /// `NOW()`, then clock value updates are suppressed and the pipeline ignores this setting.
941 ///
942 /// It is set to 1 second (1,000,000 microseconds) by default.
943 pub clock_resolution_usecs: Option<u64>,
944
945 /// Fixed timezone offset for the SQL `NOW()` clock.
946 ///
947 /// An ISO-8601 UTC offset, for example `"+05:30"` or `"-08:00"`, that the
948 /// clock connector adds to every `NOW()` value it emits, so `NOW()`
949 /// returns local time in that fixed timezone instead of UTC.
950 ///
951 /// The offset is baked into the pipeline's checkpointed state and cannot
952 /// be changed when the pipeline resumes from a checkpoint: the value from
953 /// the checkpoint stays in effect, and a differing new value is ignored
954 /// with a warning in the pipeline log.
955 #[schema(value_type = Option<String>, example = "+05:30")]
956 pub clock_timezone_offset: Option<ClockTimezoneOffset>,
957
958 /// Optionally, a list of CPU numbers for CPUs to which the pipeline may pin
959 /// its worker threads. Specify at least twice as many CPU numbers as
960 /// workers. CPUs are generally numbered starting from 0. The pipeline
961 /// might not be able to honor CPU pinning requests.
962 ///
963 /// CPU pinning can make pipelines run faster and perform more consistently,
964 /// as long as different pipelines running on the same machine are pinned to
965 /// different CPUs.
966 pub pin_cpus: Vec<usize>,
967
968 /// Timeout in seconds for the `Provisioning` phase of the pipeline.
969 /// Setting this value will override the default of the runner.
970 pub provisioning_timeout_secs: Option<u64>,
971
972 /// The maximum number of connectors initialized in parallel during pipeline
973 /// startup.
974 ///
975 /// At startup, the pipeline must initialize all of its input and output connectors.
976 /// Depending on the number and types of connectors, this can take a long time.
977 /// To accelerate the process, multiple connectors are initialized concurrently.
978 /// This option controls the maximum number of connectors that can be initialized
979 /// in parallel.
980 ///
981 /// The default is 10.
982 pub max_parallel_connector_init: Option<u64>,
983
984 /// Specification of additional (sidecar) containers.
985 pub init_containers: Option<serde_json::Value>,
986
987 /// Deprecated: setting this true or false does not have an effect anymore.
988 pub checkpoint_during_suspend: bool,
989
990 /// Sets the number of available runtime threads for the http server.
991 ///
992 /// In most cases, this does not need to be set explicitly and
993 /// the default is sufficient. Can be increased in case the
994 /// pipeline HTTP API operations are a bottleneck.
995 ///
996 /// If not specified, the default is set to `workers`.
997 pub http_workers: Option<u64>,
998
999 /// Sets the number of available runtime threads for async IO tasks.
1000 ///
1001 /// This affects some networking and file I/O operations
1002 /// especially adapters and ad-hoc queries.
1003 ///
1004 /// In most cases, this does not need to be set explicitly and
1005 /// the default is sufficient. Can be increased in case
1006 /// ingress, egress or ad-hoc queries are a bottleneck.
1007 ///
1008 /// If not specified, the default is set to `workers`.
1009 pub io_workers: Option<u64>,
1010
1011 /// Environment variables for the pipeline process.
1012 ///
1013 /// These are key-value pairs injected into the pipeline process environment.
1014 /// Some variable names are reserved by the platform and cannot be overridden
1015 /// (for example `RUST_LOG`, and variables in the `FELDERA_`,
1016 /// `KUBERNETES_`, and `TOKIO_` namespaces).
1017 #[serde(default)]
1018 pub env: BTreeMap<String, String>,
1019
1020 /// Optional settings for tweaking Feldera internals.
1021 pub dev_tweaks: DevTweaks,
1022
1023 /// Log filtering directives.
1024 ///
1025 /// If set to a valid [tracing-subscriber] filter, this controls the log
1026 /// messages emitted by the pipeline process. Otherwise, or if the filter
1027 /// has invalid syntax, messages at "info" severity and higher are written
1028 /// to the log and all others are discarded.
1029 ///
1030 /// [tracing-subscriber]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives
1031 pub logging: Option<String>,
1032
1033 /// ConfigMap containing a custom pipeline template (Enterprise only).
1034 ///
1035 /// This feature is only available in Feldera Enterprise. If set, the Kubernetes runner
1036 /// will read the template from the specified ConfigMap and use it instead of the default
1037 /// StatefulSet template for the configured pipeline.
1038 ///
1039 /// check [`PipelineTemplateConfig`] documentation for details.
1040 pub pipeline_template_configmap: Option<PipelineTemplateConfig>,
1041}
1042
1043/// Accepts "true" and "false" and converts them to the new format.
1044fn deserialize_storage_options<'de, D>(deserializer: D) -> Result<Option<StorageOptions>, D::Error>
1045where
1046 D: Deserializer<'de>,
1047{
1048 struct BoolOrStruct;
1049
1050 impl<'de> Visitor<'de> for BoolOrStruct {
1051 type Value = Option<StorageOptions>;
1052
1053 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1054 formatter.write_str("boolean or StorageOptions")
1055 }
1056
1057 fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
1058 where
1059 E: de::Error,
1060 {
1061 match v {
1062 false => Ok(None),
1063 true => Ok(Some(StorageOptions::default())),
1064 }
1065 }
1066
1067 fn visit_unit<E>(self) -> Result<Self::Value, E>
1068 where
1069 E: de::Error,
1070 {
1071 Ok(None)
1072 }
1073
1074 fn visit_none<E>(self) -> Result<Self::Value, E>
1075 where
1076 E: de::Error,
1077 {
1078 Ok(None)
1079 }
1080
1081 fn visit_map<M>(self, map: M) -> Result<Option<StorageOptions>, M::Error>
1082 where
1083 M: MapAccess<'de>,
1084 {
1085 Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)).map(Some)
1086 }
1087 }
1088
1089 deserializer.deserialize_any(BoolOrStruct)
1090}
1091
1092/// Accepts very old 'initial_state' and 'latest_checkpoint' as enabling fault
1093/// tolerance.
1094///
1095/// Accepts `null` as disabling fault tolerance.
1096///
1097/// Otherwise, deserializes [FtConfig] in the way that one might otherwise
1098/// expect.
1099fn deserialize_fault_tolerance<'de, D>(deserializer: D) -> Result<FtConfig, D::Error>
1100where
1101 D: Deserializer<'de>,
1102{
1103 struct StringOrStruct;
1104
1105 impl<'de> Visitor<'de> for StringOrStruct {
1106 type Value = FtConfig;
1107
1108 fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1109 formatter.write_str("none or FtConfig or 'initial_state' or 'latest_checkpoint'")
1110 }
1111
1112 fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1113 where
1114 E: de::Error,
1115 {
1116 match v {
1117 "initial_state" | "latest_checkpoint" => Ok(FtConfig {
1118 model: Some(FtModel::default()),
1119 ..FtConfig::default()
1120 }),
1121 _ => Err(de::Error::invalid_value(de::Unexpected::Str(v), &self)),
1122 }
1123 }
1124
1125 fn visit_unit<E>(self) -> Result<Self::Value, E>
1126 where
1127 E: de::Error,
1128 {
1129 Ok(FtConfig::default())
1130 }
1131
1132 fn visit_none<E>(self) -> Result<Self::Value, E>
1133 where
1134 E: de::Error,
1135 {
1136 Ok(FtConfig::default())
1137 }
1138
1139 fn visit_map<M>(self, map: M) -> Result<FtConfig, M::Error>
1140 where
1141 M: MapAccess<'de>,
1142 {
1143 Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
1144 }
1145 }
1146
1147 deserializer.deserialize_any(StringOrStruct)
1148}
1149
1150impl Default for RuntimeConfig {
1151 fn default() -> Self {
1152 Self {
1153 workers: 8,
1154 max_rss_mb: None,
1155 datafusion_memory_mb: None,
1156 hosts: 1,
1157 storage: Some(StorageOptions::default()),
1158 fault_tolerance: FtConfig::default(),
1159 cpu_profiler: true,
1160 tracing: {
1161 // We discovered that the jaeger crate can use up gigabytes of RAM, so it's not harmless
1162 // to keep it on by default.
1163 false
1164 },
1165 tracing_endpoint_jaeger: "127.0.0.1:6831".to_string(),
1166 min_batch_size_records: 0,
1167 max_buffering_delay_usecs: 0,
1168 resources: ResourceConfig::default(),
1169 clock_resolution_usecs: { Some(DEFAULT_CLOCK_RESOLUTION_USECS) },
1170 clock_timezone_offset: None,
1171 pin_cpus: Vec::new(),
1172 provisioning_timeout_secs: None,
1173 max_parallel_connector_init: None,
1174 init_containers: None,
1175 checkpoint_during_suspend: true,
1176 io_workers: None,
1177 http_workers: None,
1178 env: BTreeMap::default(),
1179 dev_tweaks: DevTweaks::default(),
1180 logging: None,
1181 pipeline_template_configmap: None,
1182 }
1183 }
1184}
1185
1186/// Upper bound on the default DataFusion pool size, in MB.
1187///
1188/// Spill-to-disk handles overflow; reserving more starves the circuit.
1189pub const DEFAULT_DATAFUSION_MEMORY_MB_CEILING: u64 = 2048;
1190
1191/// Default DataFusion pool size as a percentage of the pipeline's
1192/// effective memory budget. The remainder is left for the DBSP circuit.
1193pub const DEFAULT_DATAFUSION_MEMORY_PERCENT: u64 = 5;
1194
1195impl RuntimeConfig {
1196 /// Pipeline's effective memory budget in MB: `max_rss_mb`, falling back
1197 /// to `resources.memory_mb_max` (the k8s pod limit).
1198 pub fn effective_memory_mb(&self) -> Option<u64> {
1199 self.max_rss_mb.or(self.resources.memory_mb_max)
1200 }
1201
1202 /// Resolved DataFusion pool size in MB: explicit `datafusion_memory_mb`
1203 /// if set, else 5% of the effective budget capped at
1204 /// `DEFAULT_DATAFUSION_MEMORY_MB_CEILING`. `None` if no budget is
1205 /// configured.
1206 pub fn resolved_datafusion_memory_mb(&self) -> Option<u64> {
1207 if let Some(explicit) = self.datafusion_memory_mb {
1208 return Some(explicit);
1209 }
1210 let effective = self.effective_memory_mb()?;
1211 let fraction = effective * DEFAULT_DATAFUSION_MEMORY_PERCENT / 100;
1212 Some(fraction.min(DEFAULT_DATAFUSION_MEMORY_MB_CEILING))
1213 }
1214}
1215
1216/// Fault-tolerance configuration.
1217///
1218/// The default [FtConfig] (via [FtConfig::default]) disables fault tolerance,
1219/// which is the configuration that one gets if [RuntimeConfig] omits fault
1220/// tolerance configuration.
1221///
1222/// The default value for [FtConfig::model] enables fault tolerance, as
1223/// `Some(FtModel::default())`. This is the configuration that one gets if
1224/// [RuntimeConfig] includes a fault tolerance configuration but does not
1225/// specify a particular model.
1226#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
1227#[serde(rename_all = "snake_case")]
1228pub struct FtConfig {
1229 /// Fault tolerance model to use.
1230 #[serde(with = "none_as_string")]
1231 #[serde(default = "default_model")]
1232 #[schema(
1233 schema_with = none_as_string_schema::<FtModel>,
1234 )]
1235 pub model: Option<FtModel>,
1236
1237 /// Interval between automatic checkpoints, in seconds.
1238 ///
1239 /// The default is 60 seconds. Values less than 1 or greater than 3600 will
1240 /// be forced into that range.
1241 #[serde(default = "default_checkpoint_interval_secs")]
1242 pub checkpoint_interval_secs: Option<u64>,
1243}
1244
1245fn default_model() -> Option<FtModel> {
1246 Some(FtModel::default())
1247}
1248
1249pub fn default_checkpoint_interval_secs() -> Option<u64> {
1250 Some(60)
1251}
1252
1253impl Default for FtConfig {
1254 fn default() -> Self {
1255 Self {
1256 model: None,
1257 checkpoint_interval_secs: default_checkpoint_interval_secs(),
1258 }
1259 }
1260}
1261
1262#[cfg(test)]
1263mod test {
1264 use super::deserialize_fault_tolerance;
1265 use crate::config::{
1266 ConnectorConfig, DEFAULT_DATAFUSION_MEMORY_MB_CEILING, FtConfig, FtModel, PipelineConfig,
1267 ResourceConfig, RuntimeConfig, TransportConfig,
1268 };
1269 use serde::{Deserialize, Serialize};
1270 use serde_json::json;
1271
1272 /// A configuration written by an older version of Feldera carries no
1273 /// `soft_delete` field, and a connector that does not use soft deletes
1274 /// serializes exactly as it did before the field existed. This keeps
1275 /// configurations readable and writable across versions.
1276 #[test]
1277 fn soft_delete_is_backward_compatible() {
1278 let config: ConnectorConfig = serde_json::from_value(json!({
1279 "transport": {"name": "empty_input"}
1280 }))
1281 .unwrap();
1282 assert!(!config.soft_delete);
1283
1284 let serialized = serde_json::to_value(&config).unwrap();
1285 assert_eq!(serialized.get("soft_delete"), None);
1286
1287 let config: ConnectorConfig = serde_json::from_value(json!({
1288 "transport": {"name": "empty_input"},
1289 "soft_delete": true
1290 }))
1291 .unwrap();
1292 assert!(config.soft_delete);
1293 assert_eq!(
1294 serde_json::to_value(&config).unwrap().get("soft_delete"),
1295 Some(&json!(true))
1296 );
1297 }
1298
1299 /// Soft deletes change how input is interpreted, so flipping the flag must
1300 /// count as a connector change even where flow-control settings do not.
1301 #[test]
1302 fn soft_delete_invalidates_checkpointed_connector_state() {
1303 let config = ConnectorConfig::new(TransportConfig::EmptyInput, None);
1304 let mut soft_delete = config.clone();
1305 soft_delete.soft_delete = true;
1306
1307 assert!(config.equal_for_input_checkpoint_replay(&config));
1308 assert!(!config.equal_for_input_checkpoint_replay(&soft_delete));
1309 }
1310
1311 fn config_with_name(name: Option<&str>) -> PipelineConfig {
1312 PipelineConfig {
1313 global: RuntimeConfig::default(),
1314 multihost: None,
1315 name: name.map(str::to_string),
1316 given_name: None,
1317 storage_config: None,
1318 secrets_dir: None,
1319 inputs: Default::default(),
1320 outputs: Default::default(),
1321 program_ir: None,
1322 }
1323 }
1324
1325 #[test]
1326 fn pipeline_identity_uses_system_and_given_names() {
1327 let mut config = config_with_name(Some("pipeline-018f6f57-4e15-7438-91af-3fd21ff2a8d3"));
1328 config.given_name = Some("my-pipeline".to_string());
1329
1330 let metadata = config.pipeline_identity().unwrap();
1331 assert_eq!(
1332 metadata.name,
1333 "pipeline-018f6f57-4e15-7438-91af-3fd21ff2a8d3"
1334 );
1335 assert_eq!(metadata.given_name.as_deref(), Some("my-pipeline"));
1336 }
1337
1338 #[test]
1339 fn pipeline_identity_is_none_without_system_name() {
1340 assert_eq!(config_with_name(None).pipeline_identity(), None);
1341 }
1342
1343 #[test]
1344 fn resolved_datafusion_memory_explicit_passes_through() {
1345 let config = RuntimeConfig {
1346 max_rss_mb: Some(8_000),
1347 datafusion_memory_mb: Some(1_500),
1348 ..Default::default()
1349 };
1350 assert_eq!(config.resolved_datafusion_memory_mb(), Some(1_500));
1351 }
1352
1353 #[test]
1354 fn resolved_datafusion_memory_unconfigured_returns_none() {
1355 let config = RuntimeConfig::default();
1356 assert!(config.max_rss_mb.is_none());
1357 assert!(config.resources.memory_mb_max.is_none());
1358 assert_eq!(config.resolved_datafusion_memory_mb(), None);
1359 }
1360
1361 #[test]
1362 fn resolved_datafusion_memory_small_budget_scales_down() {
1363 // Small pipelines must provision cleanly; the default just shrinks.
1364 let config = RuntimeConfig {
1365 max_rss_mb: Some(256),
1366 ..Default::default()
1367 };
1368 assert_eq!(config.resolved_datafusion_memory_mb(), Some(12));
1369
1370 let config = RuntimeConfig {
1371 max_rss_mb: Some(512),
1372 ..Default::default()
1373 };
1374 assert_eq!(config.resolved_datafusion_memory_mb(), Some(25));
1375 }
1376
1377 #[test]
1378 fn resolved_datafusion_memory_clamps_to_ceiling_for_large_budgets() {
1379 // 5% of 64 GB = 3.2 GB, above the 2 GB ceiling.
1380 let config = RuntimeConfig {
1381 max_rss_mb: Some(64_000),
1382 ..Default::default()
1383 };
1384 assert_eq!(
1385 config.resolved_datafusion_memory_mb(),
1386 Some(DEFAULT_DATAFUSION_MEMORY_MB_CEILING),
1387 );
1388 }
1389
1390 #[test]
1391 fn resolved_datafusion_memory_midrange_uses_five_percent() {
1392 // 5% of 16 GB = 800 MB, inside the clamp range.
1393 let config = RuntimeConfig {
1394 max_rss_mb: Some(16_000),
1395 ..Default::default()
1396 };
1397 assert_eq!(config.resolved_datafusion_memory_mb(), Some(800));
1398 }
1399
1400 #[test]
1401 fn resolved_datafusion_memory_falls_back_to_resources() {
1402 // No max_rss_mb, but resources.memory_mb_max is set.
1403 let config = RuntimeConfig {
1404 max_rss_mb: None,
1405 resources: ResourceConfig {
1406 memory_mb_max: Some(16_000),
1407 ..Default::default()
1408 },
1409 ..Default::default()
1410 };
1411 assert_eq!(config.resolved_datafusion_memory_mb(), Some(800));
1412 }
1413
1414 #[test]
1415 fn ft_config() {
1416 #[derive(Serialize, Deserialize, Default, PartialEq, Eq, Debug)]
1417 #[serde(default)]
1418 struct Wrapper {
1419 #[serde(deserialize_with = "deserialize_fault_tolerance")]
1420 config: FtConfig,
1421 }
1422
1423 // Omitting FtConfig, or specifying null, or specifying model "none", disables fault tolerance.
1424 for s in [
1425 "{}",
1426 r#"{"config": null}"#,
1427 r#"{"config": {"model": "none"}}"#,
1428 ] {
1429 let config: Wrapper = serde_json::from_str(s).unwrap();
1430 assert_eq!(
1431 config,
1432 Wrapper {
1433 config: FtConfig {
1434 model: None,
1435 checkpoint_interval_secs: Some(60)
1436 }
1437 }
1438 );
1439 }
1440
1441 // Serializing disabled FT produces explicit "none" form.
1442 let s = serde_json::to_string(&Wrapper {
1443 config: FtConfig::default(),
1444 })
1445 .unwrap();
1446 assert!(s.contains("\"none\""));
1447
1448 // `{}` for FtConfig, or `{...}` with `model` omitted, enables fault
1449 // tolerance.
1450 for s in [r#"{"config": {}}"#, r#"{"checkpoint_interval_secs": 60}"#] {
1451 assert_eq!(
1452 serde_json::from_str::<FtConfig>(s).unwrap(),
1453 FtConfig {
1454 model: Some(FtModel::default()),
1455 checkpoint_interval_secs: Some(60)
1456 }
1457 );
1458 }
1459
1460 // `"checkpoint_interval_secs": null` disables periodic checkpointing.
1461 assert_eq!(
1462 serde_json::from_str::<FtConfig>(r#"{"checkpoint_interval_secs": null}"#).unwrap(),
1463 FtConfig {
1464 model: Some(FtModel::default()),
1465 checkpoint_interval_secs: None
1466 }
1467 );
1468 }
1469}
1470
1471impl FtConfig {
1472 pub fn is_enabled(&self) -> bool {
1473 self.model.is_some()
1474 }
1475
1476 /// Returns the checkpoint interval, if fault tolerance is enabled, and
1477 /// otherwise `None`.
1478 pub fn checkpoint_interval(&self) -> Option<Duration> {
1479 if self.is_enabled() {
1480 self.checkpoint_interval_secs
1481 .map(|interval| Duration::from_secs(interval.clamp(1, 3600)))
1482 } else {
1483 None
1484 }
1485 }
1486}
1487
1488/// Serde implementation for de/serializing a string into `Option<T>` where
1489/// `"none"` indicates `None` and any other string indicates `Some`.
1490///
1491/// This could be extended to handle non-strings by adding more forwarding
1492/// `visit_*` methods to the Visitor implementation. I don't see a way to write
1493/// them automatically.
1494mod none_as_string {
1495 use std::marker::PhantomData;
1496
1497 use serde::de::{Deserialize, Deserializer, IntoDeserializer, Visitor};
1498 use serde::ser::{Serialize, Serializer};
1499
1500 pub(super) fn serialize<S, T>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
1501 where
1502 S: Serializer,
1503 T: Serialize,
1504 {
1505 match value.as_ref() {
1506 Some(value) => value.serialize(serializer),
1507 None => "none".serialize(serializer),
1508 }
1509 }
1510
1511 struct NoneAsString<T>(PhantomData<fn() -> T>);
1512
1513 impl<'de, T> Visitor<'de> for NoneAsString<T>
1514 where
1515 T: Deserialize<'de>,
1516 {
1517 type Value = Option<T>;
1518
1519 fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1520 formatter.write_str("string")
1521 }
1522
1523 fn visit_none<E>(self) -> Result<Self::Value, E>
1524 where
1525 E: serde::de::Error,
1526 {
1527 Ok(None)
1528 }
1529
1530 fn visit_str<E>(self, value: &str) -> Result<Option<T>, E>
1531 where
1532 E: serde::de::Error,
1533 {
1534 if &value.to_ascii_lowercase() == "none" {
1535 Ok(None)
1536 } else {
1537 Ok(Some(T::deserialize(value.into_deserializer())?))
1538 }
1539 }
1540 }
1541
1542 pub(super) fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
1543 where
1544 D: Deserializer<'de>,
1545 T: Deserialize<'de>,
1546 {
1547 deserializer.deserialize_str(NoneAsString(PhantomData))
1548 }
1549}
1550
1551/// Generates an OpenAPI schema for an `Option<T>` field serialized with `none_as_string`.
1552/// The schema is a `oneOf` with a reference to `T`'s schema and a `"none"` string enum.
1553fn none_as_string_schema<'a, T: ToSchema<'a> + Default + Serialize>() -> Schema {
1554 Schema::OneOf(
1555 OneOfBuilder::new()
1556 .item(RefOr::Ref(Ref::new(format!(
1557 "#/components/schemas/{}",
1558 T::schema().0
1559 ))))
1560 .item(
1561 ObjectBuilder::new()
1562 .schema_type(SchemaType::String)
1563 .enum_values(Some(vec!["none"])),
1564 )
1565 .default(Some(
1566 serde_json::to_value(T::default()).expect("Failed to serialize default value"),
1567 ))
1568 .build(),
1569 )
1570}
1571
1572/// Fault tolerance model.
1573///
1574/// The ordering is significant: we consider [Self::ExactlyOnce] to be a "higher
1575/// level" of fault tolerance than [Self::AtLeastOnce].
1576#[derive(
1577 Debug, Copy, Clone, Default, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
1578)]
1579#[serde(rename_all = "snake_case")]
1580pub enum FtModel {
1581 /// Each record is output at least once. Crashes may duplicate output, but
1582 /// no input or output is dropped.
1583 AtLeastOnce,
1584
1585 /// Each record is output exactly once. Crashes do not drop or duplicate
1586 /// input or output.
1587 #[default]
1588 ExactlyOnce,
1589}
1590
1591impl FtModel {
1592 pub fn option_as_str(value: Option<FtModel>) -> &'static str {
1593 value.map_or("no", |model| model.as_str())
1594 }
1595
1596 pub fn as_str(&self) -> &'static str {
1597 match self {
1598 FtModel::AtLeastOnce => "at_least_once",
1599 FtModel::ExactlyOnce => "exactly_once",
1600 }
1601 }
1602}
1603
1604pub struct FtModelUnknown;
1605
1606impl FromStr for FtModel {
1607 type Err = FtModelUnknown;
1608
1609 fn from_str(s: &str) -> Result<Self, Self::Err> {
1610 match s.to_ascii_lowercase().as_str() {
1611 "exactly_once" => Ok(Self::ExactlyOnce),
1612 "at_least_once" => Ok(Self::AtLeastOnce),
1613 _ => Err(FtModelUnknown),
1614 }
1615 }
1616}
1617
1618/// Describes an input connector configuration
1619#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
1620pub struct InputEndpointConfig {
1621 /// The name of the input stream of the circuit that this endpoint is
1622 /// connected to.
1623 pub stream: Cow<'static, str>,
1624
1625 /// Connector configuration.
1626 #[serde(flatten)]
1627 pub connector_config: ConnectorConfig,
1628}
1629
1630impl InputEndpointConfig {
1631 pub fn new(stream: impl Into<Cow<'static, str>>, connector_config: ConnectorConfig) -> Self {
1632 Self {
1633 stream: stream.into(),
1634 connector_config,
1635 }
1636 }
1637}
1638
1639/// Deserialize the `start_after` property of a connector configuration.
1640/// It requires a non-standard deserialization because we want to accept
1641/// either a string or an array of strings.
1642fn deserialize_start_after<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
1643where
1644 D: Deserializer<'de>,
1645{
1646 let value = Option::<JsonValue>::deserialize(deserializer)?;
1647 match value {
1648 Some(JsonValue::String(s)) => Ok(Some(vec![s])),
1649 Some(JsonValue::Array(arr)) => {
1650 let vec = arr
1651 .into_iter()
1652 .map(|item| {
1653 item.as_str()
1654 .map(|s| s.to_string())
1655 .ok_or_else(|| serde::de::Error::custom("invalid 'start_after' property: expected a string, an array of strings, or null"))
1656 })
1657 .collect::<Result<Vec<String>, _>>()?;
1658 Ok(Some(vec))
1659 }
1660 Some(JsonValue::Null) | None => Ok(None),
1661 _ => Err(serde::de::Error::custom(
1662 "invalid 'start_after' property: expected a string, an array of strings, or null",
1663 )),
1664 }
1665}
1666
1667#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
1668pub struct ConnectorConfig {
1669 /// Send a full snapshot of a materialized view when the connector first
1670 /// starts. Valid for output connectors only.
1671 ///
1672 /// When `true`, the pipeline emits the current contents of the view as the
1673 /// initial batch the first time the connector runs. The view must be
1674 /// materialized (declared with `CREATE MATERIALIZED VIEW`).
1675 ///
1676 /// The snapshot is sent exactly once per connector lifetime: it does not
1677 /// fire again when the pipeline resumes from a checkpoint. Modifying the
1678 /// connector configuration or invoking the reset API triggers a fresh
1679 /// snapshot when the connector supports reset (e.g., Delta Lake in
1680 /// `truncate` mode and Postgres).
1681 #[serde(default)]
1682 pub send_snapshot: bool,
1683
1684 /// Ingest deletions as insertions, recording the original polarity in the
1685 /// `is_delete` metadata attribute. Valid for input connectors only.
1686 ///
1687 /// When `true`, a delete received by the connector is pushed to the table
1688 /// as an insertion of the same record, and the connector attaches the
1689 /// `is_delete` metadata attribute set to `true` to it. Insertions carry no
1690 /// `is_delete` attribute, so a column declared as
1691 /// `DEFAULT CAST(CONNECTOR_METADATA()['is_delete'] AS BOOLEAN)` is `NULL`
1692 /// for them. The table then contains the entire history of the input
1693 /// stream instead of tracking its current contents.
1694 ///
1695 /// Only tables without a primary key support this mode, since deletions in
1696 /// a table with a primary key delete a key rather than a record.
1697 ///
1698 /// Versions of Feldera that predate this option ignore it and apply
1699 /// deletions as regular deletions.
1700 #[serde(default, skip_serializing_if = "std::ops::Not::not")]
1701 pub soft_delete: bool,
1702
1703 /// Transport endpoint configuration.
1704 pub transport: TransportConfig,
1705
1706 /// Optional preprocessor configuration
1707 #[serde(skip_serializing_if = "Option::is_none")]
1708 pub preprocessor: Option<Vec<PreprocessorConfig>>,
1709
1710 /// Parser configuration.
1711 pub format: Option<FormatConfig>,
1712
1713 /// Optional postprocessor configuration
1714 #[serde(skip_serializing_if = "Option::is_none")]
1715 pub postprocessor: Option<Vec<PostprocessorConfig>>,
1716
1717 /// Name of the index that the connector is attached to.
1718 ///
1719 /// This property is valid for output connectors only. It is used with data
1720 /// transports and formats that expect output updates in the form of key/value
1721 /// pairs, where the key typically represents a unique id associated with the
1722 /// table or view.
1723 ///
1724 /// To support such output formats, an output connector can be attached to an
1725 /// index created using the SQL CREATE INDEX statement. An index of a table
1726 /// or view contains the same updates as the table or view itself, indexed by
1727 /// one or more key columns.
1728 ///
1729 /// See individual connector documentation for details on how they work
1730 /// with indexes.
1731 pub index: Option<String>,
1732
1733 /// Output buffer configuration.
1734 #[serde(flatten)]
1735 pub output_buffer_config: OutputBufferConfig,
1736
1737 /// Maximum number of records from this connector to process in a single batch.
1738 ///
1739 /// When set, this caps how many records are taken from the connector’s input
1740 /// buffer and pushed through the circuit at once.
1741 ///
1742 /// This is typically configured lower than `max_queued_records` to allow the
1743 /// connector time to restart and refill its buffer while a batch is being
1744 /// processed.
1745 ///
1746 /// Not all input adapters honor this limit.
1747 ///
1748 /// If this is not set, the batch size is derived from `max_worker_batch_size`.
1749 #[serde(skip_serializing_if = "Option::is_none")]
1750 pub max_batch_size: Option<u64>,
1751
1752 /// Maximum number of records processed per batch, per worker thread.
1753 ///
1754 /// When `max_batch_size` is not set, this setting is used to cap
1755 /// the number of records that can be taken from the connector’s input
1756 /// buffer and pushed through the circuit at once. The effective batch size is computed as:
1757 /// `max_worker_batch_size × workers`.
1758 ///
1759 /// This provides an alternative to `max_batch_size` that automatically adjusts batch
1760 /// size as the number of worker threads changes to maintain constant amount of
1761 /// work per worker per batch.
1762 ///
1763 /// Defaults to 10,000 records per worker.
1764 #[serde(skip_serializing_if = "Option::is_none")]
1765 pub max_worker_batch_size: Option<u64>,
1766
1767 /// Backpressure threshold, in records.
1768 ///
1769 /// Maximal number of records queued by the endpoint before the endpoint
1770 /// is paused by the backpressure mechanism.
1771 ///
1772 /// For input endpoints, this setting bounds the number of records that have
1773 /// been received from the input transport but haven't yet been consumed by
1774 /// the circuit, since the circuit is still busy processing previous inputs.
1775 ///
1776 /// For output endpoints, this setting bounds the number of records that have
1777 /// been produced by the circuit but not yet sent via the output transport endpoint
1778 /// nor stored in the output buffer (see `enable_output_buffer`).
1779 ///
1780 /// Note that this is not a hard bound: there can be a small delay between
1781 /// the backpressure mechanism is triggered and the endpoint is paused, during
1782 /// which more data may be queued.
1783 ///
1784 /// The default is 1 million.
1785 #[serde(default = "default_max_queued_records")]
1786 pub max_queued_records: u64,
1787
1788 /// Backpressure threshold, in bytes.
1789 ///
1790 /// Maximal number of bytes queued by the endpoint before the endpoint
1791 /// is paused by the backpressure mechanism.
1792 ///
1793 /// For input endpoints, this setting bounds the number of bytes that have
1794 /// been received from the input transport but haven't yet been consumed by
1795 /// the circuit since the circuit, since the circuit is still busy processing
1796 /// previous inputs.
1797 ///
1798 /// This setting is not yet implemented for output endpoints.
1799 ///
1800 /// Note that this is not a hard bound: there can be a small delay between
1801 /// the backpressure mechanism is triggered and the endpoint is paused, during
1802 /// which more data may be queued.
1803 ///
1804 /// When this is unspecified, it defaults to `1000 * max_queued_records`.
1805 #[serde(skip_serializing_if = "Option::is_none")]
1806 pub max_queued_bytes: Option<u64>,
1807
1808 /// Create connector in paused state.
1809 ///
1810 /// The default is `false`.
1811 #[serde(default)]
1812 pub paused: bool,
1813
1814 /// Arbitrary user-defined text labels associated with the connector.
1815 ///
1816 /// These labels can be used in conjunction with the `start_after` property
1817 /// to control the start order of connectors.
1818 #[serde(default)]
1819 pub labels: Vec<String>,
1820
1821 /// Start the connector after all connectors with specified labels.
1822 ///
1823 /// This property is used to control the start order of connectors.
1824 /// The connector will not start until all connectors with the specified
1825 /// labels have finished processing all inputs.
1826 #[serde(deserialize_with = "deserialize_start_after")]
1827 #[serde(default)]
1828 pub start_after: Option<Vec<String>>,
1829}
1830
1831impl ConnectorConfig {
1832 pub fn new(transport: TransportConfig, format: Option<FormatConfig>) -> Self {
1833 Self {
1834 send_snapshot: false,
1835 soft_delete: false,
1836 transport,
1837 preprocessor: None,
1838 format,
1839 postprocessor: None,
1840 index: None,
1841 output_buffer_config: Default::default(),
1842 max_batch_size: None,
1843 max_worker_batch_size: None,
1844 max_queued_records: default_max_queued_records(),
1845 max_queued_bytes: None,
1846 paused: false,
1847 labels: Vec::new(),
1848 start_after: None,
1849 }
1850 }
1851
1852 pub fn with_max_batch_size(mut self, max_batch_size: Option<u64>) -> Self {
1853 self.max_batch_size = max_batch_size;
1854 self
1855 }
1856
1857 pub fn with_max_queued_records(mut self, max_queued_records: u64) -> Self {
1858 self.max_queued_records = max_queued_records;
1859 self
1860 }
1861
1862 /// Compare two configs modulo the `paused` field.
1863 ///
1864 /// Used to compare checkpointed and current connector configs.
1865 pub fn equal_modulo_paused(&self, other: &Self) -> bool {
1866 let mut a = self.clone();
1867 let mut b = other.clone();
1868 a.paused = false;
1869 b.paused = false;
1870 a == b
1871 }
1872
1873 /// Compare two input connector configs modulo fields that only affect
1874 /// runtime flow control and do not invalidate checkpointed connector state.
1875 pub fn equal_for_input_checkpoint_replay(&self, other: &Self) -> bool {
1876 let mut a = self.clone();
1877 let mut b = other.clone();
1878 a.normalize_for_input_checkpoint_replay();
1879 b.normalize_for_input_checkpoint_replay();
1880 a == b
1881 }
1882
1883 fn normalize_for_input_checkpoint_replay(&mut self) {
1884 self.paused = false;
1885 self.max_batch_size = None;
1886 self.max_worker_batch_size = None;
1887 self.max_queued_records = default_max_queued_records();
1888 self.max_queued_bytes = None;
1889 }
1890
1891 /// Adopt input connector settings that are safe to change while replaying
1892 /// checkpointed connector state.
1893 pub fn apply_input_checkpoint_replay_config_from(&mut self, other: &Self) {
1894 self.max_batch_size = other.max_batch_size;
1895 self.max_worker_batch_size = other.max_worker_batch_size;
1896 self.max_queued_records = other.max_queued_records;
1897 self.max_queued_bytes = other.max_queued_bytes;
1898 }
1899
1900 /// Returns `max_queued_records` or, if it is not set, the default.
1901 pub fn max_queued_records(&self) -> u64 {
1902 self.max_queued_records
1903 }
1904
1905 /// Returns `max_queued_bytes` or, if it is not set, the default based on
1906 /// `max_queued_records`.
1907 pub fn max_queued_bytes(&self) -> u64 {
1908 self.max_queued_bytes
1909 .unwrap_or_else(|| self.max_queued_records().saturating_mul(1000))
1910 }
1911}
1912
1913#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
1914#[serde(default)]
1915pub struct OutputBufferConfig {
1916 /// Enable output buffering.
1917 ///
1918 /// The output buffering mechanism allows decoupling the rate at which the pipeline
1919 /// pushes changes to the output transport from the rate of input changes.
1920 ///
1921 /// By default, output updates produced by the pipeline are pushed directly to
1922 /// the output transport. Some destinations may prefer to receive updates in fewer
1923 /// bigger batches. For instance, when writing Parquet files, producing
1924 /// one bigger file every few minutes is usually better than creating
1925 /// small files every few milliseconds.
1926 ///
1927 /// To achieve such input/output decoupling, users can enable output buffering by
1928 /// setting the `enable_output_buffer` flag to `true`. When buffering is enabled, output
1929 /// updates produced by the pipeline are consolidated in an internal buffer and are
1930 /// pushed to the output transport when one of several conditions is satisfied:
1931 ///
1932 /// * data has been accumulated in the buffer for more than `max_output_buffer_time_millis`
1933 /// milliseconds.
1934 /// * buffer size exceeds `max_output_buffer_size_records` records.
1935 ///
1936 /// This flag is `false` by default.
1937 // TODO: on-demand output triggered via the API.
1938 pub enable_output_buffer: bool,
1939
1940 /// Maximum time in milliseconds data is kept in the output buffer.
1941 ///
1942 /// By default, data is kept in the buffer indefinitely until one of
1943 /// the other output conditions is satisfied. When this option is
1944 /// set the buffer will be flushed at most every
1945 /// `max_output_buffer_time_millis` milliseconds.
1946 ///
1947 /// NOTE: this configuration option requires the `enable_output_buffer` flag
1948 /// to be set.
1949 pub max_output_buffer_time_millis: usize,
1950
1951 /// Maximum number of updates to be kept in the output buffer.
1952 ///
1953 /// This parameter bounds the maximal size of the buffer.
1954 /// Note that the size of the buffer is not always equal to the
1955 /// total number of updates output by the pipeline. Updates to the
1956 /// same record can overwrite or cancel previous updates.
1957 ///
1958 /// The default is 10,000,000.
1959 ///
1960 /// NOTE: this configuration option requires the `enable_output_buffer` flag
1961 /// to be set.
1962 pub max_output_buffer_size_records: usize,
1963}
1964
1965impl Default for OutputBufferConfig {
1966 fn default() -> Self {
1967 Self {
1968 enable_output_buffer: false,
1969 max_output_buffer_size_records: DEFAULT_MAX_OUTPUT_BUFFER_SIZE_RECORDS,
1970 max_output_buffer_time_millis: usize::MAX,
1971 }
1972 }
1973}
1974
1975/// Describes an output connector configuration
1976#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
1977pub struct OutputEndpointConfig {
1978 /// The name of the output stream of the circuit that this endpoint is
1979 /// connected to.
1980 pub stream: Cow<'static, str>,
1981
1982 /// Connector configuration.
1983 #[serde(flatten)]
1984 pub connector_config: ConnectorConfig,
1985}
1986
1987impl OutputEndpointConfig {
1988 pub fn new(stream: impl Into<Cow<'static, str>>, connector_config: ConnectorConfig) -> Self {
1989 Self {
1990 stream: stream.into(),
1991 connector_config,
1992 }
1993 }
1994}
1995
1996/// Transport-specific endpoint configuration passed to
1997/// `crate::OutputTransport::new_endpoint`
1998/// and `crate::InputTransport::new_endpoint`.
1999#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
2000#[serde(tag = "name", content = "config", rename_all = "snake_case")]
2001pub enum TransportConfig {
2002 FileInput(FileInputConfig),
2003 FileOutput(FileOutputConfig),
2004 NatsInput(NatsInputConfig),
2005 KafkaInput(KafkaInputConfig),
2006 KafkaOutput(KafkaOutputConfig),
2007 PubSubInput(PubSubInputConfig),
2008 UrlInput(UrlInputConfig),
2009 S3Input(S3InputConfig),
2010 DeltaTableInput(DeltaTableReaderConfig),
2011 DeltaTableOutput(DeltaTableWriterConfig),
2012 // Snake case would rename "DynamoDBOutput" to `dynamo_db_output`.
2013 // However, DynamoDB is a single word, so override the tag to `dynamodb_output`.
2014 #[serde(rename = "dynamodb_output")]
2015 DynamoDBOutput(DynamoDBWriterConfig),
2016 RedisOutput(RedisOutputConfig),
2017 // Prevent rust from complaining about large size difference between enum variants.
2018 IcebergInput(Box<IcebergReaderConfig>),
2019 PostgresInput(PostgresReaderConfig),
2020 PostgresCdcInput(PostgresCdcReaderConfig),
2021 PostgresOutput(PostgresWriterConfig),
2022 Datagen(DatagenInputConfig),
2023 Nexmark(NexmarkInputConfig),
2024 /// Direct HTTP input: cannot be instantiated through API
2025 HttpInput(HttpInputConfig),
2026 /// Direct HTTP output: cannot be instantiated through API
2027 HttpOutput(HttpOutputConfig),
2028 /// Ad hoc input: cannot be instantiated through API
2029 AdHocInput(AdHocInputConfig),
2030 ClockInput(ClockConfig),
2031 /// Output connector that discards all data.
2032 NullOutput,
2033 /// Input connector that produces no data.
2034 EmptyInput,
2035}
2036
2037impl TransportConfig {
2038 pub fn name(&self) -> String {
2039 match self {
2040 TransportConfig::FileInput(_) => "file_input".to_string(),
2041 TransportConfig::FileOutput(_) => "file_output".to_string(),
2042 TransportConfig::NatsInput(_) => "nats_input".to_string(),
2043 TransportConfig::KafkaInput(_) => "kafka_input".to_string(),
2044 TransportConfig::KafkaOutput(_) => "kafka_output".to_string(),
2045 TransportConfig::PubSubInput(_) => "pub_sub_input".to_string(),
2046 TransportConfig::UrlInput(_) => "url_input".to_string(),
2047 TransportConfig::S3Input(_) => "s3_input".to_string(),
2048 TransportConfig::DeltaTableInput(_) => "delta_table_input".to_string(),
2049 TransportConfig::DeltaTableOutput(_) => "delta_table_output".to_string(),
2050 TransportConfig::DynamoDBOutput(_) => "dynamodb_output".to_string(),
2051 TransportConfig::IcebergInput(_) => "iceberg_input".to_string(),
2052 TransportConfig::PostgresInput(_) => "postgres_input".to_string(),
2053 TransportConfig::PostgresCdcInput(_) => "postgres_cdc_input".to_string(),
2054 TransportConfig::PostgresOutput(_) => "postgres_output".to_string(),
2055 TransportConfig::Datagen(_) => "datagen".to_string(),
2056 TransportConfig::Nexmark(_) => "nexmark".to_string(),
2057 TransportConfig::HttpInput(_) => "http_input".to_string(),
2058 TransportConfig::HttpOutput(_) => "http_output".to_string(),
2059 TransportConfig::AdHocInput(_) => "adhoc_input".to_string(),
2060 TransportConfig::RedisOutput(_) => "redis_output".to_string(),
2061 TransportConfig::ClockInput(_) => "clock".to_string(),
2062 TransportConfig::NullOutput => "null_output".to_string(),
2063 TransportConfig::EmptyInput => "empty_input".to_string(),
2064 }
2065 }
2066
2067 /// Returns true if the connector is transient, i.e., is created and destroyed
2068 /// at runtime on demand, rather than being configured as part of the pipeline.
2069 pub fn is_transient(&self) -> bool {
2070 matches!(
2071 self,
2072 TransportConfig::AdHocInput(_)
2073 | TransportConfig::HttpInput(_)
2074 | TransportConfig::HttpOutput(_)
2075 | TransportConfig::ClockInput(_)
2076 )
2077 }
2078
2079 pub fn is_http_input(&self) -> bool {
2080 matches!(self, TransportConfig::HttpInput(_))
2081 }
2082}
2083
2084/// Data format specification used to parse raw data received from the
2085/// endpoint or to encode data sent to the endpoint.
2086#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, ToSchema)]
2087pub struct FormatConfig {
2088 /// Format name, e.g., "csv", "json", "bincode", etc.
2089 pub name: Cow<'static, str>,
2090
2091 /// Format-specific parser or encoder configuration.
2092 #[serde(default)]
2093 #[schema(value_type = Object)]
2094 pub config: JsonValue,
2095}
2096
2097#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, ToSchema)]
2098#[serde(default)]
2099pub struct ResourceConfig {
2100 /// The minimum number of CPU cores to reserve
2101 /// for an instance of this pipeline
2102 #[serde(deserialize_with = "crate::serde_via_value::deserialize")]
2103 pub cpu_cores_min: Option<f64>,
2104
2105 /// The maximum number of CPU cores to reserve
2106 /// for an instance of this pipeline
2107 #[serde(deserialize_with = "crate::serde_via_value::deserialize")]
2108 pub cpu_cores_max: Option<f64>,
2109
2110 /// The minimum memory in Megabytes to reserve
2111 /// for an instance of this pipeline
2112 pub memory_mb_min: Option<u64>,
2113
2114 /// The maximum memory in Megabytes to reserve
2115 /// for an instance of this pipeline
2116 pub memory_mb_max: Option<u64>,
2117
2118 /// The total storage in Megabytes to reserve
2119 /// for an instance of this pipeline
2120 pub storage_mb_max: Option<u64>,
2121
2122 /// Storage class to use for an instance of this pipeline.
2123 /// The class determines storage performance such as IOPS and throughput.
2124 pub storage_class: Option<String>,
2125
2126 /// Kubernetes service account name to use for an instance of this pipeline.
2127 /// The account determines permissions and access controls.
2128 pub service_account_name: Option<String>,
2129
2130 /// Kubernetes namespace to use for an instance of this pipeline.
2131 /// The namespace determines the scope of names for resources created
2132 /// for the pipeline.
2133 /// If not set, the pipeline will be deployed in the same namespace
2134 /// as the control-plane.
2135 // The type of this field should not be backward incompatibly changed, and its location in the
2136 // runtime configuration JSON (`runtime_config.resources.namespace`) should not be changed.
2137 pub namespace: Option<String>,
2138}