1use std::time::Duration;
9
10use crabka_client_admin::AdminClient;
11use crabka_connect::RuntimeState;
12use tokio::sync::watch;
13use tokio::task::JoinHandle;
14
15use crate::config::{NamingPolicy, PolicyConfig, ReplicatorConfig};
16use crate::error::ReplicatorError;
17use crate::naming::Renamer;
18use crate::selector::Selector;
19use crate::worker::{FlowWorker, FlowWorkerParams};
20
21const SUPERVISE_INTERVAL: Duration = Duration::from_secs(3);
23
24#[derive(Clone)]
30struct RebuildSpec {
31 flow_name: String,
32 source_bootstrap: String,
33 target_bootstrap: String,
34 source_alias: String,
35 target_alias: String,
36 naming: NamingPolicy,
37 topics: Vec<String>,
38 target_zones: Vec<String>,
39 policies: Vec<PolicyConfig>,
40 group_selector: Selector,
41}
42
43fn make_params(spec: &RebuildSpec) -> FlowWorkerParams {
46 FlowWorkerParams {
47 flow_name: spec.flow_name.clone(),
48 source_bootstrap: spec.source_bootstrap.clone(),
49 target_bootstrap: spec.target_bootstrap.clone(),
50 source_alias: spec.source_alias.clone(),
51 target_alias: spec.target_alias.clone(),
52 naming: spec.naming,
53 topics: spec.topics.clone(),
54 target_zones: spec.target_zones.clone(),
55 policies: spec.policies.clone(),
56 group_selector: spec.group_selector.clone(),
57 security_source: None,
58 security_target: None,
59 }
60}
61
62fn is_internal(name: &str) -> bool {
70 name.starts_with("__")
71 || name == "heartbeats"
72 || name == "crabka-replicator-offsets"
73 || name.ends_with(".checkpoints.internal")
74 || name.starts_with("mm2-offset-syncs.")
75}
76
77pub struct FlowSupervisor {
79 shutdown: watch::Sender<bool>,
80 handle: JoinHandle<()>,
81}
82
83impl FlowSupervisor {
84 pub async fn run(config: ReplicatorConfig) -> crate::Result<Self> {
98 config.validate()?;
99
100 let mut entries: Vec<(RebuildSpec, FlowWorker)> = Vec::with_capacity(config.flows.len());
101
102 for flow in &config.flows {
103 let from = &config.clusters[&flow.from];
105 let to = &config.clusters[&flow.to];
106
107 let flow_name = format!("{}__{}", flow.from, flow.to);
108
109 let mut admin = AdminClient::connect(std::slice::from_ref(&from.bootstrap))
111 .await
112 .map_err(|e| ReplicatorError::Client(e.to_string()))?;
113 let md = admin
114 .metadata(&[])
115 .await
116 .map_err(|e| ReplicatorError::Client(e.to_string()))?;
117
118 let topic_sel = Selector::compile(&flow.topics.include, &flow.topics.exclude)?;
119 let renamer = Renamer::new(flow.naming, &flow.from);
120
121 let mut topics: Vec<String> = md
122 .topics
123 .into_iter()
124 .map(|t| t.name)
125 .filter(|name| {
126 topic_sel.matches(name) && !renamer.is_remote(name) && !is_internal(name)
127 })
128 .collect();
129 topics.sort();
130 topics.dedup();
131
132 let group_selector = Selector::compile(&flow.groups.include, &flow.groups.exclude)?;
133
134 let spec = RebuildSpec {
135 flow_name: flow_name.clone(),
136 source_bootstrap: from.bootstrap.clone(),
137 target_bootstrap: to.bootstrap.clone(),
138 source_alias: flow.from.clone(),
139 target_alias: flow.to.clone(),
140 naming: flow.naming,
141 topics,
142 target_zones: to.zones.clone(),
143 policies: config.policies.clone(),
144 group_selector,
145 };
146
147 let worker = FlowWorker::start(make_params(&spec)).await?;
148 tracing::info!(
149 flow = %flow_name,
150 source = %flow.from,
151 target = %flow.to,
152 topics = spec.topics.len(),
153 "started flow worker",
154 );
155 entries.push((spec, worker));
156 }
157
158 let (shutdown, mut rx) = watch::channel(false);
159 let handle = tokio::spawn(async move {
160 let mut ticker = tokio::time::interval(SUPERVISE_INTERVAL);
161 ticker.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip);
162 loop {
163 tokio::select! {
164 res = rx.changed() => {
165 if res.is_err() || *rx.borrow() {
167 break;
168 }
169 }
170 _ = ticker.tick() => {
171 for (spec, worker) in &mut entries {
172 if worker.state() == RuntimeState::Failed {
173 tracing::warn!(
174 flow = %spec.flow_name,
175 "flow worker failed; rebuilding",
176 );
177 let old = std::mem::replace(
180 worker,
181 match FlowWorker::start(make_params(spec)).await {
182 Ok(w) => w,
183 Err(e) => {
184 tracing::error!(
185 flow = %spec.flow_name,
186 error = %e,
187 "failed to rebuild flow worker; \
188 will retry next tick",
189 );
190 continue;
191 }
192 },
193 );
194 old.shutdown().await;
195 }
196 }
197 }
198 }
199 }
200
201 for (_spec, worker) in entries {
202 worker.shutdown().await;
203 }
204 });
205
206 Ok(Self { shutdown, handle })
207 }
208
209 #[cfg_attr(test, mutants::skip)]
212 pub async fn shutdown(self) {
213 let _ = self.shutdown.send(true);
214 let _ = self.handle.await;
215 }
216}
217
218#[cfg(test)]
219mod tests {
220 use std::collections::BTreeMap;
221
222 use assert2::assert;
223
224 use super::*;
225 use crate::config::{
226 ClusterConfig, Delivery, FlowConfig, NamingPolicy, ReplicatorConfig, Selectors,
227 };
228
229 #[test]
230 fn is_internal_excludes_kafka_and_replicator_topics() {
231 assert!(is_internal("__consumer_offsets"));
232 assert!(is_internal("heartbeats"));
233 assert!(is_internal("crabka-replicator-offsets"));
234 assert!(is_internal("us-east.eu-west.checkpoints.internal"));
235 assert!(is_internal("mm2-offset-syncs.us-east.internal"));
236 assert!(!is_internal("orders"));
237 assert!(!is_internal("telemetry.cpu"));
238 }
239
240 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
241 async fn spawns_a_worker_per_flow_and_replicates() {
242 let s_dir = tempfile::TempDir::new().unwrap();
243 let t_dir = tempfile::TempDir::new().unwrap();
244 let source = crabka_broker::Broker::start(crabka_broker::BrokerConfig::for_tests(
245 s_dir.path().to_path_buf(),
246 ))
247 .await
248 .unwrap();
249 let target = crabka_broker::Broker::start(crabka_broker::BrokerConfig::for_tests(
250 t_dir.path().to_path_buf(),
251 ))
252 .await
253 .unwrap();
254 let sb = source.listen_addr().to_string();
255 let tb = target.listen_addr().to_string();
256
257 crate::test_util::create_topic(&sb, "orders", 1).await;
258 crate::test_util::produce(&sb, "orders", b"k", b"v").await;
259
260 crate::test_util::create_topic(&sb, "noise", 1).await;
265 crate::test_util::produce(&sb, "noise", b"k", b"v").await;
266
267 let mut clusters = BTreeMap::new();
268 clusters.insert(
269 "us-east".to_string(),
270 ClusterConfig {
271 bootstrap: sb.clone(),
272 region: "us".into(),
273 zones: vec!["us".into()],
274 },
275 );
276 clusters.insert(
277 "eu-west".to_string(),
278 ClusterConfig {
279 bootstrap: tb.clone(),
280 region: "eu".into(),
281 zones: vec!["eu".into(), "gdpr".into()],
282 },
283 );
284 let config = ReplicatorConfig {
285 clusters,
286 flows: vec![FlowConfig {
287 from: "us-east".into(),
288 to: "eu-west".into(),
289 topics: Selectors {
290 include: vec!["orders".into()],
291 exclude: vec![],
292 },
293 groups: Selectors::default(),
294 naming: NamingPolicy::Default,
295 delivery: Delivery::AtLeastOnce,
296 }],
297 policies: vec![],
298 };
299
300 let sup = FlowSupervisor::run(config).await.unwrap();
301 crate::test_util::await_topic_count(
302 &tb,
303 "us-east.orders",
304 1,
305 std::time::Duration::from_secs(20),
306 )
307 .await;
308 sup.shutdown().await;
309 assert!(crate::test_util::topic_record_count(&tb, "us-east.orders").await >= 1);
310 assert!(crate::test_util::topic_record_count(&tb, "us-east.noise").await == 0);
313 }
314}