use std::collections::HashMap;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::process::ExitCode;
use std::time::Duration;
use camel_builder::{RouteBuilder, StepAccumulator};
use camel_component_jms::{BrokerConfig, BrokerType, JmsBridgePool, JmsComponent, JmsPoolConfig};
use camel_core::CamelContext;
const DEFAULT_QUEUE: &str = "queue.test-crash-probe";
const DEFAULT_USERNAME: &str = "admin";
const DEFAULT_PASSWORD: &str = "admin";
const BRIDGE_START_TIMEOUT_MS: u64 = 90_000;
const BROKER_NAME: &str = "default";
const ROUTE_ID: &str = "jms-crash-probe";
const STATUS_POLL_INTERVAL: Duration = Duration::from_millis(200);
#[tokio::main]
async fn main() -> ExitCode {
let _ = rustls::crypto::ring::default_provider().install_default();
match run().await {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("jms_crash_probe: setup failed: {message}");
ExitCode::FAILURE
}
}
}
async fn run() -> Result<(), String> {
let artifact = std::env::args_os()
.nth(1)
.map(PathBuf::from)
.ok_or_else(|| "usage: jms_crash_probe <handoff-marker-or-receipt-path>".to_string())?;
let broker_url = required_env("CAMEL_JMS_BROKER_URL")?;
let _bridge_binary = required_env("CAMEL_JMS_BRIDGE_BINARY_PATH")?;
let broker_type = parse_broker_type(optional_env("CAMEL_JMS_BROKER_TYPE").as_deref())?;
let role = parse_role(optional_env("CAMEL_JMS_CRASH_ROLE").as_deref())?;
let username =
optional_env("CAMEL_JMS_USERNAME").unwrap_or_else(|| DEFAULT_USERNAME.to_string());
let password =
optional_env("CAMEL_JMS_PASSWORD").unwrap_or_else(|| DEFAULT_PASSWORD.to_string());
let queue = optional_env("CAMEL_JMS_CRASH_QUEUE").unwrap_or_else(|| DEFAULT_QUEUE.to_string());
let pool_config = JmsPoolConfig {
max_bridges: 1,
brokers: HashMap::from([(
BROKER_NAME.to_string(),
BrokerConfig {
broker_url,
broker_type,
username: Some(username),
password: Some(password),
},
)]),
bridge_start_timeout_ms: BRIDGE_START_TIMEOUT_MS,
..JmsPoolConfig::default()
};
let pool = JmsBridgePool::from_config(pool_config)
.map_err(|e| format!("invalid JMS pool config: {e}"))?;
let component = JmsComponent::with_scheme("jms", std::sync::Arc::new(pool));
let mut ctx = CamelContext::builder()
.build()
.await
.map_err(|e| format!("cannot build Camel context: {e}"))?;
ctx.register_component(component);
let from_uri = format!("jms:queue:{queue}");
let route = RouteBuilder::from(from_uri.as_str())
.process(move |exchange: camel_api::Exchange| {
let artifact = artifact.clone();
async move {
match role {
ProbeRole::Probe => write_probe_marker(&artifact)?,
ProbeRole::Survivor => write_survivor_receipt(&artifact, &exchange).await?,
ProbeRole::Threshold => {
append_threshold_event(&artifact, &exchange)?;
return Err(camel_api::CamelError::ProcessorError(
THRESHOLD_ROUTE_ERROR.to_string(),
));
}
ProbeRole::Gated => {
write_probe_marker(&artifact)?;
let mut release = artifact.as_os_str().to_owned();
release.push(".release");
let release = PathBuf::from(release);
while !release.is_file() {
tokio::time::sleep(STATUS_POLL_INTERVAL).await;
}
return Ok(exchange);
}
}
std::future::pending::<()>().await;
Ok(exchange)
}
})
.route_id(ROUTE_ID)
.build()
.map_err(|e| format!("cannot build crash-probe route: {e}"))?;
ctx.add_route_definition(route)
.await
.map_err(|e| format!("cannot register crash-probe route: {e}"))?;
ctx.start()
.await
.map_err(|e| format!("cannot start crash-probe context: {e}"))?;
loop {
match ctx.runtime_route_status(ROUTE_ID).await {
Ok(Some(status)) if status == "Failed" => {
return Err(format!(
"crash-probe route '{ROUTE_ID}' failed after start \
(asynchronous subscribe/startup rejection)"
));
}
Ok(Some(_)) => {}
Ok(None) => {
return Err(format!("crash-probe route '{ROUTE_ID}' is not registered"));
}
Err(e) => {
return Err(format!("cannot read crash-probe route status: {e}"));
}
}
tokio::time::sleep(STATUS_POLL_INTERVAL).await;
}
}
fn required_env(name: &str) -> Result<String, String> {
match std::env::var(name) {
Ok(value) if !value.is_empty() => Ok(value),
_ => Err(format!("{name} is required and must be non-empty")),
}
}
fn optional_env(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|value| !value.is_empty())
}
fn parse_broker_type(raw: Option<&str>) -> Result<BrokerType, String> {
match raw {
None | Some("activemq") | Some("activemq-classic") => Ok(BrokerType::ActiveMq),
Some("artemis") => Ok(BrokerType::Artemis),
Some(other) => Err(format!(
"unknown CAMEL_JMS_BROKER_TYPE '{other}': expected activemq or artemis"
)),
}
}
#[derive(Clone, Copy)]
enum ProbeRole {
Probe,
Survivor,
Threshold,
Gated,
}
const THRESHOLD_ROUTE_ERROR: &str = "jms-threshold-always-errors";
fn parse_role(raw: Option<&str>) -> Result<ProbeRole, String> {
match raw {
None | Some("probe") => Ok(ProbeRole::Probe),
Some("survivor") => Ok(ProbeRole::Survivor),
Some("threshold") => Ok(ProbeRole::Threshold),
Some("gated") => Ok(ProbeRole::Gated),
Some(other) => Err(format!(
"unknown CAMEL_JMS_CRASH_ROLE '{other}': expected probe, survivor, threshold, or gated"
)),
}
}
fn write_artifact_atomic(path: &Path, bytes: &[u8]) -> std::io::Result<()> {
write_artifact_atomic_with(path, bytes, |_| {})
}
fn write_artifact_atomic_with(
path: &Path,
bytes: &[u8],
on_staged: impl FnOnce(&Path),
) -> std::io::Result<()> {
let temp_path = atomic_temp_sibling(path);
let staged = (|| {
let mut file = std::fs::File::create(&temp_path)?;
on_staged(path);
file.write_all(bytes)
})();
if let Err(e) = staged {
let _ = std::fs::remove_file(&temp_path);
return Err(e);
}
if let Err(e) = std::fs::rename(&temp_path, path) {
let _ = std::fs::remove_file(&temp_path);
return Err(e);
}
Ok(())
}
fn atomic_temp_sibling(path: &Path) -> PathBuf {
let mut temp = path.as_os_str().to_owned();
temp.push(".tmp");
PathBuf::from(temp)
}
fn write_probe_marker(path: &Path) -> Result<(), camel_api::CamelError> {
write_artifact_atomic(path, b"accepted\n").map_err(|e| {
camel_api::CamelError::ProcessorError(format!(
"cannot write handoff marker {}: {e}",
path.display()
))
})
}
fn append_threshold_event(
path: &Path,
exchange: &camel_api::Exchange,
) -> Result<(), camel_api::CamelError> {
let message_id = exchange
.input
.header("JMSMessageID")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let reservation_count = exchange
.property("CamelJmsReservationCount")
.and_then(|v| v.as_u64())
.unwrap_or(0);
let line = format!("accepted id={message_id} reservation_count={reservation_count}\n");
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(path)
.map_err(|e| {
camel_api::CamelError::ProcessorError(format!(
"cannot open threshold event file {}: {e}",
path.display()
))
})?;
file.write_all(line.as_bytes()).map_err(|e| {
camel_api::CamelError::ProcessorError(format!(
"cannot append threshold event to {}: {e}",
path.display()
))
})
}
async fn write_survivor_receipt(
path: &Path,
exchange: &camel_api::Exchange,
) -> Result<(), camel_api::CamelError> {
let body_bytes = exchange
.input
.body
.clone()
.into_bytes(1_048_576)
.await
.map_err(|e| {
camel_api::CamelError::ProcessorError(format!("cannot read survivor body: {e}"))
})?;
let body = String::from_utf8_lossy(&body_bytes).into_owned();
let message_id = exchange
.input
.header("JMSMessageID")
.and_then(|v| v.as_str())
.unwrap_or_default()
.to_string();
let reservation_count = exchange
.property("CamelJmsReservationCount")
.and_then(|v| v.as_u64());
let receipt = serde_json::json!({
"message_id": message_id,
"body": body,
"reservation_count": reservation_count,
"headers": exchange.input.headers,
});
let json = serde_json::to_vec_pretty(&receipt).map_err(|e| {
camel_api::CamelError::ProcessorError(format!("cannot serialize survivor receipt: {e}"))
})?;
write_artifact_atomic(path, &json).map_err(|e| {
camel_api::CamelError::ProcessorError(format!(
"cannot write survivor receipt {}: {e}",
path.display()
))
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn artifact_not_visible_before_complete_write() {
let dir = tempfile::tempdir().expect("tempdir");
let target = dir.path().join("receipt.json");
write_artifact_atomic(&target, b"{\"generation\":1}").expect("write first artifact");
let observed = std::cell::RefCell::new(None::<Vec<u8>>);
write_artifact_atomic_with(&target, b"{\"generation\":2}", |target| {
*observed.borrow_mut() = Some(std::fs::read(target).expect("read target while staged"));
})
.expect("write replacement artifact");
assert_eq!(
observed.into_inner().as_deref(),
Some(&b"{\"generation\":1}"[..]),
"the target must not change until the complete artifact is published"
);
assert_eq!(
std::fs::read(&target)
.expect("read final artifact")
.as_slice(),
b"{\"generation\":2}",
"the complete replacement must be visible after publish"
);
}
}