sui_dockerfile_node_cache_daemon/
lib.rs1#![warn(clippy::pedantic)]
37#![allow(clippy::module_name_repetitions)]
38
39pub mod protocol;
40pub mod server;
41pub mod store;
42
43use std::path::{Path, PathBuf};
44use std::sync::Arc;
45
46use tokio::net::{UnixListener, UnixStream};
47use tokio::sync::watch;
48
49pub use protocol::{CachedArtifact, DaemonRequest, DaemonResponse, WarmStatus};
50pub use server::NodeCacheDaemon;
51pub use store::{LocalCacheStore, MockLocalCacheStore, RealLocalCacheStore};
52
53#[derive(Debug, thiserror::Error)]
56pub enum DaemonError {
57 #[error("io: {0}")]
58 Io(#[from] std::io::Error),
59 #[error("protocol: {0}")]
60 Protocol(String),
61 #[error("remote cache backend: {0}")]
62 Remote(#[from] sui_cache::CacheError),
63}
64
65pub fn bind_unix_listener(socket_path: &Path) -> Result<UnixListener, DaemonError> {
73 if socket_path.exists() {
74 std::fs::remove_file(socket_path)?;
75 }
76 if let Some(parent) = socket_path.parent() {
77 std::fs::create_dir_all(parent)?;
78 }
79 Ok(UnixListener::bind(socket_path)?)
80}
81
82pub async fn serve<L: LocalCacheStore + 'static>(
95 listener: UnixListener,
96 daemon: Arc<NodeCacheDaemon<L>>,
97 mut shutdown: watch::Receiver<bool>,
98) {
99 loop {
100 tokio::select! {
101 accepted = listener.accept() => {
102 match accepted {
103 Ok((stream, _addr)) => {
104 let daemon = Arc::clone(&daemon);
105 tokio::spawn(async move {
106 if let Err(e) = handle_connection(stream, &daemon).await {
107 tracing::warn!(
108 target: "sui-dockerfile-node-cache-daemon",
109 error = %e,
110 "connection handling failed"
111 );
112 }
113 });
114 }
115 Err(e) => {
116 tracing::warn!(target: "sui-dockerfile-node-cache-daemon", error = %e, "accept failed");
117 }
118 }
119 }
120 _ = shutdown.changed() => {
121 if *shutdown.borrow() {
122 tracing::info!(target: "sui-dockerfile-node-cache-daemon", "graceful shutdown requested");
123 break;
124 }
125 }
126 }
127 }
128}
129
130async fn handle_connection<L: LocalCacheStore + 'static>(
131 mut stream: UnixStream,
132 daemon: &NodeCacheDaemon<L>,
133) -> Result<(), DaemonError> {
134 let request: DaemonRequest = protocol::read_message(&mut stream).await?;
135 let response = daemon.handle_request(request).await;
136 protocol::write_message(&mut stream, &response).await
137}
138
139#[must_use]
145pub fn default_socket_path() -> PathBuf {
146 PathBuf::from("/var/run/sui-dockerfile-cache/node-cache.sock")
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use async_trait::async_trait;
153 use sui_cache::storage::StorageBackend;
154 use sui_cache::CacheError;
155 use tokio::net::UnixStream as ClientStream;
156
157 struct EmptyRemote;
158 #[async_trait]
159 impl StorageBackend for EmptyRemote {
160 async fn get_narinfo(&self, _hash: &str) -> Result<Option<String>, CacheError> {
161 Ok(None)
162 }
163 async fn put_narinfo(&self, _hash: &str, _content: &str) -> Result<(), CacheError> {
164 Ok(())
165 }
166 async fn get_nar(&self, _path: &str) -> Result<Option<Vec<u8>>, CacheError> {
167 Ok(None)
168 }
169 async fn put_nar(&self, _path: &str, _data: &[u8]) -> Result<(), CacheError> {
170 Ok(())
171 }
172 async fn delete(&self, _hash: &str) -> Result<(), CacheError> {
173 Ok(())
174 }
175 async fn list_narinfos(&self) -> Result<Vec<String>, CacheError> {
176 Ok(Vec::new())
177 }
178 }
179
180 #[tokio::test]
181 async fn end_to_end_over_a_real_unix_socket() {
182 let dir = tempfile::tempdir().unwrap();
183 let socket_path = dir.path().join("test.sock");
184
185 let local = Arc::new(
186 MockLocalCacheStore::new().with_entry("hit", CachedArtifact { image_ref: "img:e2e".to_string() }),
187 );
188 let remote: Arc<dyn StorageBackend> = Arc::new(EmptyRemote);
189 let daemon = Arc::new(NodeCacheDaemon::new(local, remote));
190
191 let listener = bind_unix_listener(&socket_path).unwrap();
192 let (shutdown_tx, shutdown_rx) = watch::channel(false);
193 let server_task = tokio::spawn(serve(listener, daemon, shutdown_rx));
194
195 let mut client = ClientStream::connect(&socket_path).await.unwrap();
196 protocol::write_message(&mut client, &DaemonRequest::Get { content_hash: "hit".to_string() })
197 .await
198 .unwrap();
199 let resp: DaemonResponse = protocol::read_message(&mut client).await.unwrap();
200 match resp {
201 DaemonResponse::Get { artifact: Some(a) } => assert_eq!(a.image_ref, "img:e2e"),
202 other => panic!("expected Get hit, got {other:?}"),
203 }
204
205 shutdown_tx.send(true).unwrap();
206 server_task.await.unwrap();
207 }
208
209 #[test]
210 fn default_socket_path_matches_the_helm_chart_mount_contract() {
211 assert_eq!(default_socket_path(), PathBuf::from("/var/run/sui-dockerfile-cache/node-cache.sock"));
212 }
213}