1use crate::clone_backend::free_space;
4use crate::ctx::Ctx;
5use crate::error::{ErrorCode, MitosisError, Result};
6use crate::events;
7use crate::proc_util::{process_start_time, rss_bytes};
8use crate::state::CloneStatus;
9use serde::{Deserialize, Serialize};
10use std::sync::atomic::{AtomicUsize, Ordering};
11use std::sync::Arc;
12use std::time::{Duration, Instant};
13use tokio::sync::OwnedMutexGuard;
14
15const DEFAULT_RAM_ESTIMATE: u64 = 64 * 1024 * 1024;
17const RECHECK: Duration = Duration::from_secs(1);
19const MIN_LINE_WAIT: Duration = Duration::from_millis(500);
21
22#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
23pub struct CloneUsage {
24 pub id: String,
25 pub name: Option<String>,
26 pub template: String,
27 pub rss_bytes: Option<u64>,
28 pub own_disk_bytes: Option<u64>,
29 pub created_at: i64,
30 pub expires_at: i64,
31 pub protected: bool,
32 pub disk_warn: bool,
33 #[serde(default)]
35 pub status: String,
36}
37
38#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)]
39pub struct Usage {
40 pub clones: Vec<CloneUsage>,
41 pub ram_bytes: u64,
43 pub disk_bytes: u64,
44 pub free_disk_bytes: u64,
45 pub queued: usize,
47 pub max_clones: usize,
48 pub ram_budget: Option<u64>,
49 pub disk_budget: Option<u64>,
50}
51
52#[derive(Clone, Copy, Debug, PartialEq, Eq)]
53pub enum Limit {
54 Count,
55 Ram,
56 Disk,
57}
58
59impl Limit {
60 fn name(self) -> &'static str {
61 match self {
62 Limit::Count => "count",
63 Limit::Ram => "ram",
64 Limit::Disk => "disk",
65 }
66 }
67}
68
69pub fn fits(usage: &Usage, new_ram_estimate: u64) -> Option<Limit> {
71 if usage.clones.len() >= usage.max_clones {
72 return Some(Limit::Count);
73 }
74 if usage
75 .ram_budget
76 .is_some_and(|b| usage.ram_bytes.saturating_add(new_ram_estimate) > b)
77 {
78 return Some(Limit::Ram);
79 }
80 if usage.disk_budget.is_some_and(|b| usage.disk_bytes >= b) {
81 return Some(Limit::Disk);
82 }
83 None
84}
85
86pub fn ram_estimate(running_rss: &[u64]) -> u64 {
88 if running_rss.is_empty() {
89 return DEFAULT_RAM_ESTIMATE;
90 }
91 running_rss.iter().sum::<u64>() / running_rss.len() as u64
92}
93
94fn human(b: u64) -> String {
95 match b {
96 0..=999 => format!("{b} B"),
97 1_000..=999_999 => format!("{:.0} kB", b as f64 / 1e3),
98 1_000_000..=999_999_999 => format!("{:.0} MB", b as f64 / 1e6),
99 _ => format!("{:.1} GB", b as f64 / 1e9),
100 }
101}
102
103fn collect(ctx: &Ctx, want_ram: bool, want_disk: bool) -> Result<(Usage, u64)> {
105 let cfg = ctx.config();
106 let rows: Vec<_> = ctx
107 .store()
108 .list_clones()?
109 .into_iter()
110 .filter(|c| c.status != CloneStatus::Stopping)
111 .collect();
112 let busy: std::collections::HashSet<String> = ctx
115 .in_flight
116 .lock()
117 .unwrap_or_else(|e| e.into_inner())
118 .keys()
119 .cloned()
120 .collect();
121 let waking =
122 |r: &crate::state::CloneRow| r.status == CloneStatus::Sleeping && busy.contains(&r.id);
123 let mut clones = Vec::with_capacity(rows.len());
124 for r in &rows {
125 let rss = r
126 .pg_pid
127 .filter(|_| want_ram && (r.status == CloneStatus::Ready || waking(r)))
128 .filter(|pid| process_start_time(*pid) == r.pg_start_time)
129 .and_then(rss_bytes);
130 let own = want_disk
131 .then(|| ctx.backend.private_size(&r.dir))
132 .flatten();
133 clones.push(CloneUsage {
134 id: r.id.clone(),
135 name: r.name.clone(),
136 template: r.template.clone(),
137 rss_bytes: rss,
138 own_disk_bytes: own,
139 created_at: r.created_at,
140 expires_at: r.expires_at,
141 protected: r.protected,
142 disk_warn: own.is_some_and(|b| b > cfg.clone_disk_warn),
143 status: match r.status {
144 CloneStatus::Ready => "ready",
145 CloneStatus::Sleeping => "sleeping",
146 _ => "starting",
147 }
148 .into(),
149 });
150 }
151 let running: Vec<u64> = clones.iter().filter_map(|c| c.rss_bytes).collect();
152 let estimate = ram_estimate(&running);
153 let unread = if want_ram {
156 rows.iter()
157 .zip(&clones)
158 .filter(|(r, c)| {
159 (r.status != CloneStatus::Sleeping || waking(r)) && c.rss_bytes.is_none()
160 })
161 .count() as u64
162 } else {
163 0
164 };
165 let usage = Usage {
166 ram_bytes: running
167 .iter()
168 .sum::<u64>()
169 .saturating_add(estimate.saturating_mul(unread)),
170 disk_bytes: clones.iter().filter_map(|c| c.own_disk_bytes).sum(),
171 free_disk_bytes: free_space(&ctx.home.clones_dir()).unwrap_or(0),
172 queued: ctx.queued.load(Ordering::SeqCst),
173 max_clones: cfg.max_clones,
174 ram_budget: cfg.ram_budget,
175 disk_budget: cfg.disk_budget,
176 clones,
177 };
178 Ok((usage, estimate))
179}
180
181async fn snapshot(ctx: &Arc<Ctx>, full: bool) -> Result<(Usage, u64)> {
182 let c = ctx.clone();
183 let (ram, disk) = {
184 let cfg = ctx.config();
185 (
186 full || cfg.ram_budget.is_some(),
187 full || cfg.disk_budget.is_some(),
188 )
189 };
190 tokio::task::spawn_blocking(move || collect(&c, ram, disk))
191 .await
192 .map_err(|e| {
193 MitosisError::new(ErrorCode::Internal, format!("background task failed: {e}"))
194 })?
195}
196
197pub async fn usage(ctx: &Arc<Ctx>) -> Result<Usage> {
199 snapshot(ctx, true).await.map(|(u, _)| u)
200}
201
202pub(crate) async fn wake_blocked(ctx: &Arc<Ctx>) -> Result<Option<&'static str>> {
206 let (mut usage, estimate) = snapshot(ctx, false).await?;
207 usage.clones.retain(|c| c.status != "sleeping");
208 usage.disk_budget = None;
209 Ok(fits(&usage, estimate).map(|limit| match limit {
210 Limit::Count => "max_clones",
211 Limit::Ram => "ram_budget",
212 Limit::Disk => "disk_budget",
213 }))
214}
215
216pub(crate) fn cap_reached(max_clones: usize, labels: &[String]) -> MitosisError {
219 let mut shown = labels
220 .iter()
221 .take(3)
222 .cloned()
223 .collect::<Vec<_>>()
224 .join(", ");
225 if labels.len() > 3 {
226 shown.push_str(&format!(" and {} more", labels.len() - 3));
227 }
228 let of = if shown.is_empty() {
229 String::new()
230 } else {
231 format!(": {shown}")
232 };
233 MitosisError::new(
234 ErrorCode::CapReached,
235 format!("clone limit reached (max_clones = {max_clones}){of}"),
236 )
237 .with_hint(
238 "destroy one with `mitosis destroy <id-or-name>` (or the clone_destroy tool), or raise max_clones (mitosis config set max_clones <n>)",
239 )
240}
241
242fn exhausted(u: &Usage, limit: Limit, estimate: u64) -> MitosisError {
244 let largest = |size: &dyn Fn(&CloneUsage) -> u64| {
246 let mut v: Vec<&CloneUsage> = u.clones.iter().filter(|c| size(c) > 0).collect();
247 v.sort_by_key(|c| std::cmp::Reverse(size(c)));
248 let top = v
249 .iter()
250 .take(3)
251 .map(|c| {
252 let label = c.name.as_deref().unwrap_or(&c.id);
253 format!("{label} ({})", human(size(c)))
254 })
255 .collect::<Vec<_>>()
256 .join(", ");
257 if top.is_empty() {
258 top
259 } else {
260 format!("; largest: {top}")
261 }
262 };
263 match limit {
264 Limit::Count => {
265 let labels: Vec<String> = u
266 .clones
267 .iter()
268 .map(|c| c.name.clone().unwrap_or_else(|| c.id.clone()))
269 .collect();
270 cap_reached(u.max_clones, &labels)
271 }
272 Limit::Ram => MitosisError::new(
273 ErrorCode::BudgetFull,
274 format!(
275 "ram_budget of {} is full: clones use {} and a new one needs about {}{}",
276 human(u.ram_budget.unwrap_or(0)),
277 human(u.ram_bytes),
278 human(estimate),
279 largest(&|c| c.rss_bytes.unwrap_or(0)),
280 ),
281 )
282 .with_hint("destroy a clone, wait for one to expire, or raise it: mitosis config set ram_budget <size>"),
283 Limit::Disk => MitosisError::new(
284 ErrorCode::BudgetFull,
285 format!(
286 "disk_budget of {} is full: clones use {}{}",
287 human(u.disk_budget.unwrap_or(0)),
288 human(u.disk_bytes),
289 largest(&|c| c.own_disk_bytes.unwrap_or(0)),
290 ),
291 )
292 .with_hint("destroy a clone, or raise it: mitosis config set disk_budget <size>"),
293 }
294}
295
296struct InQueue<'a>(&'a AtomicUsize);
298
299impl<'a> InQueue<'a> {
300 fn new(n: &'a AtomicUsize) -> Self {
301 n.fetch_add(1, Ordering::SeqCst);
302 InQueue(n)
303 }
304}
305
306impl Drop for InQueue<'_> {
307 fn drop(&mut self) {
308 self.0.fetch_sub(1, Ordering::SeqCst);
309 }
310}
311
312pub struct Admission {
315 _turn: OwnedMutexGuard<()>,
316 pub waited: Duration,
317}
318
319tokio::task_local! {
320 pub(crate) static CLIENT_GONE: tokio::sync::watch::Receiver<bool>;
322 pub(crate) static DAEMON_STOPPING: tokio::sync::watch::Receiver<bool>;
324}
325
326async fn daemon_stopping() {
328 if let Ok(mut rx) = DAEMON_STOPPING.try_with(|r| r.clone()) {
329 if rx.wait_for(|stopping| *stopping).await.is_ok() {
330 return;
331 }
332 }
333 std::future::pending::<()>().await
334}
335
336async fn client_gone() {
338 if let Ok(mut rx) = CLIENT_GONE.try_with(|r| r.clone()) {
339 if rx.wait_for(|gone| *gone).await.is_ok() {
340 return;
341 }
342 }
343 std::future::pending::<()>().await
344}
345
346pub async fn admit(ctx: &Arc<Ctx>, wait: Duration) -> Result<Admission> {
351 tokio::select! {
352 r = admit_in_line(ctx, wait) => r,
353 () = client_gone() => Err(MitosisError::new(
354 ErrorCode::InvalidRequest,
355 "the client disconnected while the create was queued",
356 )),
357 () = daemon_stopping() => Err(MitosisError::new(
358 ErrorCode::DaemonUnavailable,
359 "the daemon is shutting down",
360 )
361 .with_hint("try again: the next command starts the daemon")),
362 }
363}
364
365async fn sleep_least_recent_idle(ctx: &Arc<Ctx>) -> bool {
369 if !crate::router::listening(ctx) {
370 return false;
371 }
372 let mut idle = match crate::lifecycle::idle_clones(ctx, crate::templates::now()).await {
373 Ok(idle) => idle,
374 Err(e) => {
375 crate::lifecycle::log(ctx, &format!("eviction: {e}"));
376 return false;
377 }
378 };
379 idle.retain(|(r, _)| r.status == CloneStatus::Ready);
380 idle.sort_by_key(|(_, since)| *since);
381 for (row, _) in idle {
382 match crate::clones::sleep(ctx, &row.id, events::reason::EVICTED).await {
383 Ok(()) => return true,
384 Err(e) => crate::lifecycle::log(ctx, &format!("eviction: clone {}: {e}", row.id)),
385 }
386 }
387 false
388}
389
390async fn admit_in_line(ctx: &Arc<Ctx>, wait: Duration) -> Result<Admission> {
391 let start = Instant::now();
392 let _counted = InQueue::new(&ctx.queued);
393 let turn =
395 tokio::time::timeout(wait.max(MIN_LINE_WAIT), ctx.admission.clone().lock_owned()).await;
396 let Ok(turn) = turn else {
397 let (u, est) = snapshot(ctx, false).await?;
398 let limit = fits(&u, est);
399 events::emit(
400 ctx,
401 events::QUEUE_TIMEOUT,
402 None,
403 None,
404 serde_json::json!({
405 "limit": limit.map(Limit::name),
406 "waited_ms": u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
407 "behind_other_creates": limit.is_none(),
408 }),
409 );
410 return Err(limit.map_or_else(
411 || {
412 MitosisError::new(
413 ErrorCode::CapReached,
414 "other creates are still ahead of this one in the queue",
415 )
416 .with_hint("try again, or raise the wait: mitosis clone --wait <duration>")
417 },
418 |l| exhausted(&u, l, est),
419 ));
420 };
421 let deadline = start + wait;
422 let mut waited_for_room = false;
423 loop {
424 let woken = ctx.room.notified();
426 tokio::pin!(woken);
427 woken.as_mut().enable();
428 let (u, est) = snapshot(ctx, false).await?;
429 let limit = fits(&u, est);
430 if limit == Some(Limit::Ram)
432 && u.disk_budget.is_none_or(|b| u.disk_bytes < b)
433 && sleep_least_recent_idle(ctx).await
434 {
435 continue;
436 }
437 let Some(limit) = limit else {
438 if waited_for_room {
439 events::emit(
440 ctx,
441 events::QUEUE_WAITED,
442 None,
443 None,
444 serde_json::json!({
445 "waited_ms": u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
446 }),
447 );
448 }
449 return Ok(Admission {
450 _turn: turn,
451 waited: start.elapsed(),
452 });
453 };
454 let left = deadline.saturating_duration_since(Instant::now());
455 if left.is_zero() {
456 events::emit(
457 ctx,
458 events::QUEUE_TIMEOUT,
459 None,
460 None,
461 serde_json::json!({
462 "limit": limit.name(),
463 "waited_ms": u64::try_from(start.elapsed().as_millis()).unwrap_or(u64::MAX),
464 }),
465 );
466 return Err(exhausted(&u, limit, est));
467 }
468 waited_for_room = true;
469 let _ = tokio::time::timeout(left.min(RECHECK), woken).await;
470 }
471}
472
473#[cfg(test)]
474mod tests {
475 use super::*;
476
477 fn c(id: &str, rss: Option<u64>, disk: Option<u64>) -> CloneUsage {
478 CloneUsage {
479 id: id.into(),
480 name: None,
481 template: "main".into(),
482 rss_bytes: rss,
483 own_disk_bytes: disk,
484 created_at: 0,
485 expires_at: 0,
486 protected: false,
487 disk_warn: false,
488 status: "ready".into(),
489 }
490 }
491
492 fn usage(n: usize, ram: u64, disk: u64) -> Usage {
493 Usage {
494 clones: (0..n).map(|i| c(&i.to_string(), None, None)).collect(),
495 ram_bytes: ram,
496 disk_bytes: disk,
497 free_disk_bytes: 0,
498 queued: 0,
499 max_clones: 3,
500 ram_budget: None,
501 disk_budget: None,
502 }
503 }
504
505 #[test]
506 fn fits_cases() {
507 assert_eq!(fits(&usage(2, 0, 0), 10), None);
508 assert_eq!(fits(&usage(3, 0, 0), 10), Some(Limit::Count));
509 assert_eq!(fits(&usage(1, u64::MAX / 2, u64::MAX), u64::MAX / 2), None);
511
512 let mut u = usage(1, 90, 0);
513 u.ram_budget = Some(100);
514 assert_eq!(fits(&u, 10), None); assert_eq!(fits(&u, 11), Some(Limit::Ram));
516
517 let mut u = usage(1, 0, 99);
518 u.disk_budget = Some(100);
519 assert_eq!(fits(&u, 10), None);
520 u.disk_bytes = 100;
521 assert_eq!(fits(&u, 10), Some(Limit::Disk));
522
523 let mut u = usage(3, 1000, 1000);
525 u.ram_budget = Some(1);
526 u.disk_budget = Some(1);
527 assert_eq!(fits(&u, 10), Some(Limit::Count));
528 }
529
530 #[test]
531 fn ram_estimate_average_and_default() {
532 assert_eq!(ram_estimate(&[]), 64 * 1024 * 1024);
533 assert_eq!(ram_estimate(&[100, 200, 300]), 200);
534 assert_eq!(ram_estimate(&[7]), 7);
535 }
536
537 #[test]
538 fn exhausted_names_limit_and_largest_three() {
539 let mut u = usage(0, 500, 0);
540 u.ram_budget = Some(400);
541 u.clones = vec![
542 c("a", Some(10), None),
543 c("b", Some(400), None),
544 c("c", Some(50), None),
545 c("d", Some(40), None),
546 ];
547 let e = exhausted(&u, Limit::Ram, 100);
548 assert_eq!(e.code, ErrorCode::BudgetFull);
549 assert!(e.message.contains("ram_budget"), "{}", e.message);
550 let at = |id: &str| e.message.find(&format!("{id} (")).unwrap_or(usize::MAX);
551 assert!(at("b") < at("c") && at("c") < at("d") && at("a") == usize::MAX);
552 let e = exhausted(&u, Limit::Disk, 0);
553 assert!(e.message.contains("disk_budget"), "{}", e.message);
554 assert_eq!(exhausted(&u, Limit::Count, 0).code, ErrorCode::CapReached);
555 }
556
557 #[test]
558 fn cap_message_names_the_setting_and_at_most_three() {
559 let labels: Vec<String> = ["a", "b", "c", "d", "e"].map(String::from).to_vec();
560 let e = cap_reached(5, &labels);
561 assert_eq!(e.code, ErrorCode::CapReached);
562 assert_eq!(
563 e.message,
564 "clone limit reached (max_clones = 5): a, b, c and 2 more"
565 );
566 assert_eq!(
567 cap_reached(2, &labels[..2]).message,
568 "clone limit reached (max_clones = 2): a, b"
569 );
570 assert_eq!(
571 cap_reached(0, &[]).message,
572 "clone limit reached (max_clones = 0)"
573 );
574 }
575
576 #[test]
577 fn budget_message_has_no_empty_largest_list() {
578 let mut u = usage(0, 0, 0);
579 u.ram_budget = Some(1);
580 let e = exhausted(&u, Limit::Ram, 64);
581 assert!(!e.message.contains("largest"), "{}", e.message);
582 u.disk_budget = Some(1);
583 u.clones = vec![c("a", None, Some(0))];
584 let e = exhausted(&u, Limit::Disk, 0);
585 assert!(!e.message.contains("largest"), "{}", e.message);
586 }
587
588 #[test]
589 fn a_waking_clone_counts_for_ram() {
590 let tmp = tempfile::Builder::new()
591 .prefix("mt")
592 .tempdir_in(crate::paths::test_tmp())
593 .unwrap();
594 let ctx = Ctx::open(
595 crate::paths::Home::at(tmp.path().to_path_buf()),
596 crate::ctx::Config::default(),
597 )
598 .unwrap();
599 let row = crate::state::CloneRow {
600 id: "cb0550f".into(),
601 name: None,
602 template: "main".into(),
603 dir: ctx.home.clones_dir().join("cb0550f"),
604 port: 54320,
605 socket_dir: ctx.home.sockets_dir().join("cb0550f"),
606 password: "x".repeat(32),
607 database: "app".into(),
608 pg_pid: None,
609 pg_start_time: None,
610 parent_pid: None,
611 parent_start_time: None,
612 created_at: 0,
613 expires_at: i64::MAX,
614 durable: false,
615 status: CloneStatus::Sleeping,
616 generation: 1,
617 protected: false,
618 suspect: false,
619 };
620 ctx.store().insert_clone(&row).unwrap();
621 assert_eq!(
622 collect(&ctx, true, false).unwrap().0.ram_bytes,
623 0,
624 "asleep: no memory"
625 );
626 let _waking = crate::clones::mark(&ctx, &row.id, crate::ctx::InFlightOp::Create).unwrap();
627 assert_eq!(
628 collect(&ctx, true, false).unwrap().0.ram_bytes,
629 DEFAULT_RAM_ESTIMATE,
630 "waking: one estimate until it can be measured"
631 );
632 }
633}