Skip to main content

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;
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    /// Optionally, a list of CPU numbers for CPUs to which the pipeline may pin
946    /// its worker threads.  Specify at least twice as many CPU numbers as
947    /// workers.  CPUs are generally numbered starting from 0.  The pipeline
948    /// might not be able to honor CPU pinning requests.
949    ///
950    /// CPU pinning can make pipelines run faster and perform more consistently,
951    /// as long as different pipelines running on the same machine are pinned to
952    /// different CPUs.
953    pub pin_cpus: Vec<usize>,
954
955    /// Timeout in seconds for the `Provisioning` phase of the pipeline.
956    /// Setting this value will override the default of the runner.
957    pub provisioning_timeout_secs: Option<u64>,
958
959    /// The maximum number of connectors initialized in parallel during pipeline
960    /// startup.
961    ///
962    /// At startup, the pipeline must initialize all of its input and output connectors.
963    /// Depending on the number and types of connectors, this can take a long time.
964    /// To accelerate the process, multiple connectors are initialized concurrently.
965    /// This option controls the maximum number of connectors that can be initialized
966    /// in parallel.
967    ///
968    /// The default is 10.
969    pub max_parallel_connector_init: Option<u64>,
970
971    /// Specification of additional (sidecar) containers.
972    pub init_containers: Option<serde_json::Value>,
973
974    /// Deprecated: setting this true or false does not have an effect anymore.
975    pub checkpoint_during_suspend: bool,
976
977    /// Sets the number of available runtime threads for the http server.
978    ///
979    /// In most cases, this does not need to be set explicitly and
980    /// the default is sufficient. Can be increased in case the
981    /// pipeline HTTP API operations are a bottleneck.
982    ///
983    /// If not specified, the default is set to `workers`.
984    pub http_workers: Option<u64>,
985
986    /// Sets the number of available runtime threads for async IO tasks.
987    ///
988    /// This affects some networking and file I/O operations
989    /// especially adapters and ad-hoc queries.
990    ///
991    /// In most cases, this does not need to be set explicitly and
992    /// the default is sufficient. Can be increased in case
993    /// ingress, egress or ad-hoc queries are a bottleneck.
994    ///
995    /// If not specified, the default is set to `workers`.
996    pub io_workers: Option<u64>,
997
998    /// Environment variables for the pipeline process.
999    ///
1000    /// These are key-value pairs injected into the pipeline process environment.
1001    /// Some variable names are reserved by the platform and cannot be overridden
1002    /// (for example `RUST_LOG`, and variables in the `FELDERA_`,
1003    /// `KUBERNETES_`, and `TOKIO_` namespaces).
1004    #[serde(default)]
1005    pub env: BTreeMap<String, String>,
1006
1007    /// Optional settings for tweaking Feldera internals.
1008    pub dev_tweaks: DevTweaks,
1009
1010    /// Log filtering directives.
1011    ///
1012    /// If set to a valid [tracing-subscriber] filter, this controls the log
1013    /// messages emitted by the pipeline process.  Otherwise, or if the filter
1014    /// has invalid syntax, messages at "info" severity and higher are written
1015    /// to the log and all others are discarded.
1016    ///
1017    /// [tracing-subscriber]: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives
1018    pub logging: Option<String>,
1019
1020    /// ConfigMap containing a custom pipeline template (Enterprise only).
1021    ///
1022    /// This feature is only available in Feldera Enterprise. If set, the Kubernetes runner
1023    /// will read the template from the specified ConfigMap and use it instead of the default
1024    /// StatefulSet template for the configured pipeline.
1025    ///
1026    /// check [`PipelineTemplateConfig`] documentation for details.
1027    pub pipeline_template_configmap: Option<PipelineTemplateConfig>,
1028}
1029
1030/// Accepts "true" and "false" and converts them to the new format.
1031fn deserialize_storage_options<'de, D>(deserializer: D) -> Result<Option<StorageOptions>, D::Error>
1032where
1033    D: Deserializer<'de>,
1034{
1035    struct BoolOrStruct;
1036
1037    impl<'de> Visitor<'de> for BoolOrStruct {
1038        type Value = Option<StorageOptions>;
1039
1040        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1041            formatter.write_str("boolean or StorageOptions")
1042        }
1043
1044        fn visit_bool<E>(self, v: bool) -> Result<Self::Value, E>
1045        where
1046            E: de::Error,
1047        {
1048            match v {
1049                false => Ok(None),
1050                true => Ok(Some(StorageOptions::default())),
1051            }
1052        }
1053
1054        fn visit_unit<E>(self) -> Result<Self::Value, E>
1055        where
1056            E: de::Error,
1057        {
1058            Ok(None)
1059        }
1060
1061        fn visit_none<E>(self) -> Result<Self::Value, E>
1062        where
1063            E: de::Error,
1064        {
1065            Ok(None)
1066        }
1067
1068        fn visit_map<M>(self, map: M) -> Result<Option<StorageOptions>, M::Error>
1069        where
1070            M: MapAccess<'de>,
1071        {
1072            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)).map(Some)
1073        }
1074    }
1075
1076    deserializer.deserialize_any(BoolOrStruct)
1077}
1078
1079/// Accepts very old 'initial_state' and 'latest_checkpoint' as enabling fault
1080/// tolerance.
1081///
1082/// Accepts `null` as disabling fault tolerance.
1083///
1084/// Otherwise, deserializes [FtConfig] in the way that one might otherwise
1085/// expect.
1086fn deserialize_fault_tolerance<'de, D>(deserializer: D) -> Result<FtConfig, D::Error>
1087where
1088    D: Deserializer<'de>,
1089{
1090    struct StringOrStruct;
1091
1092    impl<'de> Visitor<'de> for StringOrStruct {
1093        type Value = FtConfig;
1094
1095        fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
1096            formatter.write_str("none or FtConfig or 'initial_state' or 'latest_checkpoint'")
1097        }
1098
1099        fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
1100        where
1101            E: de::Error,
1102        {
1103            match v {
1104                "initial_state" | "latest_checkpoint" => Ok(FtConfig {
1105                    model: Some(FtModel::default()),
1106                    ..FtConfig::default()
1107                }),
1108                _ => Err(de::Error::invalid_value(de::Unexpected::Str(v), &self)),
1109            }
1110        }
1111
1112        fn visit_unit<E>(self) -> Result<Self::Value, E>
1113        where
1114            E: de::Error,
1115        {
1116            Ok(FtConfig::default())
1117        }
1118
1119        fn visit_none<E>(self) -> Result<Self::Value, E>
1120        where
1121            E: de::Error,
1122        {
1123            Ok(FtConfig::default())
1124        }
1125
1126        fn visit_map<M>(self, map: M) -> Result<FtConfig, M::Error>
1127        where
1128            M: MapAccess<'de>,
1129        {
1130            Deserialize::deserialize(de::value::MapAccessDeserializer::new(map))
1131        }
1132    }
1133
1134    deserializer.deserialize_any(StringOrStruct)
1135}
1136
1137impl Default for RuntimeConfig {
1138    fn default() -> Self {
1139        Self {
1140            workers: 8,
1141            max_rss_mb: None,
1142            datafusion_memory_mb: None,
1143            hosts: 1,
1144            storage: Some(StorageOptions::default()),
1145            fault_tolerance: FtConfig::default(),
1146            cpu_profiler: true,
1147            tracing: {
1148                // We discovered that the jaeger crate can use up gigabytes of RAM, so it's not harmless
1149                // to keep it on by default.
1150                false
1151            },
1152            tracing_endpoint_jaeger: "127.0.0.1:6831".to_string(),
1153            min_batch_size_records: 0,
1154            max_buffering_delay_usecs: 0,
1155            resources: ResourceConfig::default(),
1156            clock_resolution_usecs: { Some(DEFAULT_CLOCK_RESOLUTION_USECS) },
1157            pin_cpus: Vec::new(),
1158            provisioning_timeout_secs: None,
1159            max_parallel_connector_init: None,
1160            init_containers: None,
1161            checkpoint_during_suspend: true,
1162            io_workers: None,
1163            http_workers: None,
1164            env: BTreeMap::default(),
1165            dev_tweaks: DevTweaks::default(),
1166            logging: None,
1167            pipeline_template_configmap: None,
1168        }
1169    }
1170}
1171
1172/// Upper bound on the default DataFusion pool size, in MB.
1173///
1174/// Spill-to-disk handles overflow; reserving more starves the circuit.
1175pub const DEFAULT_DATAFUSION_MEMORY_MB_CEILING: u64 = 2048;
1176
1177/// Default DataFusion pool size as a percentage of the pipeline's
1178/// effective memory budget. The remainder is left for the DBSP circuit.
1179pub const DEFAULT_DATAFUSION_MEMORY_PERCENT: u64 = 5;
1180
1181impl RuntimeConfig {
1182    /// Pipeline's effective memory budget in MB: `max_rss_mb`, falling back
1183    /// to `resources.memory_mb_max` (the k8s pod limit).
1184    pub fn effective_memory_mb(&self) -> Option<u64> {
1185        self.max_rss_mb.or(self.resources.memory_mb_max)
1186    }
1187
1188    /// Resolved DataFusion pool size in MB: explicit `datafusion_memory_mb`
1189    /// if set, else 5% of the effective budget capped at
1190    /// `DEFAULT_DATAFUSION_MEMORY_MB_CEILING`. `None` if no budget is
1191    /// configured.
1192    pub fn resolved_datafusion_memory_mb(&self) -> Option<u64> {
1193        if let Some(explicit) = self.datafusion_memory_mb {
1194            return Some(explicit);
1195        }
1196        let effective = self.effective_memory_mb()?;
1197        let fraction = effective * DEFAULT_DATAFUSION_MEMORY_PERCENT / 100;
1198        Some(fraction.min(DEFAULT_DATAFUSION_MEMORY_MB_CEILING))
1199    }
1200}
1201
1202/// Fault-tolerance configuration.
1203///
1204/// The default [FtConfig] (via [FtConfig::default]) disables fault tolerance,
1205/// which is the configuration that one gets if [RuntimeConfig] omits fault
1206/// tolerance configuration.
1207///
1208/// The default value for [FtConfig::model] enables fault tolerance, as
1209/// `Some(FtModel::default())`.  This is the configuration that one gets if
1210/// [RuntimeConfig] includes a fault tolerance configuration but does not
1211/// specify a particular model.
1212#[derive(Debug, Copy, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
1213#[serde(rename_all = "snake_case")]
1214pub struct FtConfig {
1215    /// Fault tolerance model to use.
1216    #[serde(with = "none_as_string")]
1217    #[serde(default = "default_model")]
1218    #[schema(
1219        schema_with = none_as_string_schema::<FtModel>,
1220    )]
1221    pub model: Option<FtModel>,
1222
1223    /// Interval between automatic checkpoints, in seconds.
1224    ///
1225    /// The default is 60 seconds.  Values less than 1 or greater than 3600 will
1226    /// be forced into that range.
1227    #[serde(default = "default_checkpoint_interval_secs")]
1228    pub checkpoint_interval_secs: Option<u64>,
1229}
1230
1231fn default_model() -> Option<FtModel> {
1232    Some(FtModel::default())
1233}
1234
1235pub fn default_checkpoint_interval_secs() -> Option<u64> {
1236    Some(60)
1237}
1238
1239impl Default for FtConfig {
1240    fn default() -> Self {
1241        Self {
1242            model: None,
1243            checkpoint_interval_secs: default_checkpoint_interval_secs(),
1244        }
1245    }
1246}
1247
1248#[cfg(test)]
1249mod test {
1250    use super::deserialize_fault_tolerance;
1251    use crate::config::{
1252        DEFAULT_DATAFUSION_MEMORY_MB_CEILING, FtConfig, FtModel, PipelineConfig, ResourceConfig,
1253        RuntimeConfig,
1254    };
1255    use serde::{Deserialize, Serialize};
1256
1257    fn config_with_name(name: Option<&str>) -> PipelineConfig {
1258        PipelineConfig {
1259            global: RuntimeConfig::default(),
1260            multihost: None,
1261            name: name.map(str::to_string),
1262            given_name: None,
1263            storage_config: None,
1264            secrets_dir: None,
1265            inputs: Default::default(),
1266            outputs: Default::default(),
1267            program_ir: None,
1268        }
1269    }
1270
1271    #[test]
1272    fn pipeline_identity_uses_system_and_given_names() {
1273        let mut config = config_with_name(Some("pipeline-018f6f57-4e15-7438-91af-3fd21ff2a8d3"));
1274        config.given_name = Some("my-pipeline".to_string());
1275
1276        let metadata = config.pipeline_identity().unwrap();
1277        assert_eq!(
1278            metadata.name,
1279            "pipeline-018f6f57-4e15-7438-91af-3fd21ff2a8d3"
1280        );
1281        assert_eq!(metadata.given_name.as_deref(), Some("my-pipeline"));
1282    }
1283
1284    #[test]
1285    fn pipeline_identity_is_none_without_system_name() {
1286        assert_eq!(config_with_name(None).pipeline_identity(), None);
1287    }
1288
1289    #[test]
1290    fn resolved_datafusion_memory_explicit_passes_through() {
1291        let config = RuntimeConfig {
1292            max_rss_mb: Some(8_000),
1293            datafusion_memory_mb: Some(1_500),
1294            ..Default::default()
1295        };
1296        assert_eq!(config.resolved_datafusion_memory_mb(), Some(1_500));
1297    }
1298
1299    #[test]
1300    fn resolved_datafusion_memory_unconfigured_returns_none() {
1301        let config = RuntimeConfig::default();
1302        assert!(config.max_rss_mb.is_none());
1303        assert!(config.resources.memory_mb_max.is_none());
1304        assert_eq!(config.resolved_datafusion_memory_mb(), None);
1305    }
1306
1307    #[test]
1308    fn resolved_datafusion_memory_small_budget_scales_down() {
1309        // Small pipelines must provision cleanly; the default just shrinks.
1310        let config = RuntimeConfig {
1311            max_rss_mb: Some(256),
1312            ..Default::default()
1313        };
1314        assert_eq!(config.resolved_datafusion_memory_mb(), Some(12));
1315
1316        let config = RuntimeConfig {
1317            max_rss_mb: Some(512),
1318            ..Default::default()
1319        };
1320        assert_eq!(config.resolved_datafusion_memory_mb(), Some(25));
1321    }
1322
1323    #[test]
1324    fn resolved_datafusion_memory_clamps_to_ceiling_for_large_budgets() {
1325        // 5% of 64 GB = 3.2 GB, above the 2 GB ceiling.
1326        let config = RuntimeConfig {
1327            max_rss_mb: Some(64_000),
1328            ..Default::default()
1329        };
1330        assert_eq!(
1331            config.resolved_datafusion_memory_mb(),
1332            Some(DEFAULT_DATAFUSION_MEMORY_MB_CEILING),
1333        );
1334    }
1335
1336    #[test]
1337    fn resolved_datafusion_memory_midrange_uses_five_percent() {
1338        // 5% of 16 GB = 800 MB, inside the clamp range.
1339        let config = RuntimeConfig {
1340            max_rss_mb: Some(16_000),
1341            ..Default::default()
1342        };
1343        assert_eq!(config.resolved_datafusion_memory_mb(), Some(800));
1344    }
1345
1346    #[test]
1347    fn resolved_datafusion_memory_falls_back_to_resources() {
1348        // No max_rss_mb, but resources.memory_mb_max is set.
1349        let config = RuntimeConfig {
1350            max_rss_mb: None,
1351            resources: ResourceConfig {
1352                memory_mb_max: Some(16_000),
1353                ..Default::default()
1354            },
1355            ..Default::default()
1356        };
1357        assert_eq!(config.resolved_datafusion_memory_mb(), Some(800));
1358    }
1359
1360    #[test]
1361    fn ft_config() {
1362        #[derive(Serialize, Deserialize, Default, PartialEq, Eq, Debug)]
1363        #[serde(default)]
1364        struct Wrapper {
1365            #[serde(deserialize_with = "deserialize_fault_tolerance")]
1366            config: FtConfig,
1367        }
1368
1369        // Omitting FtConfig, or specifying null, or specifying model "none", disables fault tolerance.
1370        for s in [
1371            "{}",
1372            r#"{"config": null}"#,
1373            r#"{"config": {"model": "none"}}"#,
1374        ] {
1375            let config: Wrapper = serde_json::from_str(s).unwrap();
1376            assert_eq!(
1377                config,
1378                Wrapper {
1379                    config: FtConfig {
1380                        model: None,
1381                        checkpoint_interval_secs: Some(60)
1382                    }
1383                }
1384            );
1385        }
1386
1387        // Serializing disabled FT produces explicit "none" form.
1388        let s = serde_json::to_string(&Wrapper {
1389            config: FtConfig::default(),
1390        })
1391        .unwrap();
1392        assert!(s.contains("\"none\""));
1393
1394        // `{}` for FtConfig, or `{...}` with `model` omitted, enables fault
1395        // tolerance.
1396        for s in [r#"{"config": {}}"#, r#"{"checkpoint_interval_secs": 60}"#] {
1397            assert_eq!(
1398                serde_json::from_str::<FtConfig>(s).unwrap(),
1399                FtConfig {
1400                    model: Some(FtModel::default()),
1401                    checkpoint_interval_secs: Some(60)
1402                }
1403            );
1404        }
1405
1406        // `"checkpoint_interval_secs": null` disables periodic checkpointing.
1407        assert_eq!(
1408            serde_json::from_str::<FtConfig>(r#"{"checkpoint_interval_secs": null}"#).unwrap(),
1409            FtConfig {
1410                model: Some(FtModel::default()),
1411                checkpoint_interval_secs: None
1412            }
1413        );
1414    }
1415}
1416
1417impl FtConfig {
1418    pub fn is_enabled(&self) -> bool {
1419        self.model.is_some()
1420    }
1421
1422    /// Returns the checkpoint interval, if fault tolerance is enabled, and
1423    /// otherwise `None`.
1424    pub fn checkpoint_interval(&self) -> Option<Duration> {
1425        if self.is_enabled() {
1426            self.checkpoint_interval_secs
1427                .map(|interval| Duration::from_secs(interval.clamp(1, 3600)))
1428        } else {
1429            None
1430        }
1431    }
1432}
1433
1434/// Serde implementation for de/serializing a string into `Option<T>` where
1435/// `"none"` indicates `None` and any other string indicates `Some`.
1436///
1437/// This could be extended to handle non-strings by adding more forwarding
1438/// `visit_*` methods to the Visitor implementation.  I don't see a way to write
1439/// them automatically.
1440mod none_as_string {
1441    use std::marker::PhantomData;
1442
1443    use serde::de::{Deserialize, Deserializer, IntoDeserializer, Visitor};
1444    use serde::ser::{Serialize, Serializer};
1445
1446    pub(super) fn serialize<S, T>(value: &Option<T>, serializer: S) -> Result<S::Ok, S::Error>
1447    where
1448        S: Serializer,
1449        T: Serialize,
1450    {
1451        match value.as_ref() {
1452            Some(value) => value.serialize(serializer),
1453            None => "none".serialize(serializer),
1454        }
1455    }
1456
1457    struct NoneAsString<T>(PhantomData<fn() -> T>);
1458
1459    impl<'de, T> Visitor<'de> for NoneAsString<T>
1460    where
1461        T: Deserialize<'de>,
1462    {
1463        type Value = Option<T>;
1464
1465        fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
1466            formatter.write_str("string")
1467        }
1468
1469        fn visit_none<E>(self) -> Result<Self::Value, E>
1470        where
1471            E: serde::de::Error,
1472        {
1473            Ok(None)
1474        }
1475
1476        fn visit_str<E>(self, value: &str) -> Result<Option<T>, E>
1477        where
1478            E: serde::de::Error,
1479        {
1480            if &value.to_ascii_lowercase() == "none" {
1481                Ok(None)
1482            } else {
1483                Ok(Some(T::deserialize(value.into_deserializer())?))
1484            }
1485        }
1486    }
1487
1488    pub(super) fn deserialize<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
1489    where
1490        D: Deserializer<'de>,
1491        T: Deserialize<'de>,
1492    {
1493        deserializer.deserialize_str(NoneAsString(PhantomData))
1494    }
1495}
1496
1497/// Generates an OpenAPI schema for an `Option<T>` field serialized with `none_as_string`.
1498/// The schema is a `oneOf` with a reference to `T`'s schema and a `"none"` string enum.
1499fn none_as_string_schema<'a, T: ToSchema<'a> + Default + Serialize>() -> Schema {
1500    Schema::OneOf(
1501        OneOfBuilder::new()
1502            .item(RefOr::Ref(Ref::new(format!(
1503                "#/components/schemas/{}",
1504                T::schema().0
1505            ))))
1506            .item(
1507                ObjectBuilder::new()
1508                    .schema_type(SchemaType::String)
1509                    .enum_values(Some(vec!["none"])),
1510            )
1511            .default(Some(
1512                serde_json::to_value(T::default()).expect("Failed to serialize default value"),
1513            ))
1514            .build(),
1515    )
1516}
1517
1518/// Fault tolerance model.
1519///
1520/// The ordering is significant: we consider [Self::ExactlyOnce] to be a "higher
1521/// level" of fault tolerance than [Self::AtLeastOnce].
1522#[derive(
1523    Debug, Copy, Clone, Default, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize, ToSchema,
1524)]
1525#[serde(rename_all = "snake_case")]
1526pub enum FtModel {
1527    /// Each record is output at least once.  Crashes may duplicate output, but
1528    /// no input or output is dropped.
1529    AtLeastOnce,
1530
1531    /// Each record is output exactly once.  Crashes do not drop or duplicate
1532    /// input or output.
1533    #[default]
1534    ExactlyOnce,
1535}
1536
1537impl FtModel {
1538    pub fn option_as_str(value: Option<FtModel>) -> &'static str {
1539        value.map_or("no", |model| model.as_str())
1540    }
1541
1542    pub fn as_str(&self) -> &'static str {
1543        match self {
1544            FtModel::AtLeastOnce => "at_least_once",
1545            FtModel::ExactlyOnce => "exactly_once",
1546        }
1547    }
1548}
1549
1550pub struct FtModelUnknown;
1551
1552impl FromStr for FtModel {
1553    type Err = FtModelUnknown;
1554
1555    fn from_str(s: &str) -> Result<Self, Self::Err> {
1556        match s.to_ascii_lowercase().as_str() {
1557            "exactly_once" => Ok(Self::ExactlyOnce),
1558            "at_least_once" => Ok(Self::AtLeastOnce),
1559            _ => Err(FtModelUnknown),
1560        }
1561    }
1562}
1563
1564/// Describes an input connector configuration
1565#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
1566pub struct InputEndpointConfig {
1567    /// The name of the input stream of the circuit that this endpoint is
1568    /// connected to.
1569    pub stream: Cow<'static, str>,
1570
1571    /// Connector configuration.
1572    #[serde(flatten)]
1573    pub connector_config: ConnectorConfig,
1574}
1575
1576impl InputEndpointConfig {
1577    pub fn new(stream: impl Into<Cow<'static, str>>, connector_config: ConnectorConfig) -> Self {
1578        Self {
1579            stream: stream.into(),
1580            connector_config,
1581        }
1582    }
1583}
1584
1585/// Deserialize the `start_after` property of a connector configuration.
1586/// It requires a non-standard deserialization because we want to accept
1587/// either a string or an array of strings.
1588fn deserialize_start_after<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
1589where
1590    D: Deserializer<'de>,
1591{
1592    let value = Option::<JsonValue>::deserialize(deserializer)?;
1593    match value {
1594        Some(JsonValue::String(s)) => Ok(Some(vec![s])),
1595        Some(JsonValue::Array(arr)) => {
1596            let vec = arr
1597                .into_iter()
1598                .map(|item| {
1599                    item.as_str()
1600                        .map(|s| s.to_string())
1601                        .ok_or_else(|| serde::de::Error::custom("invalid 'start_after' property: expected a string, an array of strings, or null"))
1602                })
1603                .collect::<Result<Vec<String>, _>>()?;
1604            Ok(Some(vec))
1605        }
1606        Some(JsonValue::Null) | None => Ok(None),
1607        _ => Err(serde::de::Error::custom(
1608            "invalid 'start_after' property: expected a string, an array of strings, or null",
1609        )),
1610    }
1611}
1612
1613#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
1614pub struct ConnectorConfig {
1615    /// Send a full snapshot of a materialized view when the connector first
1616    /// starts. Valid for output connectors only.
1617    ///
1618    /// When `true`, the pipeline emits the current contents of the view as the
1619    /// initial batch the first time the connector runs. The view must be
1620    /// materialized (declared with `CREATE MATERIALIZED VIEW`).
1621    ///
1622    /// The snapshot is sent exactly once per connector lifetime: it does not
1623    /// fire again when the pipeline resumes from a checkpoint. Modifying the
1624    /// connector configuration or invoking the reset API triggers a fresh
1625    /// snapshot when the connector supports reset (e.g., Delta Lake in
1626    /// `truncate` mode and Postgres).
1627    #[serde(default)]
1628    pub send_snapshot: bool,
1629
1630    /// Transport endpoint configuration.
1631    pub transport: TransportConfig,
1632
1633    /// Optional preprocessor configuration
1634    #[serde(skip_serializing_if = "Option::is_none")]
1635    pub preprocessor: Option<Vec<PreprocessorConfig>>,
1636
1637    /// Parser configuration.
1638    pub format: Option<FormatConfig>,
1639
1640    /// Optional postprocessor configuration
1641    #[serde(skip_serializing_if = "Option::is_none")]
1642    pub postprocessor: Option<Vec<PostprocessorConfig>>,
1643
1644    /// Name of the index that the connector is attached to.
1645    ///
1646    /// This property is valid for output connectors only.  It is used with data
1647    /// transports and formats that expect output updates in the form of key/value
1648    /// pairs, where the key typically represents a unique id associated with the
1649    /// table or view.
1650    ///
1651    /// To support such output formats, an output connector can be attached to an
1652    /// index created using the SQL CREATE INDEX statement.  An index of a table
1653    /// or view contains the same updates as the table or view itself, indexed by
1654    /// one or more key columns.
1655    ///
1656    /// See individual connector documentation for details on how they work
1657    /// with indexes.
1658    pub index: Option<String>,
1659
1660    /// Output buffer configuration.
1661    #[serde(flatten)]
1662    pub output_buffer_config: OutputBufferConfig,
1663
1664    /// Maximum number of records from this connector to process in a single batch.
1665    ///
1666    /// When set, this caps how many records are taken from the connector’s input
1667    /// buffer and pushed through the circuit at once.
1668    ///
1669    /// This is typically configured lower than `max_queued_records` to allow the
1670    /// connector time to restart and refill its buffer while a batch is being
1671    /// processed.
1672    ///
1673    /// Not all input adapters honor this limit.
1674    ///
1675    /// If this is not set, the batch size is derived from `max_worker_batch_size`.
1676    #[serde(skip_serializing_if = "Option::is_none")]
1677    pub max_batch_size: Option<u64>,
1678
1679    /// Maximum number of records processed per batch, per worker thread.
1680    ///
1681    /// When `max_batch_size` is not set, this setting is used to cap
1682    /// the number of records that can be taken from the connector’s input
1683    /// buffer and pushed through the circuit at once.  The effective batch size is computed as:
1684    /// `max_worker_batch_size × workers`.
1685    ///
1686    /// This provides an alternative to `max_batch_size` that automatically adjusts batch
1687    /// size as the number of worker threads changes to maintain constant amount of
1688    /// work per worker per batch.
1689    ///
1690    /// Defaults to 10,000 records per worker.
1691    #[serde(skip_serializing_if = "Option::is_none")]
1692    pub max_worker_batch_size: Option<u64>,
1693
1694    /// Backpressure threshold, in records.
1695    ///
1696    /// Maximal number of records queued by the endpoint before the endpoint
1697    /// is paused by the backpressure mechanism.
1698    ///
1699    /// For input endpoints, this setting bounds the number of records that have
1700    /// been received from the input transport but haven't yet been consumed by
1701    /// the circuit, since the circuit is still busy processing previous inputs.
1702    ///
1703    /// For output endpoints, this setting bounds the number of records that have
1704    /// been produced by the circuit but not yet sent via the output transport endpoint
1705    /// nor stored in the output buffer (see `enable_output_buffer`).
1706    ///
1707    /// Note that this is not a hard bound: there can be a small delay between
1708    /// the backpressure mechanism is triggered and the endpoint is paused, during
1709    /// which more data may be queued.
1710    ///
1711    /// The default is 1 million.
1712    #[serde(default = "default_max_queued_records")]
1713    pub max_queued_records: u64,
1714
1715    /// Backpressure threshold, in bytes.
1716    ///
1717    /// Maximal number of bytes queued by the endpoint before the endpoint
1718    /// is paused by the backpressure mechanism.
1719    ///
1720    /// For input endpoints, this setting bounds the number of bytes that have
1721    /// been received from the input transport but haven't yet been consumed by
1722    /// the circuit since the circuit, since the circuit is still busy processing
1723    /// previous inputs.
1724    ///
1725    /// This setting is not yet implemented for output endpoints.
1726    ///
1727    /// Note that this is not a hard bound: there can be a small delay between
1728    /// the backpressure mechanism is triggered and the endpoint is paused, during
1729    /// which more data may be queued.
1730    ///
1731    /// When this is unspecified, it defaults to `1000 * max_queued_records`.
1732    #[serde(skip_serializing_if = "Option::is_none")]
1733    pub max_queued_bytes: Option<u64>,
1734
1735    /// Create connector in paused state.
1736    ///
1737    /// The default is `false`.
1738    #[serde(default)]
1739    pub paused: bool,
1740
1741    /// Arbitrary user-defined text labels associated with the connector.
1742    ///
1743    /// These labels can be used in conjunction with the `start_after` property
1744    /// to control the start order of connectors.
1745    #[serde(default)]
1746    pub labels: Vec<String>,
1747
1748    /// Start the connector after all connectors with specified labels.
1749    ///
1750    /// This property is used to control the start order of connectors.
1751    /// The connector will not start until all connectors with the specified
1752    /// labels have finished processing all inputs.
1753    #[serde(deserialize_with = "deserialize_start_after")]
1754    #[serde(default)]
1755    pub start_after: Option<Vec<String>>,
1756}
1757
1758impl ConnectorConfig {
1759    pub fn new(transport: TransportConfig, format: Option<FormatConfig>) -> Self {
1760        Self {
1761            send_snapshot: false,
1762            transport,
1763            preprocessor: None,
1764            format,
1765            postprocessor: None,
1766            index: None,
1767            output_buffer_config: Default::default(),
1768            max_batch_size: None,
1769            max_worker_batch_size: None,
1770            max_queued_records: default_max_queued_records(),
1771            max_queued_bytes: None,
1772            paused: false,
1773            labels: Vec::new(),
1774            start_after: None,
1775        }
1776    }
1777
1778    pub fn with_max_batch_size(mut self, max_batch_size: Option<u64>) -> Self {
1779        self.max_batch_size = max_batch_size;
1780        self
1781    }
1782
1783    pub fn with_max_queued_records(mut self, max_queued_records: u64) -> Self {
1784        self.max_queued_records = max_queued_records;
1785        self
1786    }
1787
1788    /// Compare two configs modulo the `paused` field.
1789    ///
1790    /// Used to compare checkpointed and current connector configs.
1791    pub fn equal_modulo_paused(&self, other: &Self) -> bool {
1792        let mut a = self.clone();
1793        let mut b = other.clone();
1794        a.paused = false;
1795        b.paused = false;
1796        a == b
1797    }
1798
1799    /// Compare two input connector configs modulo fields that only affect
1800    /// runtime flow control and do not invalidate checkpointed connector state.
1801    pub fn equal_for_input_checkpoint_replay(&self, other: &Self) -> bool {
1802        let mut a = self.clone();
1803        let mut b = other.clone();
1804        a.normalize_for_input_checkpoint_replay();
1805        b.normalize_for_input_checkpoint_replay();
1806        a == b
1807    }
1808
1809    fn normalize_for_input_checkpoint_replay(&mut self) {
1810        self.paused = false;
1811        self.max_batch_size = None;
1812        self.max_worker_batch_size = None;
1813        self.max_queued_records = default_max_queued_records();
1814        self.max_queued_bytes = None;
1815    }
1816
1817    /// Adopt input connector settings that are safe to change while replaying
1818    /// checkpointed connector state.
1819    pub fn apply_input_checkpoint_replay_config_from(&mut self, other: &Self) {
1820        self.max_batch_size = other.max_batch_size;
1821        self.max_worker_batch_size = other.max_worker_batch_size;
1822        self.max_queued_records = other.max_queued_records;
1823        self.max_queued_bytes = other.max_queued_bytes;
1824    }
1825
1826    /// Returns `max_queued_records` or, if it is not set, the default.
1827    pub fn max_queued_records(&self) -> u64 {
1828        self.max_queued_records
1829    }
1830
1831    /// Returns `max_queued_bytes` or, if it is not set, the default based on
1832    /// `max_queued_records`.
1833    pub fn max_queued_bytes(&self) -> u64 {
1834        self.max_queued_bytes
1835            .unwrap_or_else(|| self.max_queued_records().saturating_mul(1000))
1836    }
1837}
1838
1839#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
1840#[serde(default)]
1841pub struct OutputBufferConfig {
1842    /// Enable output buffering.
1843    ///
1844    /// The output buffering mechanism allows decoupling the rate at which the pipeline
1845    /// pushes changes to the output transport from the rate of input changes.
1846    ///
1847    /// By default, output updates produced by the pipeline are pushed directly to
1848    /// the output transport. Some destinations may prefer to receive updates in fewer
1849    /// bigger batches. For instance, when writing Parquet files, producing
1850    /// one bigger file every few minutes is usually better than creating
1851    /// small files every few milliseconds.
1852    ///
1853    /// To achieve such input/output decoupling, users can enable output buffering by
1854    /// setting the `enable_output_buffer` flag to `true`.  When buffering is enabled, output
1855    /// updates produced by the pipeline are consolidated in an internal buffer and are
1856    /// pushed to the output transport when one of several conditions is satisfied:
1857    ///
1858    /// * data has been accumulated in the buffer for more than `max_output_buffer_time_millis`
1859    ///   milliseconds.
1860    /// * buffer size exceeds `max_output_buffer_size_records` records.
1861    ///
1862    /// This flag is `false` by default.
1863    // TODO: on-demand output triggered via the API.
1864    pub enable_output_buffer: bool,
1865
1866    /// Maximum time in milliseconds data is kept in the output buffer.
1867    ///
1868    /// By default, data is kept in the buffer indefinitely until one of
1869    /// the other output conditions is satisfied.  When this option is
1870    /// set the buffer will be flushed at most every
1871    /// `max_output_buffer_time_millis` milliseconds.
1872    ///
1873    /// NOTE: this configuration option requires the `enable_output_buffer` flag
1874    /// to be set.
1875    pub max_output_buffer_time_millis: usize,
1876
1877    /// Maximum number of updates to be kept in the output buffer.
1878    ///
1879    /// This parameter bounds the maximal size of the buffer.
1880    /// Note that the size of the buffer is not always equal to the
1881    /// total number of updates output by the pipeline. Updates to the
1882    /// same record can overwrite or cancel previous updates.
1883    ///
1884    /// The default is 10,000,000.
1885    ///
1886    /// NOTE: this configuration option requires the `enable_output_buffer` flag
1887    /// to be set.
1888    pub max_output_buffer_size_records: usize,
1889}
1890
1891impl Default for OutputBufferConfig {
1892    fn default() -> Self {
1893        Self {
1894            enable_output_buffer: false,
1895            max_output_buffer_size_records: DEFAULT_MAX_OUTPUT_BUFFER_SIZE_RECORDS,
1896            max_output_buffer_time_millis: usize::MAX,
1897        }
1898    }
1899}
1900
1901/// Describes an output connector configuration
1902#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
1903pub struct OutputEndpointConfig {
1904    /// The name of the output stream of the circuit that this endpoint is
1905    /// connected to.
1906    pub stream: Cow<'static, str>,
1907
1908    /// Connector configuration.
1909    #[serde(flatten)]
1910    pub connector_config: ConnectorConfig,
1911}
1912
1913impl OutputEndpointConfig {
1914    pub fn new(stream: impl Into<Cow<'static, str>>, connector_config: ConnectorConfig) -> Self {
1915        Self {
1916            stream: stream.into(),
1917            connector_config,
1918        }
1919    }
1920}
1921
1922/// Transport-specific endpoint configuration passed to
1923/// `crate::OutputTransport::new_endpoint`
1924/// and `crate::InputTransport::new_endpoint`.
1925#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize, ToSchema)]
1926#[serde(tag = "name", content = "config", rename_all = "snake_case")]
1927pub enum TransportConfig {
1928    FileInput(FileInputConfig),
1929    FileOutput(FileOutputConfig),
1930    NatsInput(NatsInputConfig),
1931    KafkaInput(KafkaInputConfig),
1932    KafkaOutput(KafkaOutputConfig),
1933    PubSubInput(PubSubInputConfig),
1934    UrlInput(UrlInputConfig),
1935    S3Input(S3InputConfig),
1936    DeltaTableInput(DeltaTableReaderConfig),
1937    DeltaTableOutput(DeltaTableWriterConfig),
1938    // Snake case would rename "DynamoDBOutput" to `dynamo_db_output`.
1939    // However, DynamoDB is a single word, so override the tag to `dynamodb_output`.
1940    #[serde(rename = "dynamodb_output")]
1941    DynamoDBOutput(DynamoDBWriterConfig),
1942    RedisOutput(RedisOutputConfig),
1943    // Prevent rust from complaining about large size difference between enum variants.
1944    IcebergInput(Box<IcebergReaderConfig>),
1945    PostgresInput(PostgresReaderConfig),
1946    PostgresCdcInput(PostgresCdcReaderConfig),
1947    PostgresOutput(PostgresWriterConfig),
1948    Datagen(DatagenInputConfig),
1949    Nexmark(NexmarkInputConfig),
1950    /// Direct HTTP input: cannot be instantiated through API
1951    HttpInput(HttpInputConfig),
1952    /// Direct HTTP output: cannot be instantiated through API
1953    HttpOutput(HttpOutputConfig),
1954    /// Ad hoc input: cannot be instantiated through API
1955    AdHocInput(AdHocInputConfig),
1956    ClockInput(ClockConfig),
1957    /// Output connector that discards all data.
1958    NullOutput,
1959    /// Input connector that produces no data.
1960    EmptyInput,
1961}
1962
1963impl TransportConfig {
1964    pub fn name(&self) -> String {
1965        match self {
1966            TransportConfig::FileInput(_) => "file_input".to_string(),
1967            TransportConfig::FileOutput(_) => "file_output".to_string(),
1968            TransportConfig::NatsInput(_) => "nats_input".to_string(),
1969            TransportConfig::KafkaInput(_) => "kafka_input".to_string(),
1970            TransportConfig::KafkaOutput(_) => "kafka_output".to_string(),
1971            TransportConfig::PubSubInput(_) => "pub_sub_input".to_string(),
1972            TransportConfig::UrlInput(_) => "url_input".to_string(),
1973            TransportConfig::S3Input(_) => "s3_input".to_string(),
1974            TransportConfig::DeltaTableInput(_) => "delta_table_input".to_string(),
1975            TransportConfig::DeltaTableOutput(_) => "delta_table_output".to_string(),
1976            TransportConfig::DynamoDBOutput(_) => "dynamodb_output".to_string(),
1977            TransportConfig::IcebergInput(_) => "iceberg_input".to_string(),
1978            TransportConfig::PostgresInput(_) => "postgres_input".to_string(),
1979            TransportConfig::PostgresCdcInput(_) => "postgres_cdc_input".to_string(),
1980            TransportConfig::PostgresOutput(_) => "postgres_output".to_string(),
1981            TransportConfig::Datagen(_) => "datagen".to_string(),
1982            TransportConfig::Nexmark(_) => "nexmark".to_string(),
1983            TransportConfig::HttpInput(_) => "http_input".to_string(),
1984            TransportConfig::HttpOutput(_) => "http_output".to_string(),
1985            TransportConfig::AdHocInput(_) => "adhoc_input".to_string(),
1986            TransportConfig::RedisOutput(_) => "redis_output".to_string(),
1987            TransportConfig::ClockInput(_) => "clock".to_string(),
1988            TransportConfig::NullOutput => "null_output".to_string(),
1989            TransportConfig::EmptyInput => "empty_input".to_string(),
1990        }
1991    }
1992
1993    /// Returns true if the connector is transient, i.e., is created and destroyed
1994    /// at runtime on demand, rather than being configured as part of the pipeline.
1995    pub fn is_transient(&self) -> bool {
1996        matches!(
1997            self,
1998            TransportConfig::AdHocInput(_)
1999                | TransportConfig::HttpInput(_)
2000                | TransportConfig::HttpOutput(_)
2001                | TransportConfig::ClockInput(_)
2002        )
2003    }
2004
2005    pub fn is_http_input(&self) -> bool {
2006        matches!(self, TransportConfig::HttpInput(_))
2007    }
2008}
2009
2010/// Data format specification used to parse raw data received from the
2011/// endpoint or to encode data sent to the endpoint.
2012#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, ToSchema)]
2013pub struct FormatConfig {
2014    /// Format name, e.g., "csv", "json", "bincode", etc.
2015    pub name: Cow<'static, str>,
2016
2017    /// Format-specific parser or encoder configuration.
2018    #[serde(default)]
2019    #[schema(value_type = Object)]
2020    pub config: JsonValue,
2021}
2022
2023#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize, ToSchema)]
2024#[serde(default)]
2025pub struct ResourceConfig {
2026    /// The minimum number of CPU cores to reserve
2027    /// for an instance of this pipeline
2028    #[serde(deserialize_with = "crate::serde_via_value::deserialize")]
2029    pub cpu_cores_min: Option<f64>,
2030
2031    /// The maximum number of CPU cores to reserve
2032    /// for an instance of this pipeline
2033    #[serde(deserialize_with = "crate::serde_via_value::deserialize")]
2034    pub cpu_cores_max: Option<f64>,
2035
2036    /// The minimum memory in Megabytes to reserve
2037    /// for an instance of this pipeline
2038    pub memory_mb_min: Option<u64>,
2039
2040    /// The maximum memory in Megabytes to reserve
2041    /// for an instance of this pipeline
2042    pub memory_mb_max: Option<u64>,
2043
2044    /// The total storage in Megabytes to reserve
2045    /// for an instance of this pipeline
2046    pub storage_mb_max: Option<u64>,
2047
2048    /// Storage class to use for an instance of this pipeline.
2049    /// The class determines storage performance such as IOPS and throughput.
2050    pub storage_class: Option<String>,
2051
2052    /// Kubernetes service account name to use for an instance of this pipeline.
2053    /// The account determines permissions and access controls.
2054    pub service_account_name: Option<String>,
2055
2056    /// Kubernetes namespace to use for an instance of this pipeline.
2057    /// The namespace determines the scope of names for resources created
2058    /// for the pipeline.
2059    /// If not set, the pipeline will be deployed in the same namespace
2060    /// as the control-plane.
2061    // The type of this field should not be backward incompatibly changed, and its location in the
2062    // runtime configuration JSON (`runtime_config.resources.namespace`) should not be changed.
2063    pub namespace: Option<String>,
2064}