1use crate::authority::authority_per_epoch_store::AuthorityPerEpochStore;
5use crate::consensus_adapter::ConsensusAdapter;
6use crate::staggered_submission::proposers_metric_label;
7use arc_swap::ArcSwap;
8use mysten_common::debug_fatal;
9use mysten_metrics::{COUNT_BUCKETS, spawn_monitored_task};
10use prometheus::{
11 Histogram, HistogramVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, Registry,
12 register_histogram_vec_with_registry, register_histogram_with_registry,
13 register_int_counter_vec_with_registry, register_int_counter_with_registry,
14 register_int_gauge_vec_with_registry, register_int_gauge_with_registry,
15};
16use std::collections::{BTreeMap, HashMap, VecDeque};
17use std::net::IpAddr;
18use std::sync::atomic::{AtomicUsize, Ordering};
19use std::sync::{Arc, Mutex};
20use std::time::{Duration, Instant};
21use sui_macros::handle_fail_point_if;
22use sui_network::tonic;
23use sui_types::error::{SuiError, SuiErrorKind, SuiResult};
24use sui_types::messages_consensus::{
25 ConsensusPosition, ConsensusTransaction, ConsensusTransactionKey,
26};
27use tokio::sync::{mpsc, oneshot};
28use tracing::debug;
29
30pub struct QueueEntry {
32 pub gas_price: u64,
33 pub transactions: Vec<ConsensusTransaction>,
34 pub position_sender: oneshot::Sender<Result<Vec<ConsensusPosition>, tonic::Status>>,
35 pub submitter_client_addr: Option<IpAddr>,
36 pub enqueue_time: Instant,
37}
38
39pub enum PopAction {
41 Include,
43 Exclude,
45 Skip,
47 Stop,
49}
50
51pub trait AdmissionQueueEntry {
52 fn gas_price(&self) -> u64;
53 fn transaction_keys(&self) -> impl Iterator<Item = ConsensusTransactionKey>;
54 fn notify_evicted(self, min_gas_price: u64);
55 fn notify_rejected(self, min_gas_price: u64);
56}
57
58#[must_use = "call `notify` to deliver the outcome to the displaced entry"]
62pub struct InsertOutcome<E> {
63 outcome: Option<Outcome<E>>,
64 created_at: &'static std::panic::Location<'static>,
65}
66
67enum Outcome<E> {
68 Inserted {
69 newly_inserted: bool,
71 evicted: Option<E>,
73 gas_price: u64,
75 },
76 Rejected { entry: E, min_gas_price: u64 },
78}
79
80impl<E: AdmissionQueueEntry> InsertOutcome<E> {
81 #[track_caller]
82 fn new(outcome: Outcome<E>) -> Self {
83 Self {
84 outcome: Some(outcome),
85 created_at: std::panic::Location::caller(),
86 }
87 }
88
89 pub fn notify(mut self) -> SuiResult<bool> {
92 match self
93 .outcome
94 .take()
95 .expect("outcome is pending until notified")
96 {
97 Outcome::Inserted {
98 newly_inserted,
99 evicted,
100 gas_price,
101 } => {
102 if let Some(evicted) = evicted {
103 evicted.notify_evicted(gas_price);
104 }
105 Ok(newly_inserted)
106 }
107 Outcome::Rejected {
108 entry,
109 min_gas_price,
110 } => {
111 entry.notify_rejected(min_gas_price);
112 Err(
113 SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion {
114 min_gas_price,
115 }
116 .into(),
117 )
118 }
119 }
120 }
121}
122
123impl<E> Drop for InsertOutcome<E> {
124 fn drop(&mut self) {
125 if self.outcome.is_some() && !std::thread::panicking() {
126 debug_fatal!(
127 "InsertOutcome from try_insert at {} dropped without notify",
128 self.created_at
129 );
130 }
131 }
132}
133
134impl AdmissionQueueEntry for QueueEntry {
135 fn gas_price(&self) -> u64 {
136 self.gas_price
137 }
138
139 fn transaction_keys(&self) -> impl Iterator<Item = ConsensusTransactionKey> {
140 self.transactions.iter().map(ConsensusTransaction::key)
141 }
142
143 fn notify_evicted(self, min_gas_price: u64) {
144 let _ = self
145 .position_sender
146 .send(Err(tonic::Status::from(SuiError::from(
147 SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price },
148 ))));
149 }
150
151 fn notify_rejected(self, _min_gas_price: u64) {
152 }
155}
156
157impl QueueEntry {
158 #[cfg(test)]
159 pub fn new_for_test(
160 gas_price: u64,
161 position_sender: oneshot::Sender<Result<Vec<ConsensusPosition>, tonic::Status>>,
162 ) -> Self {
163 Self {
164 gas_price,
165 transactions: vec![],
166 position_sender,
167 submitter_client_addr: None,
168 enqueue_time: Instant::now(),
169 }
170 }
171}
172
173pub struct AdmissionQueueMetrics {
175 pub queue_depth: IntGauge,
179 pub queue_wait_latency: HistogramVec,
180 pub evictions: IntCounter,
181 pub rejections: IntCounter,
182 pub expired: IntCounter,
183 pub duplicate_inserts: IntCounter,
184
185 pub pool_depth: IntGaugeVec,
186 pub pool_bytes: IntGaugeVec,
187 pub pool_staggered_held: IntGauge,
188 pub pool_staggered_rejected: IntCounter,
189 pub pool_staggered_delay: Histogram,
190 pub pool_stagger_shadow_decisions: IntCounterVec,
191 pub pool_stagger_shadow_delay: Histogram,
192 pub pool_taken_per_proposal: Histogram,
193 pub pool_requeued_on_dropped_ack: IntCounter,
194 pub pool_gc_notified: IntCounter,
195 pub pool_waiting_inserts: IntGauge,
196 pub pool_already_processed: IntCounterVec,
197 pub pool_rejected_at_proposal: IntCounterVec,
198 pub pool_commit_latency: HistogramVec,
199 pub pool_abandoned: IntCounterVec,
200 pub pool_byte_budget_bytes: IntGauge,
201 pub pool_byte_budget_exhausted: IntCounter,
202}
203
204impl AdmissionQueueMetrics {
205 pub fn new(registry: &Registry) -> Self {
206 Self {
207 queue_depth: register_int_gauge_with_registry!(
208 "admission_queue_depth",
209 "Current number of entries in the admission priority queue",
210 registry,
211 )
212 .unwrap(),
213 queue_wait_latency: register_histogram_vec_with_registry!(
214 "admission_queue_wait_latency",
215 "Time a submission spends waiting in the admission queue or transaction pool before being drained or proposed",
216 &["lane", "proposers"],
217 mysten_metrics::SUBSECOND_LATENCY_SEC_BUCKETS.to_vec(),
218 registry,
219 )
220 .unwrap(),
221 evictions: register_int_counter_with_registry!(
222 "admission_queue_evictions",
223 "Number of entries evicted from the admission queue by higher gas price transactions",
224 registry,
225 )
226 .unwrap(),
227 rejections: register_int_counter_with_registry!(
228 "admission_queue_rejections",
229 "Number of transactions rejected because the queue was full and their gas price was too low",
230 registry,
231 )
232 .unwrap(),
233 expired: register_int_counter_with_registry!(
234 "admission_queue_expired",
235 "Number of queued transactions dropped for exceeding their time to live",
236 registry,
237 )
238 .unwrap(),
239 duplicate_inserts: register_int_counter_with_registry!(
240 "admission_queue_duplicate_inserts",
241 "Transactions admitted to the queue whose ConsensusTransactionKey duplicated an entry already present. Tallied as spam for DoS protection.",
242 registry,
243 )
244 .unwrap(),
245 pool_depth: register_int_gauge_vec_with_registry!(
246 "consensus_transaction_pool_depth",
247 "Current number of entries in each consensus transaction pool lane",
248 &["lane"],
249 registry,
250 )
251 .unwrap(),
252 pool_bytes: register_int_gauge_vec_with_registry!(
253 "consensus_transaction_pool_bytes",
254 "Current serialized transaction bytes in each consensus transaction pool lane",
255 &["lane"],
256 registry,
257 )
258 .unwrap(),
259 pool_staggered_held: register_int_gauge_with_registry!(
260 "consensus_transaction_pool_staggered_held",
261 "User-lane entries in the pool stamped with a staggered-submission hold, whether or not the hold has elapsed",
262 registry,
263 )
264 .unwrap(),
265 pool_staggered_rejected: register_int_counter_with_registry!(
266 "consensus_transaction_pool_staggered_rejected",
267 "User submissions rejected because this validator is outside the transaction's staggered-submission proposer set",
268 registry,
269 )
270 .unwrap(),
271 pool_staggered_delay: register_histogram_with_registry!(
272 "consensus_transaction_pool_staggered_delay",
273 "The staggered-submission hold applied to a user-lane entry without allowed proposers before it may be proposed",
274 mysten_metrics::SUBSECOND_LATENCY_SEC_BUCKETS.to_vec(),
275 registry,
276 )
277 .unwrap(),
278 pool_stagger_shadow_decisions: register_int_counter_vec_with_registry!(
279 "consensus_transaction_pool_stagger_shadow_decisions",
280 "Decision (free, hold or reject) staggering would have made per user-lane submission while inactive",
281 &["decision"],
282 registry,
283 )
284 .unwrap(),
285 pool_stagger_shadow_delay: register_histogram_with_registry!(
286 "consensus_transaction_pool_stagger_shadow_delay",
287 "Hold staggering would have applied to a user-lane submission while inactive",
288 mysten_metrics::SUBSECOND_LATENCY_SEC_BUCKETS.to_vec(),
289 registry,
290 )
291 .unwrap(),
292 pool_taken_per_proposal: register_histogram_with_registry!(
293 "consensus_transaction_pool_taken_per_proposal",
294 "Transactions taken from the consensus transaction pool per proposal",
295 COUNT_BUCKETS.to_vec(),
296 registry,
297 )
298 .unwrap(),
299 pool_requeued_on_dropped_ack: register_int_counter_with_registry!(
300 "consensus_transaction_pool_requeued_on_dropped_ack",
301 "Entries requeued because a proposal acknowledgement was dropped",
302 registry,
303 )
304 .unwrap(),
305 pool_gc_notified: register_int_counter_with_registry!(
306 "consensus_transaction_pool_gc_notified",
307 "Block-status subscribers notified that their block was garbage collected",
308 registry,
309 )
310 .unwrap(),
311 pool_waiting_inserts: register_int_gauge_with_registry!(
312 "consensus_transaction_pool_waiting_inserts",
313 "Pool submissions waiting for the matching epoch pool to become available",
314 registry,
315 )
316 .unwrap(),
317 pool_already_processed: register_int_counter_vec_with_registry!(
318 "consensus_transaction_pool_already_processed",
319 "User submissions not proposed because they were already processed elsewhere, by the stage that detected it and the path that processed them",
320 &["stage", "method"],
321 registry,
322 )
323 .unwrap(),
324 pool_rejected_at_proposal: register_int_counter_vec_with_registry!(
325 "consensus_transaction_pool_rejected_at_proposal",
326 "User submissions dropped at proposal time because their owned input objects no longer match live state, by error",
327 &["error"],
328 registry,
329 )
330 .unwrap(),
331 pool_commit_latency: register_histogram_vec_with_registry!(
332 "consensus_transaction_pool_commit_latency",
333 "Time from insert into the transaction pool to commit of the block that proposed the entry",
334 &["lane", "proposers"],
335 mysten_metrics::LATENCY_SEC_BUCKETS.to_vec(),
336 registry,
337 )
338 .unwrap(),
339 pool_abandoned: register_int_counter_vec_with_registry!(
340 "consensus_transaction_pool_abandoned",
341 "Pool entries dropped at proposal time because their submitter stopped waiting",
342 &["lane"],
343 registry,
344 )
345 .unwrap(),
346 pool_byte_budget_bytes: register_int_gauge_with_registry!(
347 "consensus_transaction_pool_byte_budget_bytes",
348 "User-lane bytes the rate limiter allows the next proposal to take",
349 registry,
350 )
351 .unwrap(),
352 pool_byte_budget_exhausted: register_int_counter_with_registry!(
353 "consensus_transaction_pool_byte_budget_exhausted",
354 "Proposals in which the user-lane byte rate limit stopped the take",
355 registry,
356 )
357 .unwrap(),
358 }
359 }
360
361 pub fn new_for_tests() -> Self {
362 Self::new(&Registry::new())
363 }
364}
365
366pub struct PriorityAdmissionQueue<E: AdmissionQueueEntry> {
370 capacity: usize,
371 map: BTreeMap<u64, VecDeque<E>>,
372 queued_keys: HashMap<ConsensusTransactionKey, u32>,
374 total_len: usize,
375 metrics: Arc<AdmissionQueueMetrics>,
376}
377
378impl<E: AdmissionQueueEntry> PriorityAdmissionQueue<E> {
379 pub fn new(capacity: usize, metrics: Arc<AdmissionQueueMetrics>) -> Self {
380 Self {
381 capacity,
382 map: BTreeMap::new(),
383 queued_keys: HashMap::new(),
384 total_len: 0,
385 metrics,
386 }
387 }
388
389 pub fn len(&self) -> usize {
390 self.total_len
391 }
392
393 pub fn min_gas_price(&self) -> Option<u64> {
394 self.map.first_key_value().map(|(&k, _)| k)
395 }
396
397 pub fn insert(&mut self, entry: E) -> SuiResult<bool> {
401 self.try_insert(entry).notify()
402 }
403
404 #[track_caller]
408 pub fn try_insert(&mut self, entry: E) -> InsertOutcome<E> {
409 let keys: Vec<_> = entry.transaction_keys().collect();
410 let newly_inserted = !keys.iter().any(|k| self.queued_keys.contains_key(k));
411 if !newly_inserted {
412 self.metrics.duplicate_inserts.inc();
413 }
414
415 let gas_price = entry.gas_price();
416 if self.total_len < self.capacity {
417 self.push_entry(entry, keys, false);
418 return InsertOutcome::new(Outcome::Inserted {
419 newly_inserted,
420 evicted: None,
421 gas_price,
422 });
423 }
424
425 let min_gas_price = self.min_gas_price().unwrap();
426 if gas_price > min_gas_price {
427 let evicted = self.evict_lowest();
428 self.push_entry(entry, keys, false);
429 self.metrics.evictions.inc();
430 return InsertOutcome::new(Outcome::Inserted {
431 newly_inserted,
432 evicted: Some(evicted),
433 gas_price,
434 });
435 }
436
437 self.metrics.rejections.inc();
438 InsertOutcome::new(Outcome::Rejected {
439 entry,
440 min_gas_price,
441 })
442 }
443
444 pub fn pop_batch(&mut self, count: usize) -> Vec<E> {
447 let mut remaining = count;
448 self.pop_batch_while(|_| {
449 if remaining == 0 {
450 PopAction::Stop
451 } else {
452 remaining -= 1;
453 PopAction::Include
454 }
455 })
456 .0
457 }
458
459 pub fn into_entries(mut self) -> Vec<E> {
460 let len = self.len();
461 self.pop_batch(len)
462 }
463
464 pub fn pop_batch_while(
470 &mut self,
471 mut action: impl FnMut(&mut E) -> PopAction,
472 ) -> (Vec<E>, Vec<E>) {
473 let mut included = Vec::new();
474 let mut excluded = Vec::new();
475 let mut stopped = false;
476 for deque in self.map.values_mut().rev() {
477 if stopped {
478 break;
479 }
480 let mut source = std::mem::take(deque);
485 while let Some(mut entry) = source.pop_front() {
486 if stopped {
487 deque.push_back(entry);
488 continue;
489 }
490 match action(&mut entry) {
491 PopAction::Stop => {
492 stopped = true;
493 deque.push_back(entry);
494 }
495 PopAction::Skip => deque.push_back(entry),
496 PopAction::Include => {
497 self.total_len -= 1;
498 included.push(entry);
499 }
500 PopAction::Exclude => {
501 self.total_len -= 1;
502 excluded.push(entry);
503 }
504 }
505 }
506 }
507 self.map.retain(|_, deque| !deque.is_empty());
508 for entry in included.iter().chain(&excluded) {
509 self.remove_keys(entry);
510 }
511 self.metrics.queue_depth.set(self.total_len as i64);
512 (included, excluded)
513 }
514
515 pub fn pop_front_while(&mut self, mut pred: impl FnMut(&mut E) -> bool) -> Vec<E> {
520 let mut popped = Vec::new();
521 for deque in self.map.values_mut() {
522 while let Some(front) = deque.front_mut()
523 && pred(front)
524 {
525 popped.push(deque.pop_front().expect("front was just observed"));
526 }
527 }
528 if popped.is_empty() {
529 return popped;
530 }
531 self.total_len -= popped.len();
532 for entry in &popped {
533 self.remove_keys(entry);
534 }
535 self.map.retain(|_, deque| !deque.is_empty());
536 self.metrics.queue_depth.set(self.total_len as i64);
537 popped
538 }
539
540 pub fn is_empty(&self) -> bool {
541 self.total_len == 0
542 }
543
544 pub fn reinsert_front(&mut self, entry: E) {
545 let keys: Vec<_> = entry.transaction_keys().collect();
546 self.push_entry(entry, keys, true);
547 }
548
549 fn push_entry(&mut self, entry: E, keys: Vec<ConsensusTransactionKey>, front: bool) {
550 for key in keys {
551 *self.queued_keys.entry(key).or_insert(0) += 1;
552 }
553 let level = self.map.entry(entry.gas_price()).or_default();
554 if front {
555 level.push_front(entry);
556 } else {
557 level.push_back(entry);
558 }
559 self.total_len += 1;
560 self.metrics.queue_depth.set(self.total_len as i64);
561 }
562
563 fn evict_lowest(&mut self) -> E {
564 let evicted = {
565 let mut first = self
566 .map
567 .first_entry()
568 .expect("evict_lowest called on empty queue");
569 let deque = first.get_mut();
570 let evicted = deque.pop_front().unwrap();
571 if deque.is_empty() {
572 first.remove();
573 }
574 evicted
575 };
576 self.remove_keys(&evicted);
577 self.total_len -= 1;
578 evicted
579 }
580
581 fn remove_keys(&mut self, entry: &E) {
582 for key in entry.transaction_keys() {
583 let std::collections::hash_map::Entry::Occupied(mut slot) = self.queued_keys.entry(key)
584 else {
585 debug_fatal!("remove_keys on absent key");
586 continue;
587 };
588 *slot.get_mut() -= 1;
589 if *slot.get() == 0 {
590 slot.remove();
591 }
592 }
593 }
594}
595
596struct InsertCommand {
598 entry: QueueEntry,
599 response: oneshot::Sender<SuiResult<bool>>,
600}
601
602#[derive(Clone)]
605pub struct AdmissionQueueHandle {
606 sender: mpsc::Sender<InsertCommand>,
607 last_drain: Arc<Mutex<Instant>>,
609 queue_depth: Arc<AtomicUsize>,
610 failover_timeout: Duration,
611}
612
613impl AdmissionQueueHandle {
614 pub fn failover_tripped(&self) -> bool {
618 if self.queue_depth.load(Ordering::Relaxed) == 0 {
619 return false;
620 }
621 self.last_drain.lock().unwrap().elapsed() > self.failover_timeout
622 }
623
624 pub async fn try_insert(
627 &self,
628 gas_price: u64,
629 transactions: Vec<ConsensusTransaction>,
630 submitter_client_addr: Option<IpAddr>,
631 ) -> SuiResult<(
632 oneshot::Receiver<Result<Vec<ConsensusPosition>, tonic::Status>>,
633 bool,
634 )> {
635 let (position_tx, position_rx) = oneshot::channel();
636 let entry = QueueEntry {
637 gas_price,
638 transactions,
639 position_sender: position_tx,
640 submitter_client_addr,
641 enqueue_time: Instant::now(),
642 };
643
644 let (resp_tx, resp_rx) = oneshot::channel();
645 let cmd = InsertCommand {
646 entry,
647 response: resp_tx,
648 };
649
650 self.sender
651 .send(cmd)
652 .await
653 .map_err(|_| SuiError::from(SuiErrorKind::TooManyTransactionsPendingConsensus))?;
654
655 let newly_inserted = resp_rx
656 .await
657 .map_err(|_| SuiError::from(SuiErrorKind::TooManyTransactionsPendingConsensus))??;
658
659 Ok((position_rx, newly_inserted))
660 }
661}
662
663pub struct AdmissionQueueManager {
667 capacity: usize,
668 failover_timeout: Duration,
669 metrics: Arc<AdmissionQueueMetrics>,
670 consensus_adapter: Arc<ConsensusAdapter>,
671 slot_freed_notify: Arc<tokio::sync::Notify>,
672}
673
674impl AdmissionQueueManager {
675 pub fn new(
676 consensus_adapter: Arc<ConsensusAdapter>,
677 metrics: Arc<AdmissionQueueMetrics>,
678 capacity_fraction: f64,
679 failover_timeout: Duration,
680 slot_freed_notify: Arc<tokio::sync::Notify>,
681 ) -> Self {
682 let max_pending = consensus_adapter.max_pending_transactions();
683 let capacity = (max_pending as f64 * capacity_fraction) as usize;
684 assert!(
685 capacity > 0,
686 "admission_queue_capacity_fraction ({capacity_fraction}) * max_pending_transactions ({max_pending}) must be > 0"
687 );
688 Self {
689 capacity,
690 failover_timeout,
691 metrics,
692 consensus_adapter,
693 slot_freed_notify,
694 }
695 }
696
697 pub fn new_for_tests(
698 consensus_adapter: Arc<ConsensusAdapter>,
699 slot_freed_notify: Arc<tokio::sync::Notify>,
700 ) -> Self {
701 Self {
702 capacity: 10_000,
703 failover_timeout: Duration::from_secs(30),
704 metrics: Arc::new(AdmissionQueueMetrics::new_for_tests()),
705 consensus_adapter,
706 slot_freed_notify,
707 }
708 }
709
710 pub fn metrics(&self) -> &Arc<AdmissionQueueMetrics> {
711 &self.metrics
712 }
713
714 pub fn spawn(&self, epoch_store: Arc<AuthorityPerEpochStore>) -> AdmissionQueueHandle {
717 let last_drain = Arc::new(Mutex::new(Instant::now()));
718 let queue_depth = Arc::new(AtomicUsize::new(0));
719
720 let (sender, receiver) = mpsc::channel(self.capacity.max(1024));
721
722 let event_loop = AdmissionQueueEventLoop {
723 receiver,
724 queue: PriorityAdmissionQueue::new(self.capacity, self.metrics.clone()),
725 consensus_adapter: self.consensus_adapter.clone(),
726 slot_freed_notify: self.slot_freed_notify.clone(),
727 epoch_store,
728 last_drain: last_drain.clone(),
729 queue_depth: queue_depth.clone(),
730 last_published_depth: 0,
731 };
732 spawn_monitored_task!(event_loop.run());
733
734 AdmissionQueueHandle {
735 sender,
736 last_drain,
737 queue_depth,
738 failover_timeout: self.failover_timeout,
739 }
740 }
741}
742
743#[derive(Clone)]
749pub struct AdmissionQueueContext {
750 manager: Arc<AdmissionQueueManager>,
751 swap: Arc<ArcSwap<AdmissionQueueHandle>>,
752}
753
754impl AdmissionQueueContext {
755 pub fn spawn(
756 manager: Arc<AdmissionQueueManager>,
757 epoch_store: Arc<AuthorityPerEpochStore>,
758 ) -> Self {
759 let initial_handle = manager.spawn(epoch_store);
760 let swap = Arc::new(ArcSwap::new(Arc::new(initial_handle)));
761 Self { manager, swap }
762 }
763
764 pub fn rotate_for_epoch(&self, epoch_store: Arc<AuthorityPerEpochStore>) {
767 self.swap.store(Arc::new(self.manager.spawn(epoch_store)));
768 }
769
770 pub(crate) fn load(&self) -> arc_swap::Guard<Arc<AdmissionQueueHandle>> {
771 self.swap.load()
772 }
773}
774
775struct AdmissionQueueEventLoop {
778 receiver: mpsc::Receiver<InsertCommand>,
779 queue: PriorityAdmissionQueue<QueueEntry>,
780 consensus_adapter: Arc<ConsensusAdapter>,
781 slot_freed_notify: Arc<tokio::sync::Notify>,
782 epoch_store: Arc<AuthorityPerEpochStore>,
783 last_drain: Arc<Mutex<Instant>>,
784 queue_depth: Arc<AtomicUsize>,
785 last_published_depth: usize,
786}
787
788impl AdmissionQueueEventLoop {
789 pub async fn run(mut self) {
790 loop {
791 self.process_pending_inserts();
792 self.publish_queue_depth();
793
794 if !handle_fail_point_if("admission_queue_disable_drain")
795 && !self.queue.is_empty()
796 && self.has_consensus_capacity()
797 {
798 self.drain_batch();
799 self.publish_queue_depth();
800 continue;
801 }
802
803 if self.queue.is_empty() {
804 match self.receiver.recv().await {
806 Some(cmd) => self.handle_insert(cmd),
807 None => {
808 debug!("Admission queue actor shutting down");
809 break;
810 }
811 }
812 continue;
813 }
814
815 let notify = self.slot_freed_notify.clone();
820 let slot_freed = notify.notified();
821 tokio::pin!(slot_freed);
822
823 self.process_pending_inserts();
824 if !handle_fail_point_if("admission_queue_disable_drain")
825 && !self.queue.is_empty()
826 && self.has_consensus_capacity()
827 {
828 continue;
829 }
830
831 tokio::select! {
832 biased;
833
834 result = self.receiver.recv() => {
835 match result {
836 Some(cmd) => self.handle_insert(cmd),
837 None => {
838 debug!("Admission queue actor shutting down");
839 break;
840 }
841 }
842 }
843
844 _ = &mut slot_freed => {}
845 }
846 }
847 }
848
849 fn publish_queue_depth(&mut self) {
850 let len = self.queue.len();
851 if len != self.last_published_depth {
852 self.queue_depth.store(len, Ordering::Relaxed);
853 self.last_published_depth = len;
854 }
855 }
856
857 fn process_pending_inserts(&mut self) {
858 while let Ok(cmd) = self.receiver.try_recv() {
859 self.handle_insert(cmd);
860 }
861 }
862
863 fn has_consensus_capacity(&self) -> bool {
864 self.consensus_adapter.num_inflight_transactions()
865 < u64::try_from(self.consensus_adapter.max_pending_transactions()).unwrap()
866 }
867
868 fn drain_batch(&mut self) {
869 let max_pending = u64::try_from(self.consensus_adapter.max_pending_transactions()).unwrap();
870 let available =
871 max_pending.saturating_sub(self.consensus_adapter.num_inflight_transactions());
872 let entries = self.queue.pop_batch(usize::try_from(available).unwrap());
873 if entries.is_empty() {
874 return;
875 }
876 for entry in entries {
877 let proposers = proposers_metric_label(&entry.transactions, &self.epoch_store);
878 self.queue
879 .metrics
880 .queue_wait_latency
881 .with_label_values(&["user", proposers])
882 .observe(entry.enqueue_time.elapsed().as_secs_f64());
883 let adapter = self.consensus_adapter.clone();
884 let es = self.epoch_store.clone();
885 spawn_monitored_task!(submit_queue_entry(entry, adapter, es));
886 }
887 *self.last_drain.lock().unwrap() = Instant::now();
888 }
889
890 fn handle_insert(&mut self, cmd: InsertCommand) {
891 let _ = cmd.response.send(self.queue.insert(cmd.entry));
892 }
893}
894
895async fn submit_queue_entry(
896 entry: QueueEntry,
897 consensus_adapter: Arc<ConsensusAdapter>,
898 epoch_store: Arc<AuthorityPerEpochStore>,
899) {
900 let _ = entry.position_sender.send(
901 consensus_adapter
902 .submit_and_get_positions(
903 entry.transactions,
904 &epoch_store,
905 entry.submitter_client_addr,
906 )
907 .await
908 .map_err(tonic::Status::from),
909 );
910}
911
912#[cfg(test)]
913mod tests {
914 use super::*;
915 use sui_macros::sim_test;
916
917 fn make_test_entry(
918 gas_price: u64,
919 ) -> (
920 QueueEntry,
921 oneshot::Receiver<Result<Vec<ConsensusPosition>, tonic::Status>>,
922 ) {
923 let (tx, rx) = oneshot::channel();
924 (QueueEntry::new_for_test(gas_price, tx), rx)
925 }
926
927 #[cfg(debug_assertions)]
928 #[test]
929 #[should_panic(expected = "dropped without notify")]
930 fn dropped_insert_outcome_without_notify_panics() {
931 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
932 let mut q = PriorityAdmissionQueue::new(1, metrics);
933 let (entry, _rx) = make_test_entry(100);
934 let _ = q.try_insert(entry);
935 }
936
937 #[test]
938 fn try_insert_notify_matches_insert() {
939 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
940 let mut q = PriorityAdmissionQueue::new(1, metrics);
941 let (low, mut low_rx) = make_test_entry(100);
942 let (high, _) = make_test_entry(200);
943 let (rejected, mut rejected_rx) = make_test_entry(50);
944
945 assert!(q.try_insert(low).notify().unwrap());
946 let outcome = q.try_insert(high);
948 assert!(low_rx.try_recv().is_err());
949 assert!(outcome.notify().unwrap());
950 assert!(matches!(
951 low_rx.try_recv(),
952 Ok(Err(status)) if status.message().contains("minimum gas price required: 200")
953 ));
954
955 assert!(matches!(
956 q.try_insert(rejected).notify().unwrap_err().as_inner(),
957 SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price: 200 }
958 ));
959 assert_eq!(q.len(), 1);
960 assert!(rejected_rx.try_recv().is_err());
961 }
962
963 fn entry_at(gas_price: u64, enqueue_time: Instant) -> QueueEntry {
964 let mut entry = make_test_entry(gas_price).0;
965 entry.enqueue_time = enqueue_time;
966 entry
967 }
968
969 fn expired_at(now: Instant, ttl: Duration) -> impl FnMut(&mut QueueEntry) -> bool {
970 move |entry| now.saturating_duration_since(entry.enqueue_time) >= ttl
971 }
972
973 fn remaining(q: &mut PriorityAdmissionQueue<QueueEntry>) -> Vec<(u64, Instant)> {
974 let len = q.len();
975 q.pop_batch(len)
976 .into_iter()
977 .map(|entry| (entry.gas_price, entry.enqueue_time))
978 .collect()
979 }
980
981 #[test]
982 fn pop_front_while_drops_aged_entries_and_their_keys() {
983 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
984 let mut q = PriorityAdmissionQueue::new(10, metrics.clone());
985 let now = Instant::now();
986 let mut keyed = entry_at(100, now);
987 keyed.transactions = vec![ConsensusTransaction::new_end_of_publish(
988 sui_types::base_types::AuthorityName::ZERO,
989 )];
990 q.insert(keyed).unwrap();
991 q.insert(entry_at(200, now)).unwrap();
992
993 let popped = q.pop_front_while(expired_at(
994 now + Duration::from_secs(120),
995 Duration::from_secs(60),
996 ));
997 assert_eq!(popped.len(), 2);
998 assert_eq!(q.len(), 0);
999 assert_eq!(metrics.queue_depth.get(), 0);
1000 assert!(q.queued_keys.is_empty());
1001 assert!(q.map.is_empty());
1002 }
1003
1004 #[test]
1005 fn pop_front_while_keeps_fresh_entries() {
1006 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1007 let mut q = PriorityAdmissionQueue::new(10, metrics);
1008 let now = Instant::now();
1009 q.insert(entry_at(100, now)).unwrap();
1010 q.insert(entry_at(200, now)).unwrap();
1011
1012 let popped = q.pop_front_while(expired_at(now, Duration::from_secs(60)));
1013 assert!(popped.is_empty());
1014 assert_eq!(q.len(), 2);
1015 }
1016
1017 #[test]
1018 fn pop_front_while_pops_the_aged_prefix_of_every_level() {
1019 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1020 let mut q = PriorityAdmissionQueue::new(10, metrics);
1021 let now = Instant::now();
1022 let ttl = Duration::from_secs(60);
1023 let fresh = now + Duration::from_secs(120);
1024 q.insert(entry_at(100, fresh)).unwrap();
1027 q.insert(entry_at(200, now)).unwrap();
1029 q.insert(entry_at(200, now + Duration::from_secs(1)))
1030 .unwrap();
1031 q.insert(entry_at(200, fresh)).unwrap();
1032 q.insert(entry_at(300, fresh)).unwrap();
1034 q.insert(entry_at(300, now)).unwrap();
1035
1036 let popped = q.pop_front_while(expired_at(now + ttl + Duration::from_secs(1), ttl));
1037 let popped: Vec<_> = popped
1038 .into_iter()
1039 .map(|entry| (entry.gas_price, entry.enqueue_time))
1040 .collect();
1041 assert_eq!(
1042 popped,
1043 vec![(200, now), (200, now + Duration::from_secs(1))]
1044 );
1045 assert_eq!(
1046 remaining(&mut q),
1047 vec![(300, fresh), (300, now), (200, fresh), (100, fresh)]
1048 );
1049 }
1050
1051 fn build_queue(capacity: usize, gas_prices: &[u64]) -> PriorityAdmissionQueue<QueueEntry> {
1052 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1053 let mut q = PriorityAdmissionQueue::new(capacity, metrics);
1054 for &gp in gas_prices {
1055 let (entry, _) = make_test_entry(gp);
1056 q.insert(entry).unwrap();
1057 }
1058 q
1059 }
1060
1061 #[test]
1062 fn test_insert_within_capacity() {
1063 let q = build_queue(3, &[100, 200, 50]);
1064 assert_eq!(q.len(), 3);
1065 }
1066
1067 #[test]
1068 fn test_pop_batch_while_partitions_and_stops() {
1069 let mut q = build_queue(10, &[100, 200, 200, 50, 75]);
1070
1071 let (included, excluded) = q.pop_batch_while(|entry| match entry.gas_price {
1072 price if price < 80 => PopAction::Stop,
1073 price if price < 150 => PopAction::Exclude,
1074 _ => PopAction::Include,
1075 });
1076 assert_eq!(
1077 included.iter().map(|e| e.gas_price).collect::<Vec<_>>(),
1078 vec![200, 200]
1079 );
1080 assert_eq!(
1081 excluded.iter().map(|e| e.gas_price).collect::<Vec<_>>(),
1082 vec![100]
1083 );
1084 assert_eq!(q.len(), 2);
1086 assert_eq!(q.min_gas_price(), Some(50));
1087 let (rest, _) = q.pop_batch_while(|_| PopAction::Include);
1088 assert_eq!(rest.len(), 2);
1089 assert!(q.is_empty());
1090 }
1091
1092 #[test]
1093 fn test_pop_batch_while_skips_and_retains() {
1094 let mut q = build_queue(10, &[100, 200, 200, 50, 75]);
1095
1096 let mut seen_200 = false;
1099 let (included, excluded) = q.pop_batch_while(|entry| match entry.gas_price {
1100 200 if !seen_200 => {
1101 seen_200 = true;
1102 PopAction::Skip
1103 }
1104 100 => PopAction::Skip,
1105 75 => PopAction::Exclude,
1106 _ => PopAction::Include,
1107 });
1108 assert_eq!(
1109 included.iter().map(|e| e.gas_price).collect::<Vec<_>>(),
1110 vec![200, 50]
1111 );
1112 assert_eq!(
1113 excluded.iter().map(|e| e.gas_price).collect::<Vec<_>>(),
1114 vec![75]
1115 );
1116 assert_eq!(q.len(), 2);
1118 let (rest, _) = q.pop_batch_while(|_| PopAction::Include);
1119 assert_eq!(
1120 rest.iter().map(|e| e.gas_price).collect::<Vec<_>>(),
1121 vec![200, 100]
1122 );
1123 assert!(q.is_empty());
1124 }
1125
1126 #[test]
1127 fn test_pop_batch_while_skip_then_stop() {
1128 let mut q = build_queue(10, &[300, 200, 100]);
1131
1132 let (included, excluded) = q.pop_batch_while(|entry| match entry.gas_price {
1133 300 => PopAction::Skip,
1134 200 => PopAction::Include,
1135 _ => PopAction::Stop,
1136 });
1137 assert_eq!(
1138 included.iter().map(|e| e.gas_price).collect::<Vec<_>>(),
1139 vec![200]
1140 );
1141 assert!(excluded.is_empty());
1142 assert_eq!(q.len(), 2);
1143 assert_eq!(q.min_gas_price(), Some(100));
1144 let (rest, _) = q.pop_batch_while(|_| PopAction::Include);
1145 assert_eq!(
1146 rest.iter().map(|e| e.gas_price).collect::<Vec<_>>(),
1147 vec![300, 100]
1148 );
1149 assert!(q.is_empty());
1150 }
1151
1152 #[test]
1153 fn test_pop_batch_while_fully_skipped_level_retained() {
1154 let mut q = build_queue(10, &[200, 200]);
1155
1156 let (included, excluded) = q.pop_batch_while(|_| PopAction::Skip);
1157 assert!(included.is_empty() && excluded.is_empty());
1158 assert_eq!(q.len(), 2);
1161 assert_eq!(q.min_gas_price(), Some(200));
1162 let (rest, _) = q.pop_batch_while(|_| PopAction::Include);
1163 assert_eq!(
1164 rest.iter().map(|e| e.gas_price).collect::<Vec<_>>(),
1165 vec![200, 200]
1166 );
1167 assert!(q.is_empty());
1168 }
1169
1170 #[test]
1171 fn test_eviction_when_full() {
1172 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1173 let mut q = PriorityAdmissionQueue::new(2, metrics);
1174
1175 let (e1, mut r1) = make_test_entry(100);
1176 let (e2, _) = make_test_entry(200);
1177 let (e3, _) = make_test_entry(300);
1178
1179 q.insert(e1).unwrap();
1180 q.insert(e2).unwrap();
1181 assert_eq!(q.len(), 2);
1182
1183 assert!(q.insert(e3).is_ok());
1184 assert_eq!(q.len(), 2);
1185 let r1_result = r1.try_recv().expect("evicted entry must be signalled");
1187 assert!(matches!(r1_result, Err(ref status) if status.message().contains("outbid")));
1188 }
1189
1190 #[test]
1191 fn test_rejection_when_full_and_low_price() {
1192 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1193 let mut q = PriorityAdmissionQueue::new(2, metrics);
1194
1195 let (e1, _) = make_test_entry(100);
1196 let (e2, _) = make_test_entry(200);
1197 let (e3, mut r3) = make_test_entry(50);
1198
1199 q.insert(e1).unwrap();
1200 q.insert(e2).unwrap();
1201
1202 assert!(matches!(
1203 q.insert(e3).unwrap_err().as_inner(),
1204 SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price: 100 }
1205 ));
1206 assert_eq!(q.len(), 2);
1207 assert!(r3.try_recv().is_err());
1208 }
1209
1210 #[test]
1211 fn test_pop_batch() {
1212 let mut q = build_queue(5, &[100, 300, 200]);
1213 let batch = q.pop_batch(2);
1214 assert_eq!(batch.len(), 2);
1215 assert_eq!(q.len(), 1);
1216 }
1217
1218 #[test]
1219 fn test_min_gas_price() {
1220 let q = build_queue(5, &[200, 100, 300]);
1221 assert_eq!(q.min_gas_price(), Some(100));
1222 }
1223
1224 #[test]
1225 fn test_gasless_tx_evicted_first() {
1226 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1227 let mut q = PriorityAdmissionQueue::new(2, metrics);
1228
1229 let (gasless, mut r_gasless) = make_test_entry(0);
1230 let (normal, _) = make_test_entry(1000);
1231 let (high, _) = make_test_entry(2000);
1232
1233 q.insert(gasless).unwrap();
1234 q.insert(normal).unwrap();
1235
1236 assert!(q.insert(high).is_ok());
1237 let gasless_result = r_gasless
1238 .try_recv()
1239 .expect("evicted gasless entry must be signalled");
1240 assert!(matches!(gasless_result, Err(ref status) if status.message().contains("outbid")));
1241 assert_eq!(q.min_gas_price(), Some(1000));
1242 }
1243
1244 #[test]
1245 fn test_pop_batch_returns_highest_gas_price_first() {
1246 let mut q = build_queue(5, &[100, 500, 200, 400, 300]);
1247 let batch = q.pop_batch(5);
1248 let gas_prices: Vec<u64> = batch.iter().map(|e| e.gas_price).collect();
1249 assert_eq!(gas_prices, vec![500, 400, 300, 200, 100]);
1250 }
1251
1252 #[test]
1253 fn test_equal_gas_price_rejected_when_full() {
1254 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1255 let mut q = PriorityAdmissionQueue::new(1, metrics);
1256
1257 let (e1, _) = make_test_entry(100);
1258 let (e2, _) = make_test_entry(100);
1259
1260 q.insert(e1).unwrap();
1261 assert!(matches!(
1262 q.insert(e2).unwrap_err().as_inner(),
1263 SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price: 100 }
1264 ));
1265 }
1266
1267 fn make_dup_entry(
1268 gas_price: u64,
1269 tx: ConsensusTransaction,
1270 ) -> (
1271 QueueEntry,
1272 oneshot::Receiver<Result<Vec<ConsensusPosition>, tonic::Status>>,
1273 ) {
1274 let (position_tx, position_rx) = oneshot::channel();
1275 let entry = QueueEntry {
1276 gas_price,
1277 transactions: vec![tx],
1278 position_sender: position_tx,
1279 submitter_client_addr: None,
1280 enqueue_time: Instant::now(),
1281 };
1282 (entry, position_rx)
1283 }
1284
1285 #[test]
1286 fn test_duplicate_transaction_admitted_and_flagged() {
1287 use sui_types::base_types::AuthorityName;
1288
1289 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1290 let mut q = PriorityAdmissionQueue::new(10, metrics);
1291
1292 let tx = ConsensusTransaction::new_end_of_publish(AuthorityName::ZERO);
1293
1294 let (entry1, _rx1) = make_dup_entry(100, tx.clone());
1295 assert!(q.insert(entry1).unwrap());
1296 assert_eq!(q.len(), 1);
1297
1298 let (entry2, _rx2) = make_dup_entry(100, tx.clone());
1301 assert!(!q.insert(entry2).unwrap());
1302 assert_eq!(q.len(), 2);
1303 }
1304
1305 #[test]
1306 fn test_duplicate_key_counter_decrements_on_pop() {
1307 use sui_types::base_types::AuthorityName;
1308
1309 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1310 let mut q = PriorityAdmissionQueue::new(10, metrics);
1311
1312 let tx = ConsensusTransaction::new_end_of_publish(AuthorityName::ZERO);
1313
1314 let (entry1, _rx1) = make_dup_entry(100, tx.clone());
1316 q.insert(entry1).unwrap();
1317 let (entry2, _rx2) = make_dup_entry(100, tx.clone());
1318 assert!(!q.insert(entry2).unwrap());
1319
1320 let batch = q.pop_batch(1);
1323 assert_eq!(batch.len(), 1);
1324 let (entry3, _rx3) = make_dup_entry(100, tx.clone());
1325 assert!(!q.insert(entry3).unwrap());
1326 assert_eq!(q.len(), 2);
1327
1328 let _ = q.pop_batch(q.len());
1331 assert!(q.is_empty());
1332 let (entry4, _rx4) = make_dup_entry(100, tx);
1333 assert!(q.insert(entry4).unwrap());
1334 }
1335
1336 #[test]
1337 fn test_duplicate_key_counter_decrements_on_evict() {
1338 use sui_types::base_types::AuthorityName;
1339
1340 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1341 let mut q = PriorityAdmissionQueue::new(2, metrics);
1342
1343 let tx = ConsensusTransaction::new_end_of_publish(AuthorityName::ZERO);
1344
1345 let (entry1, _rx1) = make_dup_entry(100, tx.clone());
1347 q.insert(entry1).unwrap();
1348 let (entry2, _rx2) = make_dup_entry(100, tx.clone());
1349 q.insert(entry2).unwrap();
1350 assert_eq!(q.len(), 2);
1351
1352 let (filler, _) = make_test_entry(200);
1354 q.insert(filler).unwrap();
1355 assert_eq!(q.len(), 2);
1356
1357 let (filler2, _) = make_test_entry(300);
1361 q.insert(filler2).unwrap();
1362
1363 let (entry3, _rx3) = make_dup_entry(500, tx);
1364 assert!(q.insert(entry3).unwrap());
1365 }
1366
1367 #[test]
1368 fn test_duplicate_key_counter_restored_on_reinsert_front() {
1369 use sui_types::base_types::AuthorityName;
1370
1371 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1372 let mut q = PriorityAdmissionQueue::new(10, metrics);
1373
1374 let tx = ConsensusTransaction::new_end_of_publish(AuthorityName::ZERO);
1375
1376 let (entry1, _rx1) = make_dup_entry(100, tx.clone());
1377 q.insert(entry1).unwrap();
1378
1379 let popped = q.pop_batch(1).pop().unwrap();
1382 q.reinsert_front(popped);
1383 assert_eq!(q.len(), 1);
1384 let (entry2, _rx2) = make_dup_entry(100, tx.clone());
1385 assert!(!q.insert(entry2).unwrap());
1386
1387 let _ = q.pop_batch(q.len());
1389 assert!(q.is_empty());
1390 let (entry3, _rx3) = make_dup_entry(100, tx);
1391 assert!(q.insert(entry3).unwrap());
1392 }
1393
1394 #[sim_test]
1395 async fn test_actor_shuts_down_when_handle_dropped() {
1396 use crate::authority::test_authority_builder::TestAuthorityBuilder;
1397 use crate::checkpoints::CheckpointStore;
1398 use crate::consensus_adapter::ConsensusAdapterMetrics;
1399 use crate::mysticeti_adapter::LazyMysticetiClient;
1400 use sui_types::base_types::AuthorityName;
1401
1402 let state = TestAuthorityBuilder::new().build().await;
1403 let epoch_store = state.epoch_store_for_testing().clone();
1404 let consensus_adapter = Arc::new(ConsensusAdapter::new(
1405 Arc::new(LazyMysticetiClient::new()),
1406 CheckpointStore::new_for_tests(),
1407 AuthorityName::ZERO,
1408 100_000,
1409 100_000,
1410 ConsensusAdapterMetrics::new_test(),
1411 Arc::new(tokio::sync::Notify::new()),
1412 ));
1413
1414 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1415 let (sender, receiver) = mpsc::channel(100);
1416 let slot_freed_notify = Arc::new(tokio::sync::Notify::new());
1417
1418 let event_loop = AdmissionQueueEventLoop {
1419 receiver,
1420 queue: PriorityAdmissionQueue::new(100, metrics),
1421 consensus_adapter,
1422 slot_freed_notify,
1423 epoch_store,
1424 last_drain: Arc::new(Mutex::new(Instant::now())),
1425 queue_depth: Arc::new(AtomicUsize::new(0)),
1426 last_published_depth: 0,
1427 };
1428
1429 let handle = tokio::spawn(event_loop.run());
1430
1431 drop(sender);
1433
1434 tokio::time::timeout(std::time::Duration::from_secs(5), handle)
1436 .await
1437 .expect("actor did not shut down within timeout")
1438 .expect("actor task panicked");
1439 }
1440
1441 async fn build_consensus_adapter(
1442 max_pending_transactions: usize,
1443 ) -> (
1444 Arc<ConsensusAdapter>,
1445 Arc<AuthorityPerEpochStore>,
1446 Arc<tokio::sync::Notify>,
1447 ) {
1448 use crate::authority::test_authority_builder::TestAuthorityBuilder;
1449 use crate::checkpoints::CheckpointStore;
1450 use crate::consensus_adapter::ConsensusAdapterMetrics;
1451 use crate::mysticeti_adapter::LazyMysticetiClient;
1452 use sui_types::base_types::AuthorityName;
1453
1454 let state = TestAuthorityBuilder::new().build().await;
1455 let epoch_store = state.epoch_store_for_testing().clone();
1456 let slot_freed_notify = Arc::new(tokio::sync::Notify::new());
1457 let adapter = Arc::new(ConsensusAdapter::new(
1458 Arc::new(LazyMysticetiClient::new()),
1459 CheckpointStore::new_for_tests(),
1460 AuthorityName::ZERO,
1461 max_pending_transactions,
1462 100_000,
1463 ConsensusAdapterMetrics::new_test(),
1464 slot_freed_notify.clone(),
1465 ));
1466 (adapter, epoch_store, slot_freed_notify)
1467 }
1468
1469 #[tokio::test]
1470 async fn test_failover_tripped_when_actor_stalls() {
1471 let handle = AdmissionQueueHandle {
1474 sender: mpsc::channel(1).0,
1475 last_drain: Arc::new(Mutex::new(Instant::now())),
1476 queue_depth: Arc::new(AtomicUsize::new(1)),
1477 failover_timeout: Duration::from_millis(10),
1478 };
1479 assert!(!handle.failover_tripped());
1480 tokio::time::sleep(Duration::from_millis(30)).await;
1481 assert!(handle.failover_tripped());
1482
1483 handle.queue_depth.store(0, Ordering::Relaxed);
1485 assert!(!handle.failover_tripped());
1486 }
1487
1488 #[sim_test]
1489 async fn test_idle_actor_does_not_trip_failover() {
1490 let (adapter, epoch_store, notify) = build_consensus_adapter(100_000).await;
1493 let manager = AdmissionQueueManager::new(
1494 adapter,
1495 Arc::new(AdmissionQueueMetrics::new_for_tests()),
1496 0.5,
1497 Duration::from_millis(10),
1498 notify,
1499 );
1500 let handle = manager.spawn(epoch_store);
1501 tokio::time::sleep(Duration::from_millis(50)).await;
1502 assert!(
1503 !handle.failover_tripped(),
1504 "idle actor with empty queue must not trip failover"
1505 );
1506 }
1507
1508 #[sim_test]
1514 async fn test_drain_batch_does_not_bump_last_drain_when_no_slots() {
1515 let (adapter, epoch_store, notify) = build_consensus_adapter(0).await;
1516 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
1517 let (_sender, receiver) = mpsc::channel(10);
1518
1519 let mut queue = PriorityAdmissionQueue::new(10, metrics.clone());
1520 let (entry, _rx) = make_test_entry(100);
1521 assert!(queue.insert(entry).is_ok());
1522 assert_eq!(queue.len(), 1);
1523
1524 let last_drain = Arc::new(Mutex::new(Instant::now()));
1525 let before = *last_drain.lock().unwrap();
1526
1527 let mut event_loop = AdmissionQueueEventLoop {
1528 receiver,
1529 queue,
1530 consensus_adapter: adapter,
1531 slot_freed_notify: notify,
1532 epoch_store,
1533 last_drain: last_drain.clone(),
1534 queue_depth: Arc::new(AtomicUsize::new(0)),
1535 last_published_depth: 0,
1536 };
1537
1538 tokio::time::sleep(Duration::from_millis(20)).await;
1541
1542 event_loop.drain_batch();
1543
1544 assert_eq!(event_loop.queue.len(), 1, "no entries should be drained");
1545 assert_eq!(
1546 *last_drain.lock().unwrap(),
1547 before,
1548 "last_drain must not advance when drain_batch drained nothing"
1549 );
1550 }
1551}