The memory the daemon has already promised to commands it let through.
One free-memory reading cannot stop several agents from starting several builds at once: each is judged in the same second, each sees the same free figure, and each is told there is room. A command's memory only shows up in that figure once the command has grown into it, which for a build is tens of seconds later.
So the daemon keeps a booking for every command it has judged: the megabytes Jev's load answer says it needs. The next command is judged against what is free LESS what is booked. A booking ends when its command does. Until then it counts in full while the command is young, and less as it ages, because by then the command's real use is in the free figure itself and counting both would book the same memory twice.
Every session on the machine talks to this one daemon, so the ledger is the machine's, not a session's.
A booking counts in full for this long after its command was let through.
23pub const COUNTED_IN_FULL_FOR: Duration = Duration::from_secs(20);
Past this age a booking counts for nothing: the command has taken what it is going to take, and the free figure shows it. Between the two, the share falls in a straight line.
28pub const COUNTED_UNTIL: Duration = Duration::from_secs(90);
The share of a booking of this age that still counts.
54impl Ledger {
Books mb megabytes for the tool call id. Booking the same call
again replaces its entry, so a call is never counted twice.
Ends the booking of a call that has finished, failed or never ran.
Ends every booking of a session that has closed.
The megabytes booked right now, each booking weighted by its age.
How many commands hold a booking.
The case the ledger exists for.
87 #[test] 88 fn five_builds_in_one_second_are_all_counted() { 89 // The case this exists for: 8 GB free, five agents each start a 90 // 3 GB build at the same moment. On the free figure alone all five 91 // fit; with bookings, the third already does not. 92 let now = Instant::now(); 93 let mut ledger = Ledger::default(); 94 let free = 8000.0; 95 let mut let_through = 0; 96 for call in 0..5 { 97 if free - ledger.booked_mb(now) >= 3000.0 { 98 ledger.book(&format!("call-{call}"), "session", 3000.0, now); 99 let_through += 1; 100 } 101 } 102 assert_eq!(let_through, 2); 103 assert_eq!(ledger.booked_mb(now), 6000.0); 104 }
A booking ends with its command, or with its session, so a crashed session cannot hold memory for ever.
108 #[test] 109 fn a_finished_command_gives_its_booking_back() { 110 let now = Instant::now(); 111 let mut ledger = Ledger::default(); 112 ledger.book("a", "one", 3000.0, now); 113 ledger.book("b", "two", 500.0, now); 114 ledger.release("a"); 115 assert_eq!(ledger.booked_mb(now), 500.0); 116 ledger.release_session("two"); 117 assert_eq!(ledger.running(), 0); 118 }
A long-running command must not be counted twice: once here, and once in the machine's own figure after it has taken its memory.
122 #[test] 123 fn a_booking_fades_as_the_command_grows_into_its_memory() { 124 let at = Instant::now(); 125 let mut ledger = Ledger::default(); 126 ledger.book("a", "one", 1000.0, at); 127 assert_eq!(ledger.booked_mb(at + COUNTED_IN_FULL_FOR), 1000.0); 128 let halfway = COUNTED_IN_FULL_FOR + (COUNTED_UNTIL - COUNTED_IN_FULL_FOR) / 2; 129 assert!((ledger.booked_mb(at + halfway) - 500.0).abs() < 1.0); 130 assert_eq!(ledger.booked_mb(at + COUNTED_UNTIL), 0.0); 131 // Still held, though it no longer counts: only the outcome ends it. 132 assert_eq!(ledger.running(), 1); 133 }
The mod asks about a waiting command many times; its booking is keyed by the call, so it is one booking.