#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
pub mod protocol;
pub mod server;
pub mod store;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::watch;
pub use protocol::{CachedArtifact, DaemonRequest, DaemonResponse, WarmStatus};
pub use server::NodeCacheDaemon;
pub use store::{LocalCacheStore, MockLocalCacheStore, RealLocalCacheStore};
#[derive(Debug, thiserror::Error)]
pub enum DaemonError {
#[error("io: {0}")]
Io(#[from] std::io::Error),
#[error("protocol: {0}")]
Protocol(String),
#[error("remote cache backend: {0}")]
Remote(#[from] sui_cache::CacheError),
}
pub fn bind_unix_listener(socket_path: &Path) -> Result<UnixListener, DaemonError> {
if socket_path.exists() {
std::fs::remove_file(socket_path)?;
}
if let Some(parent) = socket_path.parent() {
std::fs::create_dir_all(parent)?;
}
Ok(UnixListener::bind(socket_path)?)
}
pub async fn serve<L: LocalCacheStore + 'static>(
listener: UnixListener,
daemon: Arc<NodeCacheDaemon<L>>,
mut shutdown: watch::Receiver<bool>,
) {
loop {
tokio::select! {
accepted = listener.accept() => {
match accepted {
Ok((stream, _addr)) => {
let daemon = Arc::clone(&daemon);
tokio::spawn(async move {
if let Err(e) = handle_connection(stream, &daemon).await {
tracing::warn!(
target: "sui-dockerfile-node-cache-daemon",
error = %e,
"connection handling failed"
);
}
});
}
Err(e) => {
tracing::warn!(target: "sui-dockerfile-node-cache-daemon", error = %e, "accept failed");
}
}
}
_ = shutdown.changed() => {
if *shutdown.borrow() {
tracing::info!(target: "sui-dockerfile-node-cache-daemon", "graceful shutdown requested");
break;
}
}
}
}
}
async fn handle_connection<L: LocalCacheStore + 'static>(
mut stream: UnixStream,
daemon: &NodeCacheDaemon<L>,
) -> Result<(), DaemonError> {
let request: DaemonRequest = protocol::read_message(&mut stream).await?;
let response = daemon.handle_request(request).await;
protocol::write_message(&mut stream, &response).await
}
#[must_use]
pub fn default_socket_path() -> PathBuf {
PathBuf::from("/var/run/sui-dockerfile-cache/node-cache.sock")
}
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use sui_cache::storage::StorageBackend;
use sui_cache::CacheError;
use tokio::net::UnixStream as ClientStream;
struct EmptyRemote;
#[async_trait]
impl StorageBackend for EmptyRemote {
async fn get_narinfo(&self, _hash: &str) -> Result<Option<String>, CacheError> {
Ok(None)
}
async fn put_narinfo(&self, _hash: &str, _content: &str) -> Result<(), CacheError> {
Ok(())
}
async fn get_nar(&self, _path: &str) -> Result<Option<Vec<u8>>, CacheError> {
Ok(None)
}
async fn put_nar(&self, _path: &str, _data: &[u8]) -> Result<(), CacheError> {
Ok(())
}
async fn delete(&self, _hash: &str) -> Result<(), CacheError> {
Ok(())
}
async fn list_narinfos(&self) -> Result<Vec<String>, CacheError> {
Ok(Vec::new())
}
}
#[tokio::test]
async fn end_to_end_over_a_real_unix_socket() {
let dir = tempfile::tempdir().unwrap();
let socket_path = dir.path().join("test.sock");
let local = Arc::new(
MockLocalCacheStore::new().with_entry("hit", CachedArtifact { image_ref: "img:e2e".to_string() }),
);
let remote: Arc<dyn StorageBackend> = Arc::new(EmptyRemote);
let daemon = Arc::new(NodeCacheDaemon::new(local, remote));
let listener = bind_unix_listener(&socket_path).unwrap();
let (shutdown_tx, shutdown_rx) = watch::channel(false);
let server_task = tokio::spawn(serve(listener, daemon, shutdown_rx));
let mut client = ClientStream::connect(&socket_path).await.unwrap();
protocol::write_message(&mut client, &DaemonRequest::Get { content_hash: "hit".to_string() })
.await
.unwrap();
let resp: DaemonResponse = protocol::read_message(&mut client).await.unwrap();
match resp {
DaemonResponse::Get { artifact: Some(a) } => assert_eq!(a.image_ref, "img:e2e"),
other => panic!("expected Get hit, got {other:?}"),
}
shutdown_tx.send(true).unwrap();
server_task.await.unwrap();
}
#[test]
fn default_socket_path_matches_the_helm_chart_mount_contract() {
assert_eq!(default_socket_path(), PathBuf::from("/var/run/sui-dockerfile-cache/node-cache.sock"));
}
}