1use krabka_client_admin::AdminClient;
9use krabka_connect::RuntimeState;
10use krabka_units::prelude::TimeExt as _;
11use tokio::{
12 sync::{oneshot, watch},
13 task::JoinHandle,
14};
15
16use crate::{
17 config::{
18 ClientResourcePolicy, Delivery, NamingPolicy, PolicyConfig, ReplicatorConfig,
19 ReplicatorRuntimePolicy,
20 },
21 error::ReplicatorError,
22 naming::Renamer,
23 selector::Selector,
24 worker::{FlowWorker, FlowWorkerParams},
25};
26
27#[derive(Clone)]
34struct RebuildSpec {
35 flow_name: String,
36 source_bootstrap: String,
37 target_bootstrap: String,
38 source_alias: String,
39 target_alias: String,
40 naming: NamingPolicy,
41 delivery: Delivery,
42 topics: Vec<String>,
43 source_partition_counts: std::collections::BTreeMap<String, i32>,
44 target_zones: Vec<String>,
45 policies: Vec<PolicyConfig>,
46 group_selector: Selector,
47 client_resource_policy: ClientResourcePolicy,
48 runtime_policy: ReplicatorRuntimePolicy,
49}
50
51fn make_params(spec: &RebuildSpec) -> FlowWorkerParams {
54 FlowWorkerParams {
55 flow_name: spec.flow_name.clone(),
56 source_bootstrap: spec.source_bootstrap.clone(),
57 target_bootstrap: spec.target_bootstrap.clone(),
58 source_alias: spec.source_alias.clone(),
59 target_alias: spec.target_alias.clone(),
60 naming: spec.naming,
61 delivery: spec.delivery,
62 topics: spec.topics.clone(),
63 source_partition_counts: spec.source_partition_counts.clone(),
64 target_zones: spec.target_zones.clone(),
65 policies: spec.policies.clone(),
66 group_selector: spec.group_selector.clone(),
67 security_source: None,
68 security_target: None,
69 client_resource_policy: spec.client_resource_policy,
70 runtime_policy: spec.runtime_policy.clone(),
71 }
72}
73
74fn is_internal(name: &str) -> bool {
83 name.starts_with("__")
84 || name == "heartbeats"
85 || name == "krabka-replicator-offsets"
86 || name.ends_with(".checkpoints.internal")
87 || name.starts_with("mm2-offset-syncs.")
88}
89
90pub struct FlowSupervisor {
92 shutdown: watch::Sender<bool>,
93 crash: Option<oneshot::Sender<()>>,
94 handle: JoinHandle<()>,
95}
96
97impl FlowSupervisor {
98 pub async fn run(config: ReplicatorConfig) -> crate::Result<Self> {
105 Self::run_with_policy(config, ClientResourcePolicy::default()).await
106 }
107
108 #[tracing::instrument(
123 level = "info",
124 skip_all,
125 fields(flows = config.flows.len(), clusters = config.clusters.len()),
126 err,
127 )]
128 pub async fn run_with_policy(
129 config: ReplicatorConfig,
130 client_resource_policy: ClientResourcePolicy,
131 ) -> crate::Result<Self> {
132 Self::run_with_runtime_policy(
133 config,
134 client_resource_policy,
135 ReplicatorRuntimePolicy::default(),
136 )
137 .await
138 }
139
140 pub async fn run_with_runtime_policy(
145 config: ReplicatorConfig,
146 client_resource_policy: ClientResourcePolicy,
147 runtime_policy: ReplicatorRuntimePolicy,
148 ) -> crate::Result<Self> {
149 config.validate()?;
150
151 let mut entries: Vec<(RebuildSpec, FlowWorker)> = Vec::with_capacity(config.flows.len());
152
153 for flow in &config.flows {
154 let from = &config.clusters[&flow.from];
156 let to = &config.clusters[&flow.to];
157
158 let flow_name = format!("{}__{}", flow.from, flow.to);
159
160 let mut admin = AdminClient::connect_with_options(
162 std::slice::from_ref(&from.bootstrap),
163 krabka_client_core::ConnectionOptions {
164 dns_timeout: krabka_client_core::ClientDnsTimeout::new(
165 runtime_policy.client_dns_timeout,
166 )
167 .map_err(ReplicatorError::Client)?,
168 socket_connection_setup_timeout: runtime_policy.client_connect_timeout,
169 socket_connection_setup_timeout_max: runtime_policy.client_connect_timeout,
170 request_timeout: runtime_policy.client_request_timeout,
171 client_id: "krabka-operator".to_owned(),
172 dispatch_queue_capacity: client_resource_policy.dispatch_queue_capacity,
173 frame_max: client_resource_policy.frame_max,
174 security: None,
175 ..Default::default()
176 },
177 )
178 .await
179 .map_err(|e| ReplicatorError::Client(e.to_string()))?;
180 let md = admin
181 .metadata(&[])
182 .await
183 .map_err(|e| ReplicatorError::Client(e.to_string()))?;
184
185 let topic_sel = Selector::compile(&flow.topics.include, &flow.topics.exclude)?;
186 let renamer = Renamer::new(flow.naming, &flow.from);
187
188 let source_partition_counts: std::collections::BTreeMap<String, i32> = md
189 .topics
190 .into_iter()
191 .filter(|topic| {
192 topic_sel.matches(&topic.name)
193 && !renamer.is_remote(&topic.name)
194 && !is_internal(&topic.name)
195 })
196 .map(|topic| (topic.name, topic.partition_count))
197 .collect();
198 let mut topics: Vec<String> = source_partition_counts.keys().cloned().collect();
199 topics.sort();
200 topics.dedup();
201
202 let group_selector = Selector::compile(&flow.groups.include, &flow.groups.exclude)?;
203
204 let spec = RebuildSpec {
205 flow_name: flow_name.clone(),
206 source_bootstrap: from.bootstrap.clone(),
207 target_bootstrap: to.bootstrap.clone(),
208 source_alias: flow.from.clone(),
209 target_alias: flow.to.clone(),
210 naming: flow.naming,
211 delivery: flow.delivery,
212 topics,
213 source_partition_counts,
214 target_zones: to.zones.clone(),
215 policies: config.policies.clone(),
216 group_selector,
217 client_resource_policy,
218 runtime_policy: runtime_policy.clone(),
219 };
220
221 let worker = Box::pin(FlowWorker::start(make_params(&spec))).await?;
222 tracing::info!(
223 flow = %flow_name,
224 source = %flow.from,
225 target = %flow.to,
226 topics = spec.topics.len(),
227 "started flow worker",
228 );
229 entries.push((spec, worker));
230 }
231
232 let (shutdown, mut rx) = watch::channel(false);
233 let (crash, mut crash_rx) = oneshot::channel();
234 let handle = tokio::spawn(async move {
235 let mut ticker = tokio::time::interval(runtime_policy.supervisor_interval.to_std());
236 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
237 loop {
238 tokio::select! {
239 Ok(()) = &mut crash_rx => {
240 for (_spec, worker) in entries {
241 worker.crash().await;
242 }
243 return;
244 }
245 res = rx.changed() => {
246 if res.is_err() || *rx.borrow() {
248 break;
249 }
250 }
251 _ = ticker.tick() => {
252 for (spec, worker) in &mut entries {
253 if worker.state() == RuntimeState::Failed {
254 tracing::warn!(
255 flow = %spec.flow_name,
256 "flow worker failed; rebuilding",
257 );
258 let old = std::mem::replace(
261 worker,
262 match Box::pin(FlowWorker::start(make_params(spec))).await {
263 Ok(w) => w,
264 Err(e) => {
265 tracing::error!(
266 flow = %spec.flow_name,
267 error = %e,
268 "failed to rebuild flow worker; \
269 will retry next tick",
270 );
271 continue;
272 }
273 },
274 );
275 old.shutdown().await;
276 }
277 }
278 }
279 }
280 }
281
282 for (_spec, worker) in entries {
283 worker.shutdown().await;
284 }
285 });
286
287 Ok(Self {
288 shutdown,
289 crash: Some(crash),
290 handle,
291 })
292 }
293
294 #[cfg_attr(test, mutants::skip)]
297 pub async fn shutdown(self) {
298 let _ = self.shutdown.send(true);
299 let _ = self.handle.await;
300 }
301
302 pub async fn crash(mut self) {
304 if let Some(crash) = self.crash.take() {
305 let _ = crash.send(());
306 }
307 let _ = self.handle.await;
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use std::collections::BTreeMap;
314
315 use krabka_units::secs;
316
317 use super::*;
318 use crate::config::{
319 ClusterConfig, Delivery, FlowConfig, NamingPolicy, ReplicatorConfig, Selectors,
320 };
321
322 #[test]
323 fn is_internal_excludes_kafka_and_replicator_topics() {
324 for (topic, want) in [
325 ("__consumer_offsets", true),
326 ("heartbeats", true),
327 ("krabka-replicator-offsets", true),
328 ("us-east.eu-west.checkpoints.internal", true),
329 ("mm2-offset-syncs.us-east.internal", true),
330 ("orders", false),
331 ("telemetry.cpu", false),
332 ] {
333 assert2::assert!(is_internal(topic) == want);
334 }
335 }
336
337 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
338 async fn spawns_a_worker_per_flow_and_replicates() {
339 let s_dir = tempfile::TempDir::new().unwrap();
340 let t_dir = tempfile::TempDir::new().unwrap();
341 let source = krabka_broker::Broker::start(krabka_broker::BrokerConfig::for_tests(
342 s_dir.path().to_path_buf(),
343 ))
344 .await
345 .unwrap();
346 let target = krabka_broker::Broker::start(krabka_broker::BrokerConfig::for_tests(
347 t_dir.path().to_path_buf(),
348 ))
349 .await
350 .unwrap();
351 let sb = source.listen_addr().to_string();
352 let tb = target.listen_addr().to_string();
353
354 crate::test_util::create_topic(&sb, "orders", 1).await;
355 crate::test_util::produce(&sb, "orders", b"k", b"v").await;
356
357 crate::test_util::create_topic(&sb, "noise", 1).await;
362 crate::test_util::produce(&sb, "noise", b"k", b"v").await;
363
364 let mut clusters = BTreeMap::new();
365 clusters.insert(
366 "us-east".to_string(),
367 ClusterConfig {
368 bootstrap: sb.clone(),
369 region: "us".into(),
370 zones: vec!["us".into()],
371 },
372 );
373 clusters.insert(
374 "eu-west".to_string(),
375 ClusterConfig {
376 bootstrap: tb.clone(),
377 region: "eu".into(),
378 zones: vec!["eu".into(), "gdpr".into()],
379 },
380 );
381 let config = ReplicatorConfig {
382 clusters,
383 flows: vec![FlowConfig {
384 from: "us-east".into(),
385 to: "eu-west".into(),
386 topics: Selectors {
387 include: vec!["orders".into()],
388 exclude: vec![],
389 },
390 groups: Selectors::default(),
391 naming: NamingPolicy::Default,
392 delivery: Delivery::AtLeastOnce,
393 }],
394 policies: vec![],
395 };
396
397 let sup = FlowSupervisor::run(config).await.unwrap();
398 crate::test_util::await_topic_count(&tb, "us-east.orders", 1, secs(20)).await;
399 sup.shutdown().await;
400 assert2::assert!(crate::test_util::topic_record_count(&tb, "us-east.orders").await >= 1);
401 assert2::assert!(crate::test_util::topic_record_count(&tb, "us-east.noise").await == 0);
404 }
405}