1use std::sync::Arc;
5
6use serde::{Deserialize, Serialize};
7#[cfg(unix)]
8use tokio::net::UnixStream;
9
10use super::spawn_named_tracked_task;
11#[cfg(unix)]
12use super::{write_frame, DaemonResponseFrame, PROTOCOL_VERSION};
13
14#[cfg(unix)]
19const MAX_CONNECTIONS_ENV: &str = "KHIVE_DAEMON_MAX_CONNECTIONS";
20
21#[cfg(unix)]
30const DEFAULT_MAX_CONNECTIONS: usize = 512;
31
32#[cfg(unix)]
43const RESERVED_DESCRIPTORS: u64 = 192;
44
45#[cfg(unix)]
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub(super) enum CapSource {
49 Builtin,
51 Configured,
53 DescriptorLimit,
55}
56
57#[cfg(unix)]
61const BUSY_REFUSAL_WRITE_TIMEOUT: std::time::Duration = std::time::Duration::from_millis(250);
62
63pub(super) fn parse_positive_limit(raw: Option<&str>) -> Option<u64> {
66 raw.and_then(|value| value.trim().parse::<u64>().ok())
67 .filter(|value| *value > 0)
68}
69
70fn positive_limit_from_env(name: &str) -> Option<u64> {
74 let raw = std::env::var(name).ok()?;
75 let parsed = parse_positive_limit(Some(&raw));
76 if parsed.is_none() {
77 tracing::warn!(
78 variable = name,
79 value = %raw,
80 "ignoring a limit that is not a positive integer; the default applies"
81 );
82 }
83 parsed
84}
85
86#[cfg(unix)]
88fn positive_usize_from_env(name: &str) -> Option<usize> {
89 let value = positive_limit_from_env(name)?;
90 Some(usize::try_from(value).unwrap_or(usize::MAX))
91}
92
93#[cfg(unix)]
95pub(super) fn soft_nofile_limit() -> Option<u64> {
96 let mut limit = libc::rlimit {
97 rlim_cur: 0,
98 rlim_max: 0,
99 };
100 if unsafe { libc::getrlimit(libc::RLIMIT_NOFILE, &mut limit) } != 0 {
102 return None;
103 }
104 #[allow(clippy::unnecessary_cast)]
107 let soft = limit.rlim_cur as u64;
108 Some(soft)
109}
110
111#[cfg(unix)]
117pub(super) fn effective_connection_cap(
118 configured: Option<usize>,
119 soft_nofile: Option<u64>,
120) -> (usize, CapSource) {
121 let (wanted, source) = match configured {
122 Some(value) => (value, CapSource::Configured),
123 None => (DEFAULT_MAX_CONNECTIONS, CapSource::Builtin),
124 };
125 let room = match soft_nofile {
126 Some(soft) => soft.saturating_sub(RESERVED_DESCRIPTORS),
127 None => u64::MAX,
128 };
129 let room = usize::try_from(room).unwrap_or(usize::MAX);
130 let reduced = room < wanted;
131 let cap = if reduced { room } else { wanted };
132 let source = if reduced {
133 CapSource::DescriptorLimit
134 } else {
135 source
136 };
137 (cap.max(1), source)
138}
139
140#[cfg(unix)]
145pub(super) struct ConnectionAdmission {
146 limit: usize,
148 configured: Option<usize>,
151 permits: Arc<tokio::sync::Semaphore>,
152 refused: std::sync::atomic::AtomicU64,
153}
154
155#[cfg(unix)]
156impl ConnectionAdmission {
157 pub(super) fn new(limit: usize) -> Self {
158 let limit = limit.clamp(1, tokio::sync::Semaphore::MAX_PERMITS);
159 Self {
160 limit,
161 configured: None,
162 permits: Arc::new(tokio::sync::Semaphore::new(limit)),
163 refused: std::sync::atomic::AtomicU64::new(0),
164 }
165 }
166
167 pub(super) fn from_limits(configured: Option<usize>, soft_nofile: Option<u64>) -> Self {
171 let (limit, source) = effective_connection_cap(configured, soft_nofile);
172 let mut admission = Self::new(limit);
173 match soft_nofile {
174 None => tracing::warn!(
175 cap = admission.limit,
176 "could not read the descriptor limit; the connection cap is not reduced to fit it"
177 ),
178 Some(soft) if source == CapSource::DescriptorLimit => {
179 let wanted = configured.unwrap_or(DEFAULT_MAX_CONNECTIONS);
180 admission.configured = Some(wanted);
181 tracing::warn!(
182 configured_cap = wanted,
183 effective_cap = admission.limit,
184 soft_nofile = soft,
185 reserved_descriptors = RESERVED_DESCRIPTORS,
186 "connection cap reduced to fit the process descriptor limit"
187 );
188 }
189 Some(_) => {}
190 }
191 admission
192 }
193
194 pub(super) fn from_env() -> Self {
195 Self::from_limits(
196 positive_usize_from_env(MAX_CONNECTIONS_ENV),
197 soft_nofile_limit(),
198 )
199 }
200
201 pub(super) fn try_admit(&self) -> Result<tokio::sync::OwnedSemaphorePermit, u64> {
204 Arc::clone(&self.permits).try_acquire_owned().map_err(|_| {
205 self.refused
206 .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
207 + 1
208 })
209 }
210
211 pub(super) fn snapshot(&self) -> ConnectionCapSnapshot {
212 ConnectionCapSnapshot {
213 limit: self.limit,
214 configured: self.configured,
215 active: self.limit - self.permits.available_permits(),
216 refused: self.refused.load(std::sync::atomic::Ordering::SeqCst),
217 }
218 }
219}
220
221#[cfg(unix)]
232pub(super) async fn admit_or_refuse_busy(
233 admission: &ConnectionAdmission,
234 stream: &mut UnixStream,
235 config_id: &str,
236) -> Option<tokio::sync::OwnedSemaphorePermit> {
237 let refused_total = match admission.try_admit() {
238 Ok(permit) => return Some(permit),
239 Err(refused_total) => refused_total,
240 };
241 if refused_total.is_power_of_two() {
242 tracing::warn!(
243 cap = admission.limit,
244 refused_total,
245 "daemon at connection cap; refusing new connections"
246 );
247 }
248 let limit = admission.limit;
249 let refusal = DaemonResponseFrame {
250 ok: false,
251 result: None,
252 error: Some(format!(
253 "daemon is at its connection limit ({limit}); request was not admitted, retry shortly"
254 )),
255 error_detail: Some(serde_json::json!({
256 "kind": "runtime", "code": "daemon_busy", "limit": limit,
257 "domain_disposition": crate::DomainDisposition::NotCommitted.as_str(),
258 })),
259 namespace_mismatch: false,
260 config_mismatch: false,
261 served_config_id: Some(config_id.to_owned()),
262 version_mismatch: false,
263 daemon_protocol_version: PROTOCOL_VERSION,
264 metrics: None,
265 request_id: None,
266 };
267 if let Ok(payload) = serde_json::to_vec(&refusal) {
268 let write = write_frame(stream, &payload);
269 match tokio::time::timeout(BUSY_REFUSAL_WRITE_TIMEOUT, write).await {
270 Ok(Ok(())) => {}
271 Ok(Err(error)) => tracing::debug!(%error, "failed to write connection-limit refusal"),
272 Err(_) => tracing::debug!("connection-limit refusal write timed out"),
273 }
274 }
275 None
276}
277
278#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
282pub struct ConnectionCapSnapshot {
283 pub limit: usize,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
288 pub configured: Option<usize>,
289 pub active: usize,
290 pub refused: u64,
291}
292
293const RECALL_LEDGER_MAX_PENDING_ENV: &str = "KHIVE_RECALL_LEDGER_MAX_PENDING";
299
300const RECALL_LEDGER_TIMEOUT_MS_ENV: &str = "KHIVE_RECALL_LEDGER_TIMEOUT_MS";
304
305const DEFAULT_RECALL_LEDGER_MAX_PENDING: usize = 256;
309
310const DEFAULT_RECALL_LEDGER_TIMEOUT_MS: u64 = 30_000;
313
314pub(super) struct RecallLedgerBound {
321 max_pending: usize,
322 timeout: std::time::Duration,
323 permits: Arc<tokio::sync::Semaphore>,
324 skipped: std::sync::atomic::AtomicU64,
325 timed_out: std::sync::atomic::AtomicU64,
326}
327
328impl RecallLedgerBound {
329 pub(super) fn new(max_pending: usize, timeout: std::time::Duration) -> Self {
330 let max_pending = max_pending.clamp(1, tokio::sync::Semaphore::MAX_PERMITS);
331 Self {
332 max_pending,
333 timeout,
334 permits: Arc::new(tokio::sync::Semaphore::new(max_pending)),
335 skipped: std::sync::atomic::AtomicU64::new(0),
336 timed_out: std::sync::atomic::AtomicU64::new(0),
337 }
338 }
339
340 fn from_env() -> Self {
341 let max_pending = match positive_limit_from_env(RECALL_LEDGER_MAX_PENDING_ENV) {
342 Some(value) => usize::try_from(value).unwrap_or(usize::MAX),
343 None => DEFAULT_RECALL_LEDGER_MAX_PENDING,
344 };
345 let timeout_ms = positive_limit_from_env(RECALL_LEDGER_TIMEOUT_MS_ENV)
346 .unwrap_or(DEFAULT_RECALL_LEDGER_TIMEOUT_MS);
347 Self::new(max_pending, std::time::Duration::from_millis(timeout_ms))
348 }
349
350 pub(super) fn try_spawn<F>(self: &Arc<Self>, fut: F) -> Option<tokio::task::JoinHandle<()>>
354 where
355 F: std::future::Future<Output = ()> + Send + 'static,
356 {
357 let Ok(permit) = Arc::clone(&self.permits).try_acquire_owned() else {
358 self.skipped
359 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
360 return None;
361 };
362 let bound = Arc::clone(self);
363 let handle = spawn_named_tracked_task("memory_recall_serve_ledger", async move {
364 let _permit = permit;
366 if tokio::time::timeout(bound.timeout, fut).await.is_err() {
367 bound
368 .timed_out
369 .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
370 tracing::warn!(
371 timeout_ms = bound.timeout.as_millis() as u64,
372 "recall serve ledger task timed out; the ledger write was missed"
373 );
374 }
375 });
376 Some(handle)
377 }
378
379 pub(super) fn snapshot(&self) -> RecallLedgerSnapshot {
380 RecallLedgerSnapshot {
381 max_pending: self.max_pending,
382 pending: self.max_pending - self.permits.available_permits(),
383 timeout_ms: self.timeout.as_millis().min(u128::from(u64::MAX)) as u64,
384 skipped: self.skipped.load(std::sync::atomic::Ordering::SeqCst),
385 timed_out: self.timed_out.load(std::sync::atomic::Ordering::SeqCst),
386 }
387 }
388}
389
390fn recall_ledger() -> &'static Arc<RecallLedgerBound> {
391 static BOUND: std::sync::OnceLock<Arc<RecallLedgerBound>> = std::sync::OnceLock::new();
392 BOUND.get_or_init(|| Arc::new(RecallLedgerBound::from_env()))
393}
394
395pub fn track_recall_ledger_task<F>(fut: F)
399where
400 F: std::future::Future<Output = ()> + Send + 'static,
401{
402 drop(recall_ledger().try_spawn(fut));
403}
404
405pub fn recall_ledger_snapshot() -> RecallLedgerSnapshot {
407 recall_ledger().snapshot()
408}
409
410#[derive(Serialize, Deserialize, Debug, Clone, Default, PartialEq, Eq)]
415pub struct RecallLedgerSnapshot {
416 pub max_pending: usize,
417 pub pending: usize,
418 pub timeout_ms: u64,
419 pub skipped: u64,
420 pub timed_out: u64,
421}