use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crabka_connect::{ConnectorRuntime, RuntimeState};
use tracing::warn;
use crate::checkpoint_store::InternalTopicCheckpointStore;
use crate::config::{NamingPolicy, PolicyConfig};
use crate::record::ReplicatedRecord;
use crate::selector::Selector;
use crate::sink::{SinkParams, TargetSink};
use crate::source::SourceConsumer;
use crate::tasks::checkpoint::{CheckpointParams, CheckpointTask};
use crate::tasks::heartbeat::{HeartbeatParams, HeartbeatTask};
const MAX_BUILD_ELAPSED: Duration = Duration::from_secs(30);
const INITIAL_BACKOFF: Duration = Duration::from_millis(250);
const MAX_BACKOFF: Duration = Duration::from_secs(8);
pub struct FlowWorkerParams {
pub flow_name: String,
pub source_bootstrap: String,
pub target_bootstrap: String,
pub source_alias: String,
pub target_alias: String,
pub naming: NamingPolicy,
pub topics: Vec<String>,
pub target_zones: Vec<String>,
pub policies: Vec<PolicyConfig>,
pub group_selector: Selector,
pub security_source: Option<crabka_client_core::security::ClientSecurity>,
pub security_target: Option<crabka_client_core::security::ClientSecurity>,
}
pub struct FlowWorker {
runtime: crabka_connect::ConnectorHandle,
heartbeat: HeartbeatTask,
checkpoint: CheckpointTask,
}
fn now_ms() -> i64 {
i64::try_from(
SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_millis(),
)
.unwrap_or(i64::MAX)
}
fn next_backoff(current: Duration) -> Duration {
(current * 2).min(MAX_BACKOFF)
}
impl FlowWorker {
pub async fn start(p: FlowWorkerParams) -> crate::Result<Self> {
let mut backoff = INITIAL_BACKOFF;
let mut elapsed = Duration::ZERO;
loop {
match Self::build(&p).await {
Ok(worker) => return Ok(worker),
Err(e) => {
if elapsed >= MAX_BUILD_ELAPSED {
return Err(e);
}
warn!(
flow = %p.flow_name,
error = %e,
backoff_ms = backoff.as_millis(),
"flow worker build failed; retrying after backoff"
);
tokio::time::sleep(backoff).await;
elapsed += backoff;
backoff = next_backoff(backoff);
}
}
}
}
async fn build(p: &FlowWorkerParams) -> crate::Result<Self> {
let group_id = format!("crabka-replicator-{}", p.flow_name);
let source = SourceConsumer::start(
&p.source_bootstrap,
&group_id,
&p.topics,
p.security_source.clone(),
)
.await?;
let sink = TargetSink::start(SinkParams {
target_bootstrap: p.target_bootstrap.clone(),
source_alias: p.source_alias.clone(),
naming: p.naming,
target_zones: p.target_zones.clone(),
policies: p.policies.clone(),
security: p.security_target.clone(),
})
.await?;
let store = InternalTopicCheckpointStore::start(
&p.target_bootstrap,
&p.flow_name,
p.security_target.clone(),
)
.await?;
let runtime = ConnectorRuntime::<(), ReplicatedRecord>::new()
.add_source(source)
.add_sink(sink)
.checkpoint_store(Arc::new(store))
.commit_interval(Duration::from_millis(500))
.max_batch(500)
.run()?;
let heartbeat = HeartbeatTask::start(HeartbeatParams {
target_bootstrap: p.target_bootstrap.clone(),
source_alias: p.source_alias.clone(),
target_alias: p.target_alias.clone(),
interval: Duration::from_secs(1),
now_ms,
security: p.security_target.clone(),
})
.await?;
let checkpoint = CheckpointTask::start(
CheckpointParams {
source_bootstrap: p.source_bootstrap.clone(),
target_bootstrap: p.target_bootstrap.clone(),
source_alias: p.source_alias.clone(),
naming: p.naming,
group_selector: p.group_selector.clone(),
security: p.security_target.clone(),
},
Duration::from_secs(5),
)
.await?;
Ok(Self {
runtime,
heartbeat,
checkpoint,
})
}
#[must_use]
pub fn state(&self) -> RuntimeState {
self.runtime.state()
}
pub async fn shutdown(self) {
let _ = self.runtime.shutdown().await;
self.heartbeat.shutdown().await;
self.checkpoint.shutdown().await;
}
}
#[cfg(test)]
mod tests {
use assert2::assert;
use super::*;
#[test]
fn now_ms_is_a_recent_epoch_millis() {
assert!(super::now_ms() > 1_700_000_000_000);
}
#[test]
fn next_backoff_doubles_and_caps() {
assert!(super::next_backoff(Duration::from_millis(250)) == Duration::from_millis(500));
assert!(super::next_backoff(MAX_BACKOFF) == MAX_BACKOFF);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 4)]
async fn worker_replicates_one_flow() {
let s_dir = tempfile::TempDir::new().unwrap();
let t_dir = tempfile::TempDir::new().unwrap();
let source = crabka_broker::Broker::start(crabka_broker::BrokerConfig::for_tests(
s_dir.path().to_path_buf(),
))
.await
.unwrap();
let target = crabka_broker::Broker::start(crabka_broker::BrokerConfig::for_tests(
t_dir.path().to_path_buf(),
))
.await
.unwrap();
let sb = source.listen_addr().to_string();
let tb = target.listen_addr().to_string();
crate::test_util::create_topic(&sb, "orders", 1).await;
crate::test_util::produce(&sb, "orders", b"k", b"v").await;
let worker = FlowWorker::start(FlowWorkerParams {
flow_name: "us-east__eu-west".into(),
source_bootstrap: sb,
target_bootstrap: tb.clone(),
source_alias: "us-east".into(),
target_alias: "eu-west".into(),
naming: crate::config::NamingPolicy::Default,
topics: vec!["orders".to_string()],
target_zones: vec!["us".into()],
policies: vec![],
group_selector: crate::selector::Selector::compile(&[], &[]).unwrap(),
security_source: None,
security_target: None,
})
.await
.unwrap();
crate::test_util::await_topic_count(
&tb,
"us-east.orders",
1,
std::time::Duration::from_secs(15),
)
.await;
worker.shutdown().await;
assert!(crate::test_util::topic_record_count(&tb, "us-east.orders").await >= 1);
}
}