1use std::collections::{BTreeMap, BTreeSet};
4use std::fmt;
5use std::ops::{BitAnd, BitOr, BitOrAssign, Sub};
6use std::sync::{Arc, Mutex};
7use std::time::{Duration, Instant};
8
9use crate::accounting::{ResourceClass, ResourceLedger};
10use crate::metrics::{ChannelMetricClass, RuntimeMetrics, WakeMetric};
11
12pub type CapabilityId = u64;
13
14#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
15pub struct ReadyFlags(u16);
16
17impl ReadyFlags {
18 pub const READABLE: Self = Self(1 << 0);
19 pub const WRITABLE: Self = Self(1 << 1);
20 pub const ACCEPT: Self = Self(1 << 2);
21 pub const DATAGRAM: Self = Self(1 << 3);
22 pub const END: Self = Self(1 << 4);
23 pub const ERROR: Self = Self(1 << 5);
24 pub const CLOSE: Self = Self(1 << 6);
25
26 pub const fn is_empty(self) -> bool {
27 self.0 == 0
28 }
29
30 pub const fn intersects(self, other: Self) -> bool {
31 self.0 & other.0 != 0
32 }
33
34 pub const fn bits(self) -> u16 {
35 self.0
36 }
37}
38
39impl BitOr for ReadyFlags {
40 type Output = Self;
41
42 fn bitor(self, rhs: Self) -> Self::Output {
43 Self(self.0 | rhs.0)
44 }
45}
46
47impl BitOrAssign for ReadyFlags {
48 fn bitor_assign(&mut self, rhs: Self) {
49 self.0 |= rhs.0;
50 }
51}
52
53impl BitAnd for ReadyFlags {
54 type Output = Self;
55
56 fn bitand(self, rhs: Self) -> Self::Output {
57 Self(self.0 & rhs.0)
58 }
59}
60
61impl Sub for ReadyFlags {
62 type Output = Self;
63
64 fn sub(self, rhs: Self) -> Self::Output {
65 Self(self.0 & !rhs.0)
66 }
67}
68
69#[derive(Clone, Copy, Debug, Eq, PartialEq)]
70pub struct ReadyWake {
71 pub generation: u64,
72 pub epoch: u64,
73}
74
75#[derive(Clone, Copy, Debug, Eq, PartialEq)]
76pub struct ReadyObservation {
77 pub capability_id: CapabilityId,
78 pub capability_generation: u64,
79 pub flags: ReadyFlags,
80 pub revision: u64,
81}
82
83#[derive(Clone, Debug, Eq, PartialEq)]
84pub struct ReadyBatch {
85 pub generation: u64,
86 pub epoch: u64,
87 pub entries: Vec<ReadyObservation>,
88 pub signals_ready: bool,
89 pub timers_ready: bool,
90 pub more: bool,
91}
92
93#[derive(Clone, Copy, Debug, Eq, PartialEq)]
94pub struct ReadyAcknowledgement {
95 pub capability_id: CapabilityId,
96 pub capability_generation: u64,
97 pub observed_revision: u64,
98 pub clear: ReadyFlags,
99}
100
101#[derive(Debug, Clone, Eq, PartialEq)]
102pub enum ReadyError {
103 WrongGeneration {
104 supplied: u64,
105 expected: u64,
106 },
107 StaleWake {
108 supplied: u64,
109 outstanding: Option<u64>,
110 },
111 HandleLimit {
112 limit: usize,
113 },
114 ControlLimit {
115 control: &'static str,
116 limit: usize,
117 config_path: String,
118 },
119 InvalidSignal {
120 signal: i32,
121 },
122 StaleCapabilityGeneration {
123 capability_id: CapabilityId,
124 supplied: u64,
125 expected: u64,
126 },
127 MissingResourceLimit {
128 resource: ResourceClass,
129 },
130 RevisionExhausted {
131 capability_id: CapabilityId,
132 },
133 EpochExhausted,
134 WakeInvariant,
135 WakeDisconnected,
136 Poisoned,
137}
138
139impl fmt::Display for ReadyError {
140 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
141 match self {
142 Self::WrongGeneration { supplied, expected } => write!(
143 formatter,
144 "ERR_AGENTOS_READY_STALE_GENERATION: supplied generation {supplied}, expected {expected}"
145 ),
146 Self::StaleWake {
147 supplied,
148 outstanding,
149 } => write!(
150 formatter,
151 "ERR_AGENTOS_READY_STALE_WAKE: supplied epoch {supplied}, outstanding epoch {outstanding:?}"
152 ),
153 Self::HandleLimit { limit } => write!(
154 formatter,
155 "ERR_AGENTOS_READY_HANDLE_LIMIT: ready handle count exceeded {limit}; raise limits.reactor.maxReadyHandles (VM) or runtime.resources.maxReadyHandles (process)"
156 ),
157 Self::ControlLimit {
158 control,
159 limit,
160 config_path,
161 } => write!(
162 formatter,
163 "ERR_AGENTOS_READY_CONTROL_LIMIT: pending {control} count exceeded {limit}; raise {config_path}"
164 ),
165 Self::InvalidSignal { signal } => write!(
166 formatter,
167 "ERR_AGENTOS_READY_SIGNAL_INVALID: signal {signal} is outside the supported 1..=64 range"
168 ),
169 Self::StaleCapabilityGeneration {
170 capability_id,
171 supplied,
172 expected,
173 } => write!(
174 formatter,
175 "ERR_AGENTOS_READY_STALE_CAPABILITY: capability {capability_id} generation {supplied} does not match live generation {expected}"
176 ),
177 Self::MissingResourceLimit { resource } => write!(
178 formatter,
179 "ERR_AGENTOS_READY_RESOURCE_UNBOUNDED: resource={} has no configured VM limit",
180 resource.name()
181 ),
182 Self::RevisionExhausted { capability_id } => write!(
183 formatter,
184 "ERR_AGENTOS_READY_REVISION_EXHAUSTED: capability {capability_id} exhausted readiness revisions"
185 ),
186 Self::EpochExhausted => formatter.write_str(
187 "ERR_AGENTOS_READY_EPOCH_EXHAUSTED: VM generation exhausted wake epochs",
188 ),
189 Self::WakeInvariant => formatter.write_str(
190 "ERR_AGENTOS_READY_WAKE_INVARIANT: capacity-one wake lane was full while broker state was idle",
191 ),
192 Self::WakeDisconnected => formatter.write_str(
193 "ERR_AGENTOS_READY_WAKE_DISCONNECTED: VM wake consumer disconnected",
194 ),
195 Self::Poisoned => formatter.write_str(
196 "ERR_AGENTOS_READY_STATE_POISONED: VM readiness state lock poisoned",
197 ),
198 }
199 }
200}
201
202impl std::error::Error for ReadyError {}
203
204#[derive(Debug)]
205struct ReadyEntry {
206 capability_generation: u64,
207 flags: ReadyFlags,
208 revision: u64,
209 application_read_interest: bool,
210 ready_since: Option<Instant>,
211}
212
213#[derive(Debug)]
214enum WakeState {
215 Idle,
216 Outstanding { epoch: u64 },
217 Failed(WakeFailure),
218}
219
220#[derive(Clone, Copy, Debug)]
221enum WakeFailure {
222 Invariant,
223 Disconnected,
224}
225
226impl WakeFailure {
227 fn error(self) -> ReadyError {
228 match self {
229 Self::Invariant => ReadyError::WakeInvariant,
230 Self::Disconnected => ReadyError::WakeDisconnected,
231 }
232 }
233}
234
235#[derive(Debug)]
236struct ReadyState {
237 handles: BTreeMap<CapabilityId, ReadyEntry>,
238 last_delivered_capability: Option<CapabilityId>,
239 signals: BTreeSet<i32>,
240 timers: BTreeSet<u64>,
241 wake: WakeState,
242 next_epoch: u64,
243}
244
245#[derive(Clone, Debug)]
246pub struct SessionReadyBroker {
247 generation: u64,
248 max_handles: usize,
249 max_batch_handles: usize,
250 max_pending_timers: usize,
251 timer_limit_config_path: String,
252 state: Arc<Mutex<ReadyState>>,
253 wake_tx: tokio::sync::mpsc::Sender<ReadyWake>,
254 metrics: Option<RuntimeMetrics>,
255}
256
257impl SessionReadyBroker {
258 pub fn new(
259 generation: u64,
260 max_handles: usize,
261 ) -> Result<(Self, tokio::sync::mpsc::Receiver<ReadyWake>), ReadyError> {
262 Self::new_inner(
263 generation,
264 max_handles,
265 max_handles,
266 max_handles,
267 String::from("limits.jsRuntime.maxTimers"),
268 None,
269 )
270 }
271
272 pub fn new_with_metrics(
273 generation: u64,
274 max_handles: usize,
275 metrics: RuntimeMetrics,
276 ) -> Result<(Self, tokio::sync::mpsc::Receiver<ReadyWake>), ReadyError> {
277 Self::new_inner(
278 generation,
279 max_handles,
280 max_handles,
281 max_handles,
282 String::from("limits.jsRuntime.maxTimers"),
283 Some(metrics),
284 )
285 }
286
287 pub fn new_with_resources(
288 generation: u64,
289 resources: Arc<ResourceLedger>,
290 metrics: RuntimeMetrics,
291 ) -> Result<(Self, tokio::sync::mpsc::Receiver<ReadyWake>), ReadyError> {
292 let max_handles = configured_limit(&resources, ResourceClass::ReadyHandles)?;
293 let timer_limit = configured_resource_limit(&resources, ResourceClass::Timers)?;
294 Self::new_inner(
295 generation,
296 max_handles,
297 max_handles,
298 timer_limit.maximum,
299 timer_limit.config_path,
300 Some(metrics),
301 )
302 }
303
304 fn new_inner(
305 generation: u64,
306 max_handles: usize,
307 max_batch_handles: usize,
308 max_pending_timers: usize,
309 timer_limit_config_path: String,
310 metrics: Option<RuntimeMetrics>,
311 ) -> Result<(Self, tokio::sync::mpsc::Receiver<ReadyWake>), ReadyError> {
312 if max_handles == 0 {
313 return Err(ReadyError::HandleLimit { limit: 0 });
314 }
315 let (wake_tx, wake_rx) = tokio::sync::mpsc::channel(1);
316 Ok((
317 Self {
318 generation,
319 max_handles,
320 max_batch_handles,
321 max_pending_timers,
322 timer_limit_config_path,
323 state: Arc::new(Mutex::new(ReadyState {
324 handles: BTreeMap::new(),
325 last_delivered_capability: None,
326 signals: BTreeSet::new(),
327 timers: BTreeSet::new(),
328 wake: WakeState::Idle,
329 next_epoch: 1,
330 })),
331 wake_tx,
332 metrics,
333 },
334 wake_rx,
335 ))
336 }
337
338 pub fn generation(&self) -> u64 {
339 self.generation
340 }
341
342 pub fn max_batch_handles(&self) -> usize {
343 self.max_batch_handles
344 }
345
346 pub fn mark_signal_ready(&self, generation: u64, signal: i32) -> Result<(), ReadyError> {
347 self.validate_generation(generation)?;
348 if !(1..=64).contains(&signal) {
349 return Err(ReadyError::InvalidSignal { signal });
350 }
351 let mut state = self.state.lock().map_err(|_| ReadyError::Poisoned)?;
352 state.signals.insert(signal);
353 self.schedule_wake_locked(&mut state, false)
354 }
355
356 pub fn mark_timer_ready(&self, generation: u64, timer_id: u64) -> Result<(), ReadyError> {
357 self.validate_generation(generation)?;
358 let mut state = self.state.lock().map_err(|_| ReadyError::Poisoned)?;
359 if !state.timers.contains(&timer_id) && state.timers.len() >= self.max_pending_timers {
360 return Err(ReadyError::ControlLimit {
361 control: "timers",
362 limit: self.max_pending_timers,
363 config_path: self.timer_limit_config_path.clone(),
364 });
365 }
366 state.timers.insert(timer_id);
367 self.schedule_wake_locked(&mut state, false)
368 }
369
370 pub fn drain_signals(
371 &self,
372 generation: u64,
373 epoch: u64,
374 max: usize,
375 ) -> Result<Vec<i32>, ReadyError> {
376 self.validate_generation(generation)?;
377 let mut state = self.state.lock().map_err(|_| ReadyError::Poisoned)?;
378 self.validate_epoch(&state, epoch)?;
379 let values = state
380 .signals
381 .iter()
382 .copied()
383 .take(max.max(1))
384 .collect::<Vec<_>>();
385 for value in &values {
386 state.signals.remove(value);
387 }
388 Ok(values)
389 }
390
391 pub fn drain_timers(
392 &self,
393 generation: u64,
394 epoch: u64,
395 max: usize,
396 ) -> Result<Vec<u64>, ReadyError> {
397 self.validate_generation(generation)?;
398 let mut state = self.state.lock().map_err(|_| ReadyError::Poisoned)?;
399 self.validate_epoch(&state, epoch)?;
400 let values = state
401 .timers
402 .iter()
403 .copied()
404 .take(max.max(1))
405 .collect::<Vec<_>>();
406 for value in &values {
407 state.timers.remove(value);
408 }
409 Ok(values)
410 }
411
412 pub fn mark_ready(
413 &self,
414 generation: u64,
415 capability_id: CapabilityId,
416 capability_generation: u64,
417 mut flags: ReadyFlags,
418 ) -> Result<(), ReadyError> {
419 self.validate_generation(generation)?;
420 let mut state = self.state.lock().map_err(|_| ReadyError::Poisoned)?;
421 let existing = state.handles.get(&capability_id);
422 if let Some(entry) = existing {
423 validate_capability_generation(
424 capability_id,
425 capability_generation,
426 entry.capability_generation,
427 )?;
428 } else if state.handles.len() >= self.max_handles {
429 return Err(ReadyError::HandleLimit {
430 limit: self.max_handles,
431 });
432 }
433 if existing.is_some_and(|entry| !entry.application_read_interest) {
434 flags = flags - ReadyFlags::READABLE;
435 }
436 if flags.is_empty() {
437 if existing.is_none() {
438 state.handles.insert(
439 capability_id,
440 ReadyEntry {
441 capability_generation,
442 flags: ReadyFlags::default(),
443 revision: 0,
444 application_read_interest: true,
445 ready_since: None,
446 },
447 );
448 }
449 return Ok(());
450 }
451 let entry = state.handles.entry(capability_id).or_insert(ReadyEntry {
452 capability_generation,
453 flags: ReadyFlags::default(),
454 revision: 0,
455 application_read_interest: true,
456 ready_since: None,
457 });
458 if entry.flags.is_empty() {
459 entry.ready_since = Some(Instant::now());
460 }
461 entry.revision = entry
462 .revision
463 .checked_add(1)
464 .ok_or(ReadyError::RevisionExhausted { capability_id })?;
465 entry.flags |= flags;
466 if let Some(metrics) = &self.metrics {
467 metrics.record_wake(WakeMetric::Attempted);
468 }
469 let result = self.schedule_wake_locked(&mut state, false);
470 self.observe_ready_state(&state);
471 result
472 }
473
474 pub fn set_application_read_interest(
475 &self,
476 generation: u64,
477 capability_id: CapabilityId,
478 capability_generation: u64,
479 enabled: bool,
480 ) -> Result<(), ReadyError> {
481 self.validate_generation(generation)?;
482 let mut state = self.state.lock().map_err(|_| ReadyError::Poisoned)?;
483 if !state.handles.contains_key(&capability_id) && state.handles.len() >= self.max_handles {
484 return Err(ReadyError::HandleLimit {
485 limit: self.max_handles,
486 });
487 }
488 let entry = state.handles.entry(capability_id).or_insert(ReadyEntry {
489 capability_generation,
490 flags: ReadyFlags::default(),
491 revision: 0,
492 application_read_interest: enabled,
493 ready_since: None,
494 });
495 validate_capability_generation(
496 capability_id,
497 capability_generation,
498 entry.capability_generation,
499 )?;
500 entry.application_read_interest = enabled;
501 if !enabled && entry.flags.intersects(ReadyFlags::READABLE) {
502 entry.revision = entry
503 .revision
504 .checked_add(1)
505 .ok_or(ReadyError::RevisionExhausted { capability_id })?;
506 entry.flags = entry.flags - ReadyFlags::READABLE;
507 if entry.flags.is_empty() {
508 entry.ready_since = None;
509 }
510 }
511 self.observe_ready_state(&state);
512 Ok(())
513 }
514
515 pub fn remove_capability(
516 &self,
517 generation: u64,
518 capability_id: CapabilityId,
519 capability_generation: u64,
520 ) -> Result<(), ReadyError> {
521 self.validate_generation(generation)?;
522 let mut state = self.state.lock().map_err(|_| ReadyError::Poisoned)?;
523 if let Some(entry) = state.handles.get(&capability_id) {
524 validate_capability_generation(
525 capability_id,
526 capability_generation,
527 entry.capability_generation,
528 )?;
529 }
530 state.handles.remove(&capability_id);
531 self.observe_ready_state(&state);
532 Ok(())
533 }
534
535 pub fn ready_batch(
536 &self,
537 generation: u64,
538 epoch: u64,
539 max_handles: usize,
540 ) -> Result<ReadyBatch, ReadyError> {
541 self.validate_generation(generation)?;
542 let mut state = self.state.lock().map_err(|_| ReadyError::Poisoned)?;
543 self.validate_epoch(&state, epoch)?;
544 if let Some(metrics) = &self.metrics {
545 metrics.record_wake(WakeMetric::Delivered);
546 }
547 let limit = max_handles.max(1).min(self.max_batch_handles);
548 let ready_count = state
549 .handles
550 .values()
551 .filter(|entry| !entry.flags.is_empty())
552 .count();
553 let mut entries = Vec::with_capacity(limit.min(ready_count));
554
555 if let Some(cursor) = state.last_delivered_capability {
559 for (&capability_id, entry) in state.handles.range((
560 std::ops::Bound::Excluded(cursor),
561 std::ops::Bound::Unbounded,
562 )) {
563 append_ready_observation(&mut entries, limit, capability_id, entry);
564 if entries.len() == limit {
565 break;
566 }
567 }
568 if entries.len() < limit {
569 for (&capability_id, entry) in state.handles.range(..=cursor) {
570 append_ready_observation(&mut entries, limit, capability_id, entry);
571 if entries.len() == limit {
572 break;
573 }
574 }
575 }
576 } else {
577 for (&capability_id, entry) in &state.handles {
578 append_ready_observation(&mut entries, limit, capability_id, entry);
579 if entries.len() == limit {
580 break;
581 }
582 }
583 }
584 if let Some(last) = entries.last() {
585 state.last_delivered_capability = Some(last.capability_id);
586 }
587 self.observe_ready_state(&state);
588 Ok(ReadyBatch {
589 generation,
590 epoch,
591 signals_ready: !state.signals.is_empty(),
592 timers_ready: !state.timers.is_empty(),
593 more: ready_count > entries.len(),
594 entries,
595 })
596 }
597
598 pub fn complete_wake(
599 &self,
600 generation: u64,
601 epoch: u64,
602 acknowledgements: &[ReadyAcknowledgement],
603 ) -> Result<(), ReadyError> {
604 self.validate_generation(generation)?;
605 let mut state = self.state.lock().map_err(|_| ReadyError::Poisoned)?;
606 self.validate_epoch(&state, epoch)?;
607 for acknowledgement in acknowledgements {
608 let Some(entry) = state.handles.get_mut(&acknowledgement.capability_id) else {
609 continue;
610 };
611 if entry.capability_generation != acknowledgement.capability_generation {
612 continue;
613 }
614 if entry.revision == acknowledgement.observed_revision {
615 let mut clear = acknowledgement.clear;
616 if !entry.application_read_interest {
617 clear |= ReadyFlags::READABLE;
618 }
619 entry.flags = entry.flags - clear;
620 if entry.flags.is_empty() {
621 entry.ready_since = None;
622 }
623 }
624 }
625 state.wake = WakeState::Idle;
626 let result = self.schedule_wake_locked(&mut state, true);
627 self.observe_ready_state(&state);
628 result
629 }
630
631 pub fn pending_handle_count(&self) -> Result<usize, ReadyError> {
632 Ok(self
633 .state
634 .lock()
635 .map_err(|_| ReadyError::Poisoned)?
636 .handles
637 .values()
638 .filter(|entry| !entry.flags.is_empty())
639 .count())
640 }
641
642 fn validate_generation(&self, generation: u64) -> Result<(), ReadyError> {
643 if generation != self.generation {
644 return Err(ReadyError::WrongGeneration {
645 supplied: generation,
646 expected: self.generation,
647 });
648 }
649 Ok(())
650 }
651
652 fn validate_epoch(&self, state: &ReadyState, epoch: u64) -> Result<(), ReadyError> {
653 let outstanding = match state.wake {
654 WakeState::Outstanding { epoch } => Some(epoch),
655 WakeState::Idle | WakeState::Failed(_) => None,
656 };
657 if outstanding != Some(epoch) {
658 return Err(ReadyError::StaleWake {
659 supplied: epoch,
660 outstanding,
661 });
662 }
663 Ok(())
664 }
665
666 fn schedule_wake_locked(
667 &self,
668 state: &mut ReadyState,
669 rearmed: bool,
670 ) -> Result<(), ReadyError> {
671 if !state.handles.values().any(|entry| !entry.flags.is_empty())
672 && state.signals.is_empty()
673 && state.timers.is_empty()
674 {
675 return Ok(());
676 }
677 match state.wake {
678 WakeState::Idle => {}
679 WakeState::Outstanding { .. } => {
680 if let Some(metrics) = &self.metrics {
681 metrics.record_wake(WakeMetric::Coalesced);
682 }
683 return Ok(());
684 }
685 WakeState::Failed(failure) => return Err(failure.error()),
686 }
687 let epoch = state.next_epoch;
688 state.next_epoch = state
689 .next_epoch
690 .checked_add(1)
691 .ok_or(ReadyError::EpochExhausted)?;
692 state.wake = WakeState::Outstanding { epoch };
693 match self.wake_tx.try_send(ReadyWake {
694 generation: self.generation,
695 epoch,
696 }) {
697 Ok(()) => {
698 if let Some(metrics) = &self.metrics {
699 if rearmed {
700 metrics.record_wake(WakeMetric::Rearmed);
701 }
702 metrics.observe_channel(
703 ChannelMetricClass::ReadyWake,
704 1,
705 std::mem::size_of::<ReadyWake>(),
706 );
707 }
708 Ok(())
709 }
710 Err(tokio::sync::mpsc::error::TrySendError::Full(_)) => {
711 state.wake = WakeState::Failed(WakeFailure::Invariant);
712 Err(ReadyError::WakeInvariant)
713 }
714 Err(tokio::sync::mpsc::error::TrySendError::Closed(_)) => {
715 state.wake = WakeState::Failed(WakeFailure::Disconnected);
716 Err(ReadyError::WakeDisconnected)
717 }
718 }
719 }
720
721 fn observe_ready_state(&self, state: &ReadyState) {
722 let Some(metrics) = &self.metrics else {
723 return;
724 };
725 let now = Instant::now();
726 let mut size = 0usize;
727 let mut oldest = Duration::ZERO;
728 for entry in state.handles.values() {
729 if entry.flags.is_empty() {
730 continue;
731 }
732 size += 1;
733 if let Some(ready_since) = entry.ready_since {
734 oldest = oldest.max(now.saturating_duration_since(ready_since));
735 }
736 }
737 metrics.observe_readiness(size, oldest);
738 }
739}
740
741fn append_ready_observation(
742 entries: &mut Vec<ReadyObservation>,
743 limit: usize,
744 capability_id: CapabilityId,
745 entry: &ReadyEntry,
746) {
747 if entries.len() < limit && !entry.flags.is_empty() {
748 entries.push(ReadyObservation {
749 capability_id,
750 capability_generation: entry.capability_generation,
751 flags: entry.flags,
752 revision: entry.revision,
753 });
754 }
755}
756
757fn configured_limit(
758 resources: &ResourceLedger,
759 resource: ResourceClass,
760) -> Result<usize, ReadyError> {
761 resources
762 .usage(resource)
763 .limit
764 .filter(|limit| *limit > 0)
765 .ok_or(ReadyError::MissingResourceLimit { resource })
766}
767
768fn configured_resource_limit(
769 resources: &ResourceLedger,
770 resource: ResourceClass,
771) -> Result<crate::accounting::ResourceLimit, ReadyError> {
772 resources
773 .configured_limit(resource)
774 .filter(|limit| limit.maximum > 0)
775 .ok_or(ReadyError::MissingResourceLimit { resource })
776}
777
778fn validate_capability_generation(
779 capability_id: CapabilityId,
780 supplied: u64,
781 expected: u64,
782) -> Result<(), ReadyError> {
783 if supplied == expected {
784 Ok(())
785 } else {
786 Err(ReadyError::StaleCapabilityGeneration {
787 capability_id,
788 supplied,
789 expected,
790 })
791 }
792}
793
794#[cfg(test)]
795mod tests {
796 use super::*;
797 use crate::accounting::ResourceLimit;
798 use crate::capability::{CapabilityBackend, CapabilityKind, CapabilityRegistry};
799
800 #[tokio::test]
801 async fn repeated_marks_coalesce_to_one_wake() {
802 let (broker, mut wakes) = SessionReadyBroker::new(7, 8).expect("broker");
803 for _ in 0..1_000_000 {
804 broker
805 .mark_ready(7, 41, 1, ReadyFlags::READABLE)
806 .expect("mark ready");
807 }
808 let wake = wakes.recv().await.expect("one wake");
809 assert_eq!(
810 wake,
811 ReadyWake {
812 generation: 7,
813 epoch: 1
814 }
815 );
816 assert!(matches!(
817 wakes.try_recv(),
818 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
819 ));
820 let batch = broker.ready_batch(7, wake.epoch, 8).expect("ready batch");
821 assert_eq!(batch.entries.len(), 1);
822 assert_eq!(batch.entries[0].revision, 1_000_000);
823 }
824
825 #[tokio::test]
826 async fn signals_and_timers_share_one_wake_but_keep_durable_control_state() {
827 let (broker, mut wakes) = SessionReadyBroker::new(8, 8).expect("broker");
828 for _ in 0..10_000 {
829 broker.mark_signal_ready(8, 15).expect("coalesce SIGTERM");
830 broker.mark_timer_ready(8, 91).expect("coalesce timer");
831 }
832 let wake = wakes.recv().await.expect("one control wake");
833 assert!(matches!(
834 wakes.try_recv(),
835 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
836 ));
837 let batch = broker.ready_batch(8, wake.epoch, 8).expect("control batch");
838 assert!(batch.signals_ready);
839 assert!(batch.timers_ready);
840 assert_eq!(broker.drain_signals(8, wake.epoch, 8).unwrap(), vec![15]);
841 assert_eq!(broker.drain_timers(8, wake.epoch, 8).unwrap(), vec![91]);
842 broker
843 .complete_wake(8, wake.epoch, &[])
844 .expect("complete control wake");
845 assert!(matches!(
846 wakes.try_recv(),
847 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
848 ));
849 }
850
851 #[test]
852 fn timer_limit_error_names_the_owning_configuration_path() {
853 let resources = Arc::new(ResourceLedger::root(
854 "vm=timer-limit generation=10",
855 [
856 (
857 ResourceClass::ReadyHandles,
858 ResourceLimit::new(2, "limits.reactor.maxReadyHandles"),
859 ),
860 (
861 ResourceClass::Timers,
862 ResourceLimit::new(1, "limits.jsRuntime.maxTimers"),
863 ),
864 ],
865 ));
866 let (broker, _wakes) =
867 SessionReadyBroker::new_with_resources(10, resources, RuntimeMetrics::new())
868 .expect("broker");
869 broker.mark_timer_ready(10, 1).expect("first timer");
870
871 let error = broker
872 .mark_timer_ready(10, 2)
873 .expect_err("second unique timer exceeds configured limit");
874
875 assert_eq!(
876 error,
877 ReadyError::ControlLimit {
878 control: "timers",
879 limit: 1,
880 config_path: String::from("limits.jsRuntime.maxTimers"),
881 }
882 );
883 assert!(error
884 .to_string()
885 .ends_with("raise limits.jsRuntime.maxTimers"));
886 }
887
888 #[tokio::test]
889 async fn concurrent_republication_is_not_cleared_by_old_observation() {
890 let (broker, mut wakes) = SessionReadyBroker::new(9, 8).expect("broker");
891 broker
892 .mark_ready(9, 3, 1, ReadyFlags::READABLE)
893 .expect("first mark");
894 let wake = wakes.recv().await.expect("first wake");
895 let batch = broker.ready_batch(9, wake.epoch, 8).expect("batch");
896 broker
897 .mark_ready(9, 3, 1, ReadyFlags::READABLE)
898 .expect("concurrent mark");
899 broker
900 .complete_wake(
901 9,
902 wake.epoch,
903 &[ReadyAcknowledgement {
904 capability_id: 3,
905 capability_generation: 1,
906 observed_revision: batch.entries[0].revision,
907 clear: ReadyFlags::READABLE,
908 }],
909 )
910 .expect("complete old wake");
911
912 let replacement = wakes.recv().await.expect("replacement wake");
913 let replacement_batch = broker
914 .ready_batch(9, replacement.epoch, 8)
915 .expect("replacement batch");
916 assert_eq!(replacement_batch.entries[0].revision, 2);
917 }
918
919 #[tokio::test]
920 async fn bounded_batches_rotate_past_continuously_hot_low_capability_ids() {
921 let (broker, mut wakes) = SessionReadyBroker::new(10, 8).expect("broker");
922 for capability_id in 1..=8 {
923 broker
924 .mark_ready(10, capability_id, 1, ReadyFlags::READABLE)
925 .expect("mark initial readiness");
926 }
927
928 let first_wake = wakes.recv().await.expect("first wake");
929 let first = broker
930 .ready_batch(10, first_wake.epoch, 2)
931 .expect("first bounded batch");
932 assert_eq!(
933 first
934 .entries
935 .iter()
936 .map(|entry| entry.capability_id)
937 .collect::<Vec<_>>(),
938 vec![1, 2]
939 );
940
941 for capability_id in 1..=2 {
945 broker
946 .mark_ready(10, capability_id, 1, ReadyFlags::READABLE)
947 .expect("keep low capability hot");
948 }
949 let first_acknowledgements = first
950 .entries
951 .iter()
952 .map(|entry| ReadyAcknowledgement {
953 capability_id: entry.capability_id,
954 capability_generation: entry.capability_generation,
955 observed_revision: entry.revision,
956 clear: ReadyFlags::READABLE,
957 })
958 .collect::<Vec<_>>();
959 broker
960 .complete_wake(10, first_wake.epoch, &first_acknowledgements)
961 .expect("complete first wake");
962
963 let second_wake = wakes.recv().await.expect("second wake");
964 let second = broker
965 .ready_batch(10, second_wake.epoch, 2)
966 .expect("second bounded batch");
967 assert_eq!(
968 second
969 .entries
970 .iter()
971 .map(|entry| entry.capability_id)
972 .collect::<Vec<_>>(),
973 vec![3, 4]
974 );
975 assert!(second.more);
976 }
977
978 #[tokio::test]
979 async fn read_interest_suppresses_readable_publication() {
980 let (broker, mut wakes) = SessionReadyBroker::new(11, 8).expect("broker");
981 broker
982 .mark_ready(11, 5, 1, ReadyFlags::WRITABLE)
983 .expect("create entry");
984 let first = wakes.recv().await.expect("first wake");
985 let batch = broker.ready_batch(11, first.epoch, 8).expect("batch");
986 broker
987 .complete_wake(
988 11,
989 first.epoch,
990 &[ReadyAcknowledgement {
991 capability_id: 5,
992 capability_generation: 1,
993 observed_revision: batch.entries[0].revision,
994 clear: ReadyFlags::WRITABLE,
995 }],
996 )
997 .expect("complete wake");
998 broker
999 .set_application_read_interest(11, 5, 1, false)
1000 .expect("pause reads");
1001 broker
1002 .mark_ready(11, 5, 1, ReadyFlags::READABLE)
1003 .expect("suppressed readable mark");
1004 assert!(matches!(
1005 wakes.try_recv(),
1006 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
1007 ));
1008 }
1009
1010 #[tokio::test]
1011 async fn read_interest_set_before_first_publication_is_durable() {
1012 let (broker, mut wakes) = SessionReadyBroker::new(12, 8).expect("broker");
1013 broker
1014 .set_application_read_interest(12, 6, 1, false)
1015 .expect("pause before first readiness");
1016 broker
1017 .mark_ready(12, 6, 1, ReadyFlags::READABLE)
1018 .expect("suppressed first readable mark");
1019 assert!(matches!(
1020 wakes.try_recv(),
1021 Err(tokio::sync::mpsc::error::TryRecvError::Empty)
1022 ));
1023
1024 broker
1025 .mark_ready(12, 6, 1, ReadyFlags::END)
1026 .expect("terminal readiness remains observable while reads are paused");
1027 let wake = wakes.recv().await.expect("terminal wake");
1028 let batch = broker.ready_batch(12, wake.epoch, 8).expect("batch");
1029 assert_eq!(batch.entries.len(), 1);
1030 assert_eq!(batch.entries[0].flags, ReadyFlags::END);
1031 }
1032
1033 #[tokio::test]
1034 async fn stale_completion_cannot_mutate_current_epoch() {
1035 let (broker, mut wakes) = SessionReadyBroker::new(13, 8).expect("broker");
1036 broker
1037 .mark_ready(13, 1, 1, ReadyFlags::CLOSE)
1038 .expect("mark close");
1039 let wake = wakes.recv().await.expect("wake");
1040 let error = broker
1041 .complete_wake(13, wake.epoch + 1, &[])
1042 .expect_err("stale epoch must fail");
1043 assert!(matches!(error, ReadyError::StaleWake { .. }));
1044 assert_eq!(broker.pending_handle_count().expect("pending count"), 1);
1045 }
1046
1047 #[test]
1048 fn disconnected_wake_lane_remains_a_hard_error() {
1049 let (broker, wakes) = SessionReadyBroker::new(14, 8).expect("broker");
1050 drop(wakes);
1051
1052 assert!(matches!(
1053 broker.mark_ready(14, 1, 1, ReadyFlags::READABLE),
1054 Err(ReadyError::WakeDisconnected)
1055 ));
1056 assert!(matches!(
1057 broker.mark_ready(14, 1, 1, ReadyFlags::END),
1058 Err(ReadyError::WakeDisconnected)
1059 ));
1060 }
1061
1062 #[tokio::test]
1063 async fn production_metrics_follow_broker_state_without_id_labels() {
1064 let metrics = RuntimeMetrics::new();
1065 let (broker, mut wakes) =
1066 SessionReadyBroker::new_with_metrics(17, 8, metrics.clone()).expect("broker");
1067 broker
1068 .mark_ready(17, 4, 1, ReadyFlags::READABLE)
1069 .expect("first publication");
1070 broker
1071 .mark_ready(17, 4, 1, ReadyFlags::READABLE)
1072 .expect("coalesced publication");
1073 let wake = wakes.recv().await.expect("wake");
1074 let batch = broker.ready_batch(17, wake.epoch, 8).expect("batch");
1075 broker
1076 .complete_wake(
1077 17,
1078 wake.epoch,
1079 &[ReadyAcknowledgement {
1080 capability_id: 4,
1081 capability_generation: 1,
1082 observed_revision: batch.entries[0].revision,
1083 clear: ReadyFlags::READABLE,
1084 }],
1085 )
1086 .expect("complete");
1087
1088 let snapshot = metrics.snapshot();
1089 assert_eq!(snapshot.wakes[WakeMetric::Attempted.index()], 2);
1090 assert_eq!(snapshot.wakes[WakeMetric::Coalesced.index()], 1);
1091 assert_eq!(snapshot.wakes[WakeMetric::Delivered.index()], 1);
1092 assert_eq!(snapshot.readiness.current_size, 0);
1093 assert_eq!(
1094 snapshot.channels[ChannelMetricClass::ReadyWake.index()].count_high_water,
1095 1
1096 );
1097 }
1098
1099 #[tokio::test]
1100 async fn capability_admission_owns_ready_permits_without_broker_double_charge() {
1101 let resources = Arc::new(ResourceLedger::root(
1102 "vm=readiness-integration generation=23",
1103 [
1104 (
1105 ResourceClass::Capabilities,
1106 ResourceLimit::new(4, "limits.reactor.maxCapabilities"),
1107 ),
1108 (
1109 ResourceClass::ReadyHandles,
1110 ResourceLimit::new(4, "limits.reactor.maxReadyHandles"),
1111 ),
1112 (
1113 ResourceClass::Timers,
1114 ResourceLimit::new(4, "runtime.resources.maxTimers"),
1115 ),
1116 ],
1117 ));
1118 let capabilities = CapabilityRegistry::new(23, Arc::clone(&resources));
1119 let (broker, mut wakes) = SessionReadyBroker::new_with_resources(
1120 23,
1121 Arc::clone(&resources),
1122 RuntimeMetrics::new(),
1123 )
1124 .expect("bounded broker");
1125
1126 let mut leases = Vec::new();
1127 for index in 0..4_u64 {
1128 let lease = capabilities
1129 .reserve(CapabilityKind::Http2Stream)
1130 .expect("admit capability up to configured maximum")
1131 .commit(CapabilityBackend::Native {
1132 local_id: format!("stream-{index}"),
1133 })
1134 .expect("commit capability");
1135 broker
1136 .mark_ready(23, lease.id(), lease.generation(), ReadyFlags::READABLE)
1137 .expect("readiness publication must consume no second permit");
1138 leases.push(lease);
1139 }
1140 assert_eq!(resources.usage(ResourceClass::ReadyHandles).used, 4);
1141
1142 let wake = wakes.recv().await.expect("one coalesced wake");
1143 let batch = broker.ready_batch(23, wake.epoch, 4).expect("ready batch");
1144 assert_eq!(batch.entries.len(), 4);
1145 let acknowledgements = batch
1146 .entries
1147 .iter()
1148 .map(|entry| ReadyAcknowledgement {
1149 capability_id: entry.capability_id,
1150 capability_generation: entry.capability_generation,
1151 observed_revision: entry.revision,
1152 clear: entry.flags,
1153 })
1154 .collect::<Vec<_>>();
1155 broker
1156 .complete_wake(23, wake.epoch, &acknowledgements)
1157 .expect("complete wake");
1158 drop(broker);
1159 drop(leases);
1160 drop(capabilities);
1161 assert!(resources.is_zero(), "all capability permits must reconcile");
1162 }
1163}