1use std::collections::{BTreeMap, BTreeSet};
11use std::path::PathBuf;
12
13use mz_build_info::BuildInfo;
14use mz_cluster_client::ReplicaId;
15use mz_controller_types::ClusterId;
16use mz_ore::metric;
17use mz_ore::metrics::{MetricsRegistry, UIntGauge};
18use mz_ore::now::NowFn;
19use mz_sql::catalog::EnvironmentId;
20use prometheus::IntCounter;
21
22mod backend;
23mod frontend;
24mod params;
25mod sync;
26
27pub use backend::SystemParameterBackend;
28pub use frontend::{
29 ClusterEvalContext, ClusterScopeContext, ReplicaEvalContext, ReplicaScopeContext,
30 SystemParameterFrontend,
31};
32pub use params::{ModifiedParameter, SynchronizedParameters};
33pub use sync::system_parameter_sync;
34
35#[derive(Clone, Debug, Default, PartialEq, Eq)]
43pub struct ScopedParameters {
44 pub cluster: BTreeMap<ClusterId, BTreeMap<String, String>>,
46 pub replica: BTreeMap<ReplicaId, BTreeMap<String, String>>,
48}
49
50#[derive(Clone, Debug, Default, PartialEq, Eq)]
59pub struct ScopedParametersScope {
60 pub clusters: BTreeSet<ClusterId>,
62 pub replicas: BTreeSet<ReplicaId>,
64}
65
66impl ScopedParameters {
67 pub fn is_empty(&self) -> bool {
69 self.cluster.is_empty() && self.replica.is_empty()
70 }
71
72 pub fn merge(&self, other: &ScopedParameters) -> ScopedParameters {
75 let mut merged = self.clone();
76 merged
77 .cluster
78 .extend(other.cluster.iter().map(|(id, v)| (*id, v.clone())));
79 merged
80 .replica
81 .extend(other.replica.iter().map(|(id, v)| (*id, v.clone())));
82 merged
83 }
84}
85
86#[derive(Clone, Debug)]
88pub struct SystemParameterSyncConfig {
89 env_id: EnvironmentId,
91 build_info: &'static BuildInfo,
93 metrics: Metrics,
95 key_map: BTreeMap<String, String>,
99 backend_config: SystemParameterSyncClientConfig,
101}
102
103#[derive(Clone, Debug)]
104pub enum SystemParameterSyncClientConfig {
105 File {
106 path: PathBuf,
108 },
109 LaunchDarkly {
110 sdk_key: String,
112 base_uri: Option<String>,
117 now_fn: NowFn,
119 },
120}
121
122impl SystemParameterSyncClientConfig {
123 fn is_launch_darkly(&self) -> bool {
124 match &self {
125 Self::LaunchDarkly { .. } => true,
126 Self::File { .. } => false,
127 }
128 }
129}
130
131impl SystemParameterSyncConfig {
132 pub fn new(
134 env_id: EnvironmentId,
135 build_info: &'static BuildInfo,
136 registry: &MetricsRegistry,
137 key_map: BTreeMap<String, String>,
138 backend_config: SystemParameterSyncClientConfig,
139 ) -> Self {
140 Self {
141 env_id,
142 build_info,
143 metrics: Metrics::register_into(registry),
144 key_map,
145 backend_config,
146 }
147 }
148}
149
150#[derive(Debug, Clone)]
151pub(super) struct Metrics {
152 pub last_cse_time_seconds: UIntGauge,
153 pub last_sse_time_seconds: UIntGauge,
154 pub params_changed: IntCounter,
155}
156
157impl Metrics {
158 pub(super) fn register_into(registry: &MetricsRegistry) -> Self {
159 Self {
160 last_cse_time_seconds: registry.register(metric!(
161 name: "mz_parameter_frontend_last_cse_time_seconds",
162 help: "The last known time when the LaunchDarkly client sent an event to the LaunchDarkly server (as unix timestamp).",
163 )),
164 last_sse_time_seconds: registry.register(metric!(
165 name: "mz_parameter_frontend_last_sse_time_seconds",
166 help: "The last known time when the LaunchDarkly client received an event from the LaunchDarkly server (as unix timestamp).",
167 )),
168 params_changed: registry.register(metric!(
169 name: "mz_parameter_frontend_params_changed",
170 help: "The number of parameter changes pulled from the LaunchDarkly frontend.",
171 )),
172 }
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use std::collections::BTreeMap;
179
180 use mz_cluster_client::ReplicaId;
181 use mz_controller_types::ClusterId;
182
183 use super::ScopedParameters;
184
185 fn cfg(name: &str, value: &str) -> BTreeMap<String, String> {
186 BTreeMap::from([(name.to_string(), value.to_string())])
187 }
188
189 #[mz_ore::test]
190 fn test_scoped_parameters_is_empty() {
191 assert!(ScopedParameters::default().is_empty());
192
193 let mut params = ScopedParameters::default();
194 params.cluster.insert(ClusterId::User(1), cfg("f", "true"));
195 assert!(!params.is_empty());
196
197 let mut params = ScopedParameters::default();
198 params.replica.insert(ReplicaId::User(1), cfg("f", "true"));
199 assert!(!params.is_empty());
200 }
201
202 #[mz_ore::test]
203 fn test_scoped_parameters_merge() {
204 let mut base = ScopedParameters::default();
205 base.cluster.insert(ClusterId::User(1), cfg("f", "old"));
206 base.cluster.insert(ClusterId::User(2), cfg("f", "keep"));
207 base.replica.insert(ReplicaId::User(1), cfg("g", "old"));
208
209 let mut incoming = ScopedParameters::default();
210 incoming.cluster.insert(ClusterId::User(1), cfg("f", "new"));
212 incoming.replica.insert(ReplicaId::User(2), cfg("g", "new"));
214
215 let merged = base.merge(&incoming);
216
217 assert_eq!(merged.cluster[&ClusterId::User(1)], cfg("f", "new"));
219 assert_eq!(merged.cluster[&ClusterId::User(2)], cfg("f", "keep"));
221 assert_eq!(merged.replica[&ReplicaId::User(1)], cfg("g", "old"));
223 assert_eq!(merged.replica[&ReplicaId::User(2)], cfg("g", "new"));
224
225 assert_eq!(base.cluster[&ClusterId::User(1)], cfg("f", "old"));
227 }
228}