use std::io::{self, Read, Write};
use std::path::PathBuf;
use std::process;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use crate::embed::protocol::{
self, ErrorKind, Frame, HealthResponse, MsgType, WorkerError, WorkerState,
};
use crate::embed::runtime::{RuntimeConfig, WorkerRuntime, low_memory_refusal};
const MAX_SOCKET_CLIENT_THREADS: usize = 16;
fn max_socket_clients() -> usize {
std::env::var("LEINDEX_WORKER_MAX_SOCKET_CLIENTS")
.ok()
.and_then(|v| v.parse::<usize>().ok())
.filter(|&v| v > 0)
.unwrap_or(MAX_SOCKET_CLIENT_THREADS)
}
struct SocketClientSlotGuard {
active: Arc<AtomicUsize>,
}
impl Drop for SocketClientSlotGuard {
fn drop(&mut self) {
self.active.fetch_sub(1, Ordering::Relaxed);
}
}
pub fn run() -> ! {
let argv: Vec<String> = std::env::args().collect();
if argv.len() == 2 && (argv[1] == "--version" || argv[1] == "-V") {
println!("leindex-embed {}", env!("CARGO_PKG_VERSION"));
process::exit(0);
}
let socket_path = match parse_socket_arg(&argv) {
Ok(path) => path,
Err(message) => {
eprintln!("{message}");
process::exit(2);
}
};
#[cfg(target_os = "linux")]
{
unsafe {
let rc = libc::prctl(
libc::PR_SET_PDEATHSIG,
libc::SIGKILL as libc::c_ulong,
0,
0,
0,
);
if rc != 0 {
eprintln!(
"leindex-embed: warning: prctl(PR_SET_PDEATHSIG) failed (rc={}, errno may follow); \
worker will rely on idle timeout for cleanup",
rc
);
}
}
let ppid = unsafe { libc::getppid() };
if ppid == 1 {
eprintln!(
"leindex-embed: parent process already exited during startup (ppid=1); \
exiting to avoid orphaned worker"
);
process::exit(0);
}
}
let _ = tracing_subscriber::fmt()
.with_writer(std::io::stderr)
.with_env_filter(
tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| tracing_subscriber::EnvFilter::new("info")),
)
.try_init();
tracing::info!("leindex-embed worker starting");
let mut config = RuntimeConfig::from_env();
if socket_path.is_some() {
config.idle_timeout = Duration::from_secs(600);
}
if let Some(reason) = low_memory_refusal(&config) {
eprintln!("leindex-embed: {reason}");
process::exit(1);
}
if let Some(path) = socket_path {
if let Err(e) = run_socket_worker(config, path) {
tracing::error!("socket worker failed: {}", e);
process::exit(1);
}
} else {
let loop_error = {
let runtime = WorkerRuntime::new(config);
runtime.run(io::stdin(), io::stdout()).err()
};
if let Some(e) = loop_error {
tracing::error!("worker loop failed: {}", e);
process::exit(1);
}
}
tracing::info!("leindex-embed worker exiting cleanly");
process::exit(0);
}
fn parse_socket_arg(argv: &[String]) -> Result<Option<PathBuf>, &'static str> {
let mut iter = argv.iter();
while let Some(arg) = iter.next() {
if arg == "--socket" {
return iter
.next()
.map(PathBuf::from)
.map(Some)
.ok_or("--socket requires a path");
}
}
Ok(None)
}
#[cfg(unix)]
fn run_socket_worker(config: RuntimeConfig, socket_path: PathBuf) -> anyhow::Result<()> {
use std::os::unix::net::UnixListener;
let status_path = socket_path.with_extension("status");
let pid_path = socket_path.with_extension("pid");
let start_time_path = socket_path.with_extension("start");
let initial_health = HealthResponse {
state: WorkerState::Initializing,
phase: "initializing".to_string(),
started_unix_ms: unix_now_ms(),
provider: Some(config.execution_provider.clone()),
model: config.model_name.clone(),
error: None,
};
write_worker_pid(&pid_path, process::id())?;
#[cfg(target_os = "linux")]
write_worker_start_time(&start_time_path, process::id())?;
write_worker_status(&status_path, "initializing")?;
if socket_path.exists() {
std::fs::remove_file(&socket_path)?;
}
if let Some(parent) = socket_path.parent() {
std::fs::create_dir_all(parent)?;
}
let listener = match UnixListener::bind(&socket_path) {
Ok(listener) => listener,
Err(error) => {
let _ = write_worker_status(&status_path, "failed");
let _ = std::fs::remove_file(&status_path);
let _ = std::fs::remove_file(&pid_path);
let _ = std::fs::remove_file(&start_time_path);
return Err(error.into());
}
};
listener.set_nonblocking(true)?;
let lifecycle = Arc::new(SocketLifecycle::new(initial_health, config.max_frame_size));
spawn_runtime_init(config, Arc::clone(&lifecycle), status_path.clone());
tracing::info!(
"leindex-embed socket worker listening at {}",
socket_path.display()
);
run_socket_accept_loop(listener, lifecycle, socket_path, status_path, pid_path)
}
#[cfg(unix)]
fn run_socket_accept_loop(
listener: std::os::unix::net::UnixListener,
lifecycle: Arc<SocketLifecycle>,
socket_path: PathBuf,
status_path: PathBuf,
pid_path: PathBuf,
) -> anyhow::Result<()> {
let max_clients = max_socket_clients();
let active_clients = Arc::new(AtomicUsize::new(0));
loop {
if lifecycle.is_failed() {
tracing::error!("socket worker initialization failed; shutting down");
shutdown_worker(&lifecycle, &socket_path, &status_path, &pid_path);
return Ok(());
}
if let Some(runtime) = lifecycle.runtime() {
if runtime.is_idle_expired() {
tracing::info!("socket worker idle timeout expired");
shutdown_worker(&lifecycle, &socket_path, &status_path, &pid_path);
return Ok(());
}
if runtime.rss_over_cap() {
shutdown_worker(&lifecycle, &socket_path, &status_path, &pid_path);
return Ok(());
}
}
match listener.accept() {
Ok((stream, _addr)) => {
if let Err(error) = stream.set_nonblocking(false) {
tracing::warn!(error = %error, "failed to make worker client socket blocking");
continue;
}
if active_clients
.fetch_update(Ordering::Relaxed, Ordering::Relaxed, |n| {
(n < max_clients).then_some(n + 1)
})
.is_err()
{
tracing::warn!(
max_clients,
"worker socket client concurrency cap reached; dropping connection"
);
std::thread::sleep(std::time::Duration::from_millis(1));
continue;
}
let connection_lifecycle = Arc::clone(&lifecycle);
let slot_guard = SocketClientSlotGuard {
active: Arc::clone(&active_clients),
};
if let Err(error) = std::thread::Builder::new().spawn(move || {
handle_socket_client(connection_lifecycle, stream);
drop(slot_guard);
}) {
tracing::warn!(
error = %error,
"failed to spawn socket client thread; connection dropped and slot released"
);
}
}
Err(e) => {
let Some(delay) = accept_retry_delay(&e) else {
let _ = write_worker_status(&status_path, "failed");
let _ = std::fs::remove_file(&status_path);
let _ = std::fs::remove_file(&pid_path);
return Err(e.into());
};
wait_accept_retry(&e, delay);
}
}
}
}
#[cfg(unix)]
fn spawn_runtime_init(
config: RuntimeConfig,
lifecycle: Arc<SocketLifecycle>,
status_path: PathBuf,
) {
std::thread::spawn(move || {
let result =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| WorkerRuntime::new(config)));
match result {
Ok(runtime) if runtime.is_neural_ready() => {
runtime.log_startup_report();
let health = runtime.health_response(WorkerState::Ready, None);
lifecycle.set_ready(runtime, health);
let _ = write_worker_status(&status_path, "ready");
}
Ok(runtime) => {
runtime.log_startup_report();
let error = "neural runtime unavailable after initialization".to_string();
let health = runtime.health_response(WorkerState::Failed, Some(error));
lifecycle.set_failed(health);
let _ = write_worker_status(&status_path, "failed");
}
Err(_) => {
let health =
lifecycle.failed_health("worker runtime initialization panicked".to_string());
lifecycle.set_failed(health);
let _ = write_worker_status(&status_path, "failed");
}
}
});
}
#[cfg(unix)]
fn shutdown_worker(
lifecycle: &SocketLifecycle,
socket_path: &std::path::Path,
status_path: &std::path::Path,
pid_path: &std::path::Path,
) {
lifecycle.shutdown();
let _ = std::fs::remove_file(socket_path);
let _ = std::fs::remove_file(status_path);
let _ = std::fs::remove_file(pid_path);
let _ = std::fs::remove_file(socket_path.with_extension("start"));
}
#[cfg(unix)]
fn wait_accept_retry(error: &io::Error, delay: Duration) {
if error.kind() != io::ErrorKind::WouldBlock {
tracing::warn!(error = %error, "transient socket accept error; retrying");
}
if !delay.is_zero() {
std::thread::sleep(delay);
}
}
#[cfg(unix)]
enum SocketLifecycleState {
Initializing(HealthResponse),
Ready {
runtime: Box<WorkerRuntime>,
health: HealthResponse,
},
Failed(HealthResponse),
}
#[cfg(unix)]
struct SocketLifecycle {
state: Mutex<SocketLifecycleState>,
max_frame_size: usize,
}
#[cfg(unix)]
impl SocketLifecycle {
fn new(health: HealthResponse, max_frame_size: usize) -> Self {
Self {
state: Mutex::new(SocketLifecycleState::Initializing(health)),
max_frame_size,
}
}
fn max_frame(&self) -> usize {
self.max_frame_size
}
fn runtime(&self) -> Option<WorkerRuntime> {
let state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
match &*state {
SocketLifecycleState::Ready { runtime, .. } => Some((**runtime).clone()),
SocketLifecycleState::Initializing(_) | SocketLifecycleState::Failed(_) => None,
}
}
fn health(&self) -> HealthResponse {
let state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
match &*state {
SocketLifecycleState::Initializing(health)
| SocketLifecycleState::Failed(health)
| SocketLifecycleState::Ready { health, .. } => health.clone(),
}
}
fn is_failed(&self) -> bool {
let state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
matches!(&*state, SocketLifecycleState::Failed(_))
}
fn failed_health(&self, error: String) -> HealthResponse {
let mut health = self.health();
health.state = WorkerState::Failed;
health.phase = "failed".to_string();
health.error = Some(error);
health
}
fn set_ready(&self, runtime: WorkerRuntime, health: HealthResponse) {
let mut state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
*state = SocketLifecycleState::Ready {
runtime: Box::new(runtime),
health,
};
}
fn set_failed(&self, health: HealthResponse) {
let mut state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
*state = SocketLifecycleState::Failed(health);
}
fn shutdown(&self) {
let mut state = self
.state
.lock()
.unwrap_or_else(|poisoned| poisoned.into_inner());
let mut next_health = match &*state {
SocketLifecycleState::Initializing(health)
| SocketLifecycleState::Failed(health)
| SocketLifecycleState::Ready { health, .. } => health.clone(),
};
next_health.state = WorkerState::Failed;
next_health.phase = "failed".to_string();
next_health.error = Some("worker shutting down".to_string());
let previous = std::mem::replace(&mut *state, SocketLifecycleState::Failed(next_health));
drop(previous);
}
}
#[cfg(unix)]
fn write_worker_status(path: &std::path::Path, status: &str) -> io::Result<()> {
let next = path.with_extension("status.next");
std::fs::write(&next, format!("{}\n", status))?;
std::fs::rename(next, path)
}
#[cfg(unix)]
fn write_worker_pid(path: &std::path::Path, pid: u32) -> io::Result<()> {
let next = path.with_extension("pid.next");
std::fs::write(&next, format!("{}\n", pid))?;
std::fs::rename(next, path)
}
#[cfg(target_os = "linux")]
fn write_worker_start_time(path: &std::path::Path, pid: u32) -> io::Result<()> {
let stat = std::fs::read_to_string(format!("/proc/{pid}/stat"))?;
let start_time = stat
.rsplit_once(") ")
.and_then(|(_, fields)| fields.split_whitespace().nth(19))
.ok_or_else(|| io::Error::other("missing process start time"))?;
let next = path.with_extension("start.next");
std::fs::write(&next, format!("{start_time}\n"))?;
std::fs::rename(next, path)
}
#[cfg(unix)]
fn handle_socket_client(lifecycle: Arc<SocketLifecycle>, stream: std::os::unix::net::UnixStream) {
let Some(runtime) = lifecycle.runtime() else {
return handle_not_ready_client(lifecycle, stream);
};
let reader = match stream.try_clone() {
Ok(reader) => reader,
Err(e) => {
tracing::warn!(error = %e, "failed to clone embedding client socket");
return;
}
};
if let Err(e) = runtime.run_loop(reader, stream) {
if e.downcast_ref::<io::Error>().is_some_and(|io_err| {
matches!(
io_err.kind(),
io::ErrorKind::BrokenPipe
| io::ErrorKind::ConnectionReset
| io::ErrorKind::UnexpectedEof
)
}) {
tracing::debug!("socket client disconnected before response was written");
} else {
tracing::warn!(error = %e, "embedding socket client failed");
}
}
}
#[cfg(unix)]
fn handle_not_ready_client(
lifecycle: Arc<SocketLifecycle>,
mut stream: std::os::unix::net::UnixStream,
) {
let mut len_buf = [0u8; 4];
if stream.read_exact(&mut len_buf).is_err() {
return;
}
let payload_len = u32::from_le_bytes(len_buf) as usize;
let max_frame = lifecycle.max_frame().saturating_mul(2);
if payload_len > max_frame {
return;
}
let mut payload = vec![0u8; payload_len];
if stream.read_exact(&mut payload).is_err() {
return;
}
let Ok(frame) = Frame::from_wire_bytes(&payload) else {
return;
};
let response = if frame.header.msg_type == MsgType::HealthRequest {
protocol::health_response_frame(frame.header.batch_id, lifecycle.health())
} else {
let health = lifecycle.health();
let kind = if health.state == WorkerState::Initializing {
ErrorKind::Initializing
} else {
ErrorKind::OnnxRuntime
};
protocol::error_frame(
frame.header.batch_id,
WorkerError {
kind,
message: health
.error
.unwrap_or_else(|| format!("worker is {}", health.phase)),
},
)
};
let Ok(response) = response.and_then(|response| response.encode_wire()) else {
return;
};
let _ = stream.write_all(&response);
let _ = stream.flush();
}
#[cfg(unix)]
fn unix_now_ms() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|duration| duration.as_millis().min(u64::MAX as u128) as u64)
.unwrap_or(0)
}
#[cfg(unix)]
fn accept_retry_delay(error: &io::Error) -> Option<Duration> {
match error.kind() {
io::ErrorKind::WouldBlock => Some(Duration::from_millis(100)),
io::ErrorKind::Interrupted => Some(Duration::ZERO),
io::ErrorKind::ConnectionAborted => Some(Duration::from_millis(10)),
_ if matches!(error.raw_os_error(), Some(libc::EMFILE | libc::ENFILE)) => {
Some(Duration::from_millis(250))
}
_ => None,
}
}
#[cfg(not(unix))]
fn run_socket_worker(_config: RuntimeConfig, _socket_path: PathBuf) -> anyhow::Result<()> {
anyhow::bail!("socket worker mode is only supported on Unix")
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
use crate::embed::runtime::DEFAULT_MAX_FRAME_SIZE;
#[test]
fn socket_argument_requires_a_path() {
assert_eq!(parse_socket_arg(&["worker".into()]), Ok(None));
assert_eq!(
parse_socket_arg(&["worker".into(), "--socket".into()]),
Err("--socket requires a path")
);
assert_eq!(
parse_socket_arg(&["worker".into(), "--socket".into(), "run.sock".into()]),
Ok(Some(PathBuf::from("run.sock")))
);
}
#[test]
fn accept_retry_delay_covers_transient_errors() {
assert_eq!(
accept_retry_delay(&io::Error::from(io::ErrorKind::WouldBlock)),
Some(Duration::from_millis(100))
);
assert_eq!(
accept_retry_delay(&io::Error::from(io::ErrorKind::Interrupted)),
Some(Duration::ZERO)
);
assert_eq!(
accept_retry_delay(&io::Error::from_raw_os_error(libc::EMFILE)),
Some(Duration::from_millis(250))
);
assert!(accept_retry_delay(&io::Error::from(io::ErrorKind::InvalidInput)).is_none());
}
#[test]
fn initializing_socket_answers_health_and_rejects_inference() {
use std::os::unix::net::UnixStream;
let lifecycle = Arc::new(SocketLifecycle::new(
HealthResponse {
state: WorkerState::Initializing,
phase: "initializing".to_string(),
started_unix_ms: 1,
provider: Some("cpu".to_string()),
model: "test-model".to_string(),
error: None,
},
DEFAULT_MAX_FRAME_SIZE,
));
let (mut client, server) = UnixStream::pair().unwrap();
let server_lifecycle = Arc::clone(&lifecycle);
let worker = std::thread::spawn(move || handle_not_ready_client(server_lifecycle, server));
let health_batch = protocol::BatchId::new(1);
client
.write_all(
&protocol::health_request_frame(health_batch)
.unwrap()
.encode_wire()
.unwrap(),
)
.unwrap();
let mut len = [0u8; 4];
client.read_exact(&mut len).unwrap();
let mut payload = vec![0; u32::from_le_bytes(len) as usize];
client.read_exact(&mut payload).unwrap();
let response = Frame::from_wire_bytes(&payload).unwrap();
let decoded: protocol::Response = response.decode_payload().unwrap();
match decoded {
protocol::Response::Health(health) => {
assert_eq!(health.state, WorkerState::Initializing)
}
_ => panic!("expected health response"),
}
worker.join().unwrap();
let (mut client, server) = UnixStream::pair().unwrap();
let server_lifecycle = Arc::clone(&lifecycle);
let worker = std::thread::spawn(move || handle_not_ready_client(server_lifecycle, server));
let embed = protocol::embed_request_frame(
protocol::BatchId::new(2),
protocol::EmbedRequest {
texts: vec!["test".to_string()],
expected_dim: 4,
},
)
.unwrap();
client.write_all(&embed.encode_wire().unwrap()).unwrap();
client.read_exact(&mut len).unwrap();
let mut payload = vec![0; u32::from_le_bytes(len) as usize];
client.read_exact(&mut payload).unwrap();
let response = Frame::from_wire_bytes(&payload).unwrap();
let decoded: protocol::Response = response.decode_payload().unwrap();
match decoded {
protocol::Response::Error(error) => assert_eq!(error.kind, ErrorKind::Initializing),
_ => panic!("expected initializing error"),
}
worker.join().unwrap();
}
}
#[cfg(test)]
mod worker_entry_tests {
use crate::embed::protocol::{self, BatchId, EmbedRequest, Frame, MsgType};
use crate::embed::runtime::{DEFAULT_IDLE_TIMEOUT_SECS, RuntimeConfig, WorkerRuntime};
use std::io::Cursor;
use std::time::Duration;
#[test]
fn test_binary_embed_roundtrip_via_runtime() {
let request = EmbedRequest {
texts: vec!["hello".to_string(), "world".to_string()],
expected_dim: 4,
};
let frame = protocol::embed_request_frame(BatchId::new(1), request).unwrap();
let wire = frame.encode_wire().unwrap();
let decoded = Frame::from_wire_bytes(&wire[4..]).unwrap();
assert_eq!(decoded.header.batch_id, BatchId::new(1));
assert_eq!(decoded.header.msg_type, MsgType::EmbedRequest);
}
#[test]
fn test_runtime_handles_embed_request() {
let config = RuntimeConfig::default();
let rt = WorkerRuntime::new(config);
let request = EmbedRequest {
texts: vec!["test".to_string()],
expected_dim: 8,
};
let frame = protocol::embed_request_frame(BatchId::new(42), request).unwrap();
let response_frame = rt.dispatch(&frame);
assert_eq!(response_frame.header.batch_id, BatchId::new(42));
assert_eq!(response_frame.header.msg_type, MsgType::Error);
}
#[test]
fn test_run_loop_single_request() {
let config = RuntimeConfig {
idle_timeout: Duration::from_secs(DEFAULT_IDLE_TIMEOUT_SECS),
..RuntimeConfig::default()
};
let rt = WorkerRuntime::new(config);
let request = EmbedRequest {
texts: vec!["hello".to_string()],
expected_dim: 4,
};
let frame = protocol::embed_request_frame(BatchId::new(1), request).unwrap();
let wire = frame.encode_wire().unwrap();
let reader = Cursor::new(wire);
let writer = Cursor::new(Vec::<u8>::new());
let result = rt.run_loop(reader, writer);
assert!(result.is_ok());
}
}