1use std::sync::Arc;
6use std::time::{Duration, SystemTime, UNIX_EPOCH};
7
8use crabka_connect::{ConnectorRuntime, RuntimeState};
9use tracing::warn;
10
11use crate::checkpoint_store::InternalTopicCheckpointStore;
12use crate::config::{NamingPolicy, PolicyConfig};
13use crate::record::ReplicatedRecord;
14use crate::selector::Selector;
15use crate::sink::{SinkParams, TargetSink};
16use crate::source::SourceConsumer;
17use crate::tasks::checkpoint::{CheckpointParams, CheckpointTask};
18use crate::tasks::heartbeat::{HeartbeatParams, HeartbeatTask};
19
20const MAX_BUILD_ELAPSED: Duration = Duration::from_secs(30);
22const INITIAL_BACKOFF: Duration = Duration::from_millis(250);
24const MAX_BACKOFF: Duration = Duration::from_secs(8);
26
27pub struct FlowWorkerParams {
31 pub flow_name: String,
34 pub source_bootstrap: String,
36 pub target_bootstrap: String,
38 pub source_alias: String,
40 pub target_alias: String,
42 pub naming: NamingPolicy,
44 pub topics: Vec<String>,
46 pub target_zones: Vec<String>,
48 pub policies: Vec<PolicyConfig>,
50 pub group_selector: Selector,
52 pub security_source: Option<crabka_client_core::security::ClientSecurity>,
54 pub security_target: Option<crabka_client_core::security::ClientSecurity>,
56}
57
58pub struct FlowWorker {
61 runtime: crabka_connect::ConnectorHandle,
62 heartbeat: HeartbeatTask,
63 checkpoint: CheckpointTask,
64}
65
66fn now_ms() -> i64 {
68 i64::try_from(
69 SystemTime::now()
70 .duration_since(UNIX_EPOCH)
71 .unwrap_or_default()
72 .as_millis(),
73 )
74 .unwrap_or(i64::MAX)
75}
76
77fn next_backoff(current: Duration) -> Duration {
80 (current * 2).min(MAX_BACKOFF)
81}
82
83impl FlowWorker {
84 #[cfg_attr(test, mutants::skip)]
96 pub async fn start(p: FlowWorkerParams) -> crate::Result<Self> {
97 let mut backoff = INITIAL_BACKOFF;
98 let mut elapsed = Duration::ZERO;
99
100 loop {
101 match Self::build(&p).await {
102 Ok(worker) => return Ok(worker),
103 Err(e) => {
104 if elapsed >= MAX_BUILD_ELAPSED {
105 return Err(e);
106 }
107 warn!(
108 flow = %p.flow_name,
109 error = %e,
110 backoff_ms = backoff.as_millis(),
111 "flow worker build failed; retrying after backoff"
112 );
113 tokio::time::sleep(backoff).await;
114 elapsed += backoff;
115 backoff = next_backoff(backoff);
116 }
117 }
118 }
119 }
120
121 async fn build(p: &FlowWorkerParams) -> crate::Result<Self> {
124 let group_id = format!("crabka-replicator-{}", p.flow_name);
125
126 let source = SourceConsumer::start(
127 &p.source_bootstrap,
128 &group_id,
129 &p.topics,
130 p.security_source.clone(),
131 )
132 .await?;
133
134 let sink = TargetSink::start(SinkParams {
135 target_bootstrap: p.target_bootstrap.clone(),
136 source_alias: p.source_alias.clone(),
137 naming: p.naming,
138 target_zones: p.target_zones.clone(),
139 policies: p.policies.clone(),
140 security: p.security_target.clone(),
141 })
142 .await?;
143
144 let store = InternalTopicCheckpointStore::start(
145 &p.target_bootstrap,
146 &p.flow_name,
147 p.security_target.clone(),
148 )
149 .await?;
150
151 let runtime = ConnectorRuntime::<(), ReplicatedRecord>::new()
152 .add_source(source)
153 .add_sink(sink)
154 .checkpoint_store(Arc::new(store))
155 .commit_interval(Duration::from_millis(500))
156 .max_batch(500)
157 .run()?;
158
159 let heartbeat = HeartbeatTask::start(HeartbeatParams {
160 target_bootstrap: p.target_bootstrap.clone(),
161 source_alias: p.source_alias.clone(),
162 target_alias: p.target_alias.clone(),
163 interval: Duration::from_secs(1),
164 now_ms,
165 security: p.security_target.clone(),
166 })
167 .await?;
168
169 let checkpoint = CheckpointTask::start(
170 CheckpointParams {
171 source_bootstrap: p.source_bootstrap.clone(),
172 target_bootstrap: p.target_bootstrap.clone(),
173 source_alias: p.source_alias.clone(),
174 naming: p.naming,
175 group_selector: p.group_selector.clone(),
176 security: p.security_target.clone(),
177 },
178 Duration::from_secs(5),
179 )
180 .await?;
181
182 Ok(Self {
183 runtime,
184 heartbeat,
185 checkpoint,
186 })
187 }
188
189 #[must_use]
192 pub fn state(&self) -> RuntimeState {
193 self.runtime.state()
194 }
195
196 #[cfg_attr(test, mutants::skip)]
200 pub async fn shutdown(self) {
201 let _ = self.runtime.shutdown().await;
202 self.heartbeat.shutdown().await;
203 self.checkpoint.shutdown().await;
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use assert2::assert;
210
211 use super::*;
212
213 #[test]
214 fn now_ms_is_a_recent_epoch_millis() {
215 assert!(super::now_ms() > 1_700_000_000_000);
218 }
219
220 #[test]
221 fn next_backoff_doubles_and_caps() {
222 assert!(super::next_backoff(Duration::from_millis(250)) == Duration::from_millis(500));
224 assert!(super::next_backoff(MAX_BACKOFF) == MAX_BACKOFF);
226 }
227
228 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
229 async fn worker_replicates_one_flow() {
230 let s_dir = tempfile::TempDir::new().unwrap();
231 let t_dir = tempfile::TempDir::new().unwrap();
232 let source = crabka_broker::Broker::start(crabka_broker::BrokerConfig::for_tests(
233 s_dir.path().to_path_buf(),
234 ))
235 .await
236 .unwrap();
237 let target = crabka_broker::Broker::start(crabka_broker::BrokerConfig::for_tests(
238 t_dir.path().to_path_buf(),
239 ))
240 .await
241 .unwrap();
242 let sb = source.listen_addr().to_string();
243 let tb = target.listen_addr().to_string();
244
245 crate::test_util::create_topic(&sb, "orders", 1).await;
246 crate::test_util::produce(&sb, "orders", b"k", b"v").await;
247
248 let worker = FlowWorker::start(FlowWorkerParams {
249 flow_name: "us-east__eu-west".into(),
250 source_bootstrap: sb,
251 target_bootstrap: tb.clone(),
252 source_alias: "us-east".into(),
253 target_alias: "eu-west".into(),
254 naming: crate::config::NamingPolicy::Default,
255 topics: vec!["orders".to_string()],
256 target_zones: vec!["us".into()],
257 policies: vec![],
258 group_selector: crate::selector::Selector::compile(&[], &[]).unwrap(),
259 security_source: None,
260 security_target: None,
261 })
262 .await
263 .unwrap();
264
265 crate::test_util::await_topic_count(
266 &tb,
267 "us-east.orders",
268 1,
269 std::time::Duration::from_secs(15),
270 )
271 .await;
272
273 worker.shutdown().await;
274 assert!(crate::test_util::topic_record_count(&tb, "us-east.orders").await >= 1);
275 }
276}