mz_dyncfg_launchdarkly/
lib.rs1use std::time::Duration;
13
14use launchdarkly_server_sdk as ld;
15use mz_build_info::BuildInfo;
16use mz_dyncfg::{ConfigSet, ConfigUpdates, ConfigVal};
17use mz_ore::cast::CastLossy;
18use mz_ore::task;
19use tokio::time;
20
21pub async fn sync_launchdarkly_to_configset<F>(
33 set: ConfigSet,
34 build_info: &'static BuildInfo,
35 ctx_builder: F,
36 launchdarkly_sdk_key: Option<&str>,
39 config_sync_timeout: Duration,
40 config_sync_loop_interval: Option<Duration>,
41 on_update: impl Fn(&ConfigUpdates, &ConfigSet) + Send + 'static,
42) -> Result<(), anyhow::Error>
43where
44 F: FnOnce(&mut ld::MultiContextBuilder) -> Result<(), anyhow::Error>,
45{
46 for entry in set.entries() {
49 let _ = dyn_into_flag(entry.val())?;
50 }
51 let ld_client = if let Some(key) = launchdarkly_sdk_key {
52 let transport = launchdarkly_sdk_transport::HyperTransport::builder()
63 .connect_timeout(Duration::from_secs(10))
64 .read_timeout(Duration::from_secs(300))
65 .build_https()
66 .expect("failed to create HTTPS transport");
67
68 let mut data_source = ld::StreamingDataSourceBuilder::new();
69 data_source.transport(transport.clone());
70
71 let mut event_processor = ld::EventProcessorBuilder::new();
72 event_processor.transport(transport);
73
74 let config = ld::ConfigBuilder::new(key)
75 .data_source(&data_source)
76 .event_processor(&event_processor)
77 .build()
78 .expect("valid config");
79 let client = ld::Client::build(config)?;
80 client.start_with_default_executor();
81 let init = async {
82 let max_backoff = Duration::from_secs(60);
83 let mut backoff = Duration::from_secs(5);
84
85 loop {
87 match client.wait_for_initialization(config_sync_timeout).await {
88 Some(true) => break,
89 Some(false) => tracing::warn!("SyncedConfigSet failed to initialize"),
90 None => {}
91 }
92
93 tokio::time::sleep(backoff).await;
94 backoff = (backoff * 2).min(max_backoff);
95 }
96 };
97 if tokio::time::timeout(config_sync_timeout, init)
98 .await
99 .is_err()
100 {
101 tracing::info!("SyncedConfigSet initialize on boot: initialize has timed out");
102 }
103 Some(client)
104 } else {
105 None
106 };
107
108 let synced = SyncedConfigSet {
109 set,
110 ld_client,
111 ld_ctx: ld_ctx(build_info, ctx_builder)?,
112 on_update,
113 };
114 synced.sync()?;
115 task::spawn(
116 || "SyncedConfigSet sync_loop",
117 synced.sync_loop(config_sync_loop_interval),
118 );
119 Ok(())
120}
121
122fn ld_ctx<F>(build_info: &'static BuildInfo, ctx_builder: F) -> Result<ld::Context, anyhow::Error>
123where
124 F: FnOnce(&mut ld::MultiContextBuilder) -> Result<(), anyhow::Error>,
125{
126 let mut builder = ld::MultiContextBuilder::new();
133
134 builder.add_context(
135 ld::ContextBuilder::new(build_info.sha)
136 .kind("build")
137 .set_string("semver_version", build_info.semver_version().to_string())
138 .build()
139 .map_err(|e| anyhow::anyhow!(e))?,
140 );
141
142 ctx_builder(&mut builder)?;
143
144 builder.build().map_err(|e| anyhow::anyhow!(e))
145}
146
147struct SyncedConfigSet<F>
148where
149 F: Fn(&ConfigUpdates, &ConfigSet) + Send,
150{
151 set: ConfigSet,
152 ld_client: Option<ld::Client>,
153 ld_ctx: ld::Context,
154 on_update: F,
155}
156
157impl<F: Fn(&ConfigUpdates, &ConfigSet) + Send> SyncedConfigSet<F> {
158 async fn sync_loop(self, tick_interval: Option<Duration>) {
160 let Some(tick_interval) = tick_interval else {
161 tracing::info!("skipping SyncedConfigSet sync as tick_interval = None");
162 return;
163 };
164
165 let mut interval = time::interval(tick_interval);
166 interval.set_missed_tick_behavior(time::MissedTickBehavior::Skip);
167
168 tracing::info!(
169 "synchronizing SyncedConfigSet values every {} seconds",
170 tick_interval.as_secs()
171 );
172
173 loop {
174 interval.tick().await;
175
176 if let Err(err) = self.sync() {
177 tracing::info!("SyncedConfigSet: {err}");
178 }
179 }
180 }
181
182 fn sync(&self) -> Result<(), anyhow::Error> {
184 let mut updates = ConfigUpdates::default();
185 let Some(ld_client) = &self.ld_client else {
186 (self.on_update)(&updates, &self.set);
187 return Ok(());
188 };
189 for entry in self.set.entries() {
190 let val = dyn_into_flag(entry.val()).expect("new() verifies all configs can convert");
191 let flag_var = ld_client.variation(&self.ld_ctx, entry.name(), val);
192 let update = match (entry.val(), flag_var) {
193 (ConfigVal::Bool(_), ld::FlagValue::Bool(flag)) => ConfigVal::Bool(flag),
194 (ConfigVal::U32(_), ld::FlagValue::Number(flag)) => {
195 ConfigVal::U32(u32::cast_lossy(flag))
196 }
197 (ConfigVal::Usize(_), ld::FlagValue::Number(flag)) => {
198 ConfigVal::Usize(usize::cast_lossy(flag))
199 }
200 (ConfigVal::F64(_), ld::FlagValue::Number(flag)) => ConfigVal::F64(flag),
201 (ConfigVal::String(_), ld::FlagValue::Str(flag)) => ConfigVal::String(flag),
202 (ConfigVal::Duration(_), ld::FlagValue::Str(flag)) => {
203 match humantime::parse_duration(&flag) {
204 Ok(d) => ConfigVal::Duration(d),
205 Err(e) => {
206 tracing::warn!("failed to parse Duration for {}: {}", entry.name(), e);
207 continue;
208 }
209 }
210 }
211 (ConfigVal::Json(_), ld::FlagValue::Json(flag)) => ConfigVal::Json(flag),
212
213 (ConfigVal::Bool(_), _)
216 | (ConfigVal::U32(_), _)
217 | (ConfigVal::Usize(_), _)
218 | (ConfigVal::F64(_), _)
219 | (ConfigVal::Duration(_), _)
220 | (ConfigVal::Json(_), _)
221 | (ConfigVal::OptUsize(_), _)
222 | (ConfigVal::String(_), _)
223 | (ConfigVal::OptString(_), _) => {
224 tracing::warn!("LD flag type mismatch for {}", entry.name());
225 continue;
226 }
227 };
228 tracing::debug!(
229 "updating config value {} from {:?} to {:?}",
230 &entry.name(),
231 &entry.val(),
232 update
233 );
234 updates.add_dynamic(entry.name(), update);
235 }
236 updates.apply(&self.set);
237 (self.on_update)(&updates, &self.set);
238 Ok(())
239 }
240}
241
242fn dyn_into_flag(val: ConfigVal) -> Result<ld::FlagValue, anyhow::Error> {
245 Ok(match val {
249 ConfigVal::Bool(v) => ld::FlagValue::Bool(v),
250 ConfigVal::U32(v) => ld::FlagValue::Number(v.into()),
251 ConfigVal::Usize(v) => ld::FlagValue::Number(f64::cast_lossy(v)),
252 ConfigVal::OptUsize(_) => anyhow::bail!("OptUsize None cannot be converted to a FlagValue"),
253 ConfigVal::F64(v) => ld::FlagValue::Number(v),
254 ConfigVal::String(v) => ld::FlagValue::Str(v),
255 ConfigVal::OptString(_) => {
256 anyhow::bail!("OptString None cannot be converted to a FlagValue")
257 }
258 ConfigVal::Duration(v) => ld::FlagValue::Str(humantime::format_duration(v).to_string()),
259 ConfigVal::Json(v) => ld::FlagValue::Json(v),
260 })
261}