use dora_core::descriptor::{CoreNodeKind, Descriptor, DescriptorExt, resolve_path};
use dora_message::cli_to_coordinator::ControlRequest;
use dora_message::common::LogMessage;
use dora_message::coordinator_to_cli::ControlRequestReply;
use eyre::Context;
use notify::event::ModifyKind;
use notify::{Config, Event as NotifyEvent, EventKind, RecommendedWatcher, RecursiveMode, Watcher};
use std::collections::HashMap;
use std::{path::PathBuf, sync::mpsc, time::Duration};
use tracing::{error, info};
use uuid::Uuid;
use crate::common::handle_dataflow_result;
use crate::output::{LogOutputConfig, print_log_message};
use crate::ws_client::WsSession;
pub fn attach_dataflow(
dataflow: Descriptor,
dataflow_path: PathBuf,
dataflow_id: Uuid,
session: &WsSession,
hot_reload: bool,
log_level: log::LevelFilter,
) -> Result<(), eyre::ErrReport> {
let (tx, rx) = mpsc::channel();
let mut node_path_lookup = HashMap::new();
let nodes = dataflow.resolve_aliases_and_set_defaults()?;
let print_daemon_name = nodes.values().any(|n| n.deploy.is_some());
let working_dir = dataflow_path
.canonicalize()
.context("failed to canonicalize dataflow path")?
.parent()
.ok_or_else(|| eyre::eyre!("canonicalized dataflow path has no parent"))?
.to_owned();
for node in nodes.into_values() {
match node.kind {
CoreNodeKind::Custom(_cn) => (),
CoreNodeKind::Runtime(rn) => {
for op in rn.operators.iter() {
if let dora_core::descriptor::OperatorSource::Python(python_source) =
&op.config.source
{
let path = resolve_path(&python_source.source, &working_dir)
.wrap_err_with(|| {
format!("failed to resolve node source `{}`", python_source.source)
})?;
node_path_lookup
.insert(path, (dataflow_id, node.id.clone(), Some(op.id.clone())));
}
}
}
}
}
let watcher_tx = tx.clone();
let _watcher = if hot_reload {
let hash = node_path_lookup.clone();
let paths = hash.keys();
let notifier = move |event| {
if let Ok(NotifyEvent {
paths,
kind: EventKind::Modify(ModifyKind::Data(_data)),
..
}) = event
{
for path in paths {
if let Some((dataflow_id, node_id, operator_id)) = node_path_lookup.get(&path) {
watcher_tx
.send(AttachEvent::Control(ControlRequest::Reload {
dataflow_id: *dataflow_id,
node_id: node_id.clone(),
operator_id: operator_id.clone(),
}))
.context("Could not send reload request to the cli loop")
.unwrap();
}
}
}
};
let mut watcher = RecommendedWatcher::new(
notifier,
Config::default().with_poll_interval(Duration::from_secs(1)),
)?;
for path in paths {
watcher.watch(path, RecursiveMode::Recursive)?;
}
Some(watcher)
} else {
None
};
let ctrlc_tx = tx.clone();
let mut ctrlc_sent = false;
ctrlc::set_handler(move || {
if ctrlc_sent {
ctrlc_tx
.send(AttachEvent::Control(ControlRequest::Stop {
dataflow_uuid: dataflow_id,
grace_duration: None,
force: true,
}))
.ok();
std::process::exit(1);
} else {
let _ = ctrlc_tx.send(AttachEvent::Control(ControlRequest::Stop {
dataflow_uuid: dataflow_id,
grace_duration: None,
force: false,
}));
ctrlc_sent = true;
}
})
.wrap_err("failed to set ctrl-c handler")?;
let log_rx = session.subscribe_logs(
&serde_json::to_vec(&ControlRequest::LogSubscribe {
dataflow_id,
level: log_level,
})
.wrap_err("failed to serialize message")?,
)?;
std::thread::spawn(move || {
while let Ok(raw) = log_rx.recv() {
let parsed: eyre::Result<LogMessage> = match raw {
Ok(bytes) => serde_json::from_slice(&bytes).context("failed to parse log message"),
Err(err) => Err(err),
};
if tx.send(AttachEvent::Log(parsed)).is_err() {
break;
}
}
});
loop {
let control_request = match rx.recv_timeout(Duration::from_secs(1)) {
Err(_err) => ControlRequest::Check {
dataflow_uuid: dataflow_id,
},
Ok(AttachEvent::Control(control_request)) => control_request,
Ok(AttachEvent::Log(Ok(log_message))) => {
let config = LogOutputConfig {
print_daemon_name,
..LogOutputConfig::default()
};
print_log_message(log_message, &config);
continue;
}
Ok(AttachEvent::Log(Err(err))) => {
tracing::warn!("failed to parse log message: {:#?}", err);
continue;
}
};
let reply_raw = session
.request(&serde_json::to_vec(&control_request)?)
.wrap_err("failed to send request message to coordinator")?;
let result: ControlRequestReply =
serde_json::from_slice(&reply_raw).wrap_err("failed to parse reply")?;
match result {
ControlRequestReply::DataflowSpawned { uuid: _ } => (),
ControlRequestReply::DataflowStopped { uuid, result } => {
info!("dataflow {uuid} stopped");
break handle_dataflow_result(result, Some(uuid));
}
ControlRequestReply::DataflowReloaded { uuid } => {
info!("dataflow {uuid} reloaded")
}
other => error!("Received unexpected Coordinator Reply: {:#?}", other),
};
}
}
#[allow(clippy::large_enum_variant)]
enum AttachEvent {
Control(ControlRequest),
Log(eyre::Result<LogMessage>),
}