1use std::path::PathBuf;
8use std::time::Duration;
9
10use serde::{Deserialize, Serialize};
11
12use crate::types::NodeId;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct NodeConfig {
17 pub node_id: NodeId,
19
20 pub storage_uri: String,
22
23 pub cache_dir: PathBuf,
25
26 pub cores: usize,
28
29 pub memory_bytes: u64,
31
32 pub memory_per_bee: u64,
34
35 pub target_cell_size: u64,
37
38 pub max_cell_size: u64,
40
41 pub min_cell_size: u64,
43
44 pub max_cache_size: u64,
46
47 pub heartbeat_interval: Duration,
49
50 pub dead_threshold: Duration,
52}
53
54const MIN_CELL_SIZE_FLOOR: u64 = 16 * 1024 * 1024;
56
57const DEFAULT_MAX_CACHE_SIZE: u64 = 2 * 1024 * 1024 * 1024;
59
60const DEFAULT_HEARTBEAT_INTERVAL: Duration = Duration::from_secs(5);
62
63const DEFAULT_DEAD_THRESHOLD: Duration = Duration::from_secs(30);
65
66impl NodeConfig {
67 pub fn detect(storage_uri: impl Into<String>) -> Self {
84 let storage_uri = storage_uri.into();
85 let node_id = NodeId::generate();
86
87 let cores = std::thread::available_parallelism()
88 .map(|p| p.get())
89 .unwrap_or(1);
90
91 let memory_bytes = detect_memory();
93
94 let memory_per_bee = memory_bytes / cores as u64;
95 let target_cell_size = memory_per_bee / 4;
96 let max_cell_size = target_cell_size * 2;
97 let min_cell_size = MIN_CELL_SIZE_FLOOR;
98
99 let cache_dir = dirs_cache_dir().join("apiary").join("cache");
100
101 Self {
102 node_id,
103 storage_uri,
104 cache_dir,
105 cores,
106 memory_bytes,
107 memory_per_bee,
108 target_cell_size,
109 max_cell_size,
110 min_cell_size,
111 max_cache_size: DEFAULT_MAX_CACHE_SIZE,
112 heartbeat_interval: DEFAULT_HEARTBEAT_INTERVAL,
113 dead_threshold: DEFAULT_DEAD_THRESHOLD,
114 }
115 }
116}
117
118fn detect_memory() -> u64 {
121 #[cfg(target_os = "linux")]
123 {
124 if let Ok(contents) = std::fs::read_to_string("/proc/meminfo") {
125 for line in contents.lines() {
126 if let Some(rest) = line.strip_prefix("MemTotal:") {
127 let rest = rest.trim();
128 if let Some(kb_str) = rest.strip_suffix("kB") {
129 if let Ok(kb) = kb_str.trim().parse::<u64>() {
130 return kb * 1024;
131 }
132 }
133 }
134 }
135 }
136 }
137
138 #[cfg(target_os = "windows")]
139 {
140 use std::mem;
141
142 #[repr(C)]
143 #[allow(non_snake_case)]
144 struct MEMORYSTATUSEX {
145 dwLength: u32,
146 dwMemoryLoad: u32,
147 ullTotalPhys: u64,
148 ullAvailPhys: u64,
149 ullTotalPageFile: u64,
150 ullAvailPageFile: u64,
151 ullTotalVirtual: u64,
152 ullAvailVirtual: u64,
153 ullAvailExtendedVirtual: u64,
154 }
155
156 extern "system" {
157 fn GlobalMemoryStatusEx(lpBuffer: *mut MEMORYSTATUSEX) -> i32;
158 }
159
160 unsafe {
161 let mut status: MEMORYSTATUSEX = mem::zeroed();
162 status.dwLength = mem::size_of::<MEMORYSTATUSEX>() as u32;
163 if GlobalMemoryStatusEx(&mut status) != 0 {
164 return status.ullTotalPhys;
165 }
166 }
167 }
168
169 #[cfg(target_os = "macos")]
170 {
171 use std::mem;
172
173 extern "C" {
174 fn sysctl(
175 name: *const i32,
176 namelen: u32,
177 oldp: *mut std::ffi::c_void,
178 oldlenp: *mut usize,
179 newp: *const std::ffi::c_void,
180 newlen: usize,
181 ) -> i32;
182 }
183
184 unsafe {
185 let mib: [i32; 2] = [6, 24];
187 let mut memsize: u64 = 0;
188 let mut len = mem::size_of::<u64>();
189 if sysctl(
190 mib.as_ptr(),
191 2,
192 &mut memsize as *mut u64 as *mut std::ffi::c_void,
193 &mut len,
194 std::ptr::null(),
195 0,
196 ) == 0
197 {
198 return memsize;
199 }
200 }
201 }
202
203 1024 * 1024 * 1024
205}
206
207fn dirs_cache_dir() -> PathBuf {
210 home_dir().unwrap_or_else(|| PathBuf::from("."))
211}
212
213fn home_dir() -> Option<PathBuf> {
215 #[cfg(target_os = "windows")]
216 {
217 std::env::var("USERPROFILE").ok().map(PathBuf::from)
218 }
219 #[cfg(not(target_os = "windows"))]
220 {
221 std::env::var("HOME").ok().map(PathBuf::from)
222 }
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228
229 #[test]
230 fn test_detect_creates_valid_config() {
231 let config = NodeConfig::detect("local://test");
232 assert!(config.cores > 0, "cores must be > 0");
233 assert!(config.memory_bytes > 0, "memory must be > 0");
234 assert_eq!(
235 config.memory_per_bee,
236 config.memory_bytes / config.cores as u64
237 );
238 assert_eq!(config.target_cell_size, config.memory_per_bee / 4);
239 assert_eq!(config.max_cell_size, config.target_cell_size * 2);
240 assert_eq!(config.min_cell_size, MIN_CELL_SIZE_FLOOR);
241 assert_eq!(config.heartbeat_interval, DEFAULT_HEARTBEAT_INTERVAL);
242 assert_eq!(config.dead_threshold, DEFAULT_DEAD_THRESHOLD);
243 }
244
245 #[test]
246 fn test_detect_unique_node_ids() {
247 let c1 = NodeConfig::detect("local://a");
248 let c2 = NodeConfig::detect("local://b");
249 assert_ne!(c1.node_id, c2.node_id);
250 }
251
252 #[test]
253 fn test_config_serialization() {
254 let config = NodeConfig::detect("s3://bucket/prefix");
255 let json = serde_json::to_string(&config).unwrap();
256 let deserialized: NodeConfig = serde_json::from_str(&json).unwrap();
257 assert_eq!(deserialized.storage_uri, config.storage_uri);
258 assert_eq!(deserialized.cores, config.cores);
259 }
260}