1use crate::authority::authority_per_epoch_store::AuthorityPerEpochStore;
5use crate::consensus_adapter::ConsensusAdapter;
6use arc_swap::ArcSwap;
7use mysten_common::debug_fatal;
8use mysten_metrics::spawn_monitored_task;
9use prometheus::{
10 Histogram, IntCounter, IntGauge, Registry, register_histogram_with_registry,
11 register_int_counter_with_registry, register_int_gauge_with_registry,
12};
13use std::collections::{BTreeMap, HashMap, VecDeque};
14use std::net::IpAddr;
15use std::sync::atomic::{AtomicUsize, Ordering};
16use std::sync::{Arc, Mutex};
17use std::time::{Duration, Instant};
18use sui_macros::handle_fail_point_if;
19use sui_network::tonic;
20use sui_types::error::{SuiError, SuiErrorKind, SuiResult};
21use sui_types::messages_consensus::{
22 ConsensusPosition, ConsensusTransaction, ConsensusTransactionKey,
23};
24use tokio::sync::{mpsc, oneshot};
25use tracing::debug;
26
27pub struct QueueEntry {
29 pub gas_price: u64,
30 pub transactions: Vec<ConsensusTransaction>,
31 pub position_sender: oneshot::Sender<Result<Vec<ConsensusPosition>, tonic::Status>>,
32 pub submitter_client_addr: Option<IpAddr>,
33 pub enqueue_time: Instant,
34}
35
36impl QueueEntry {
37 #[cfg(test)]
38 pub fn new_for_test(
39 gas_price: u64,
40 position_sender: oneshot::Sender<Result<Vec<ConsensusPosition>, tonic::Status>>,
41 ) -> Self {
42 Self {
43 gas_price,
44 transactions: vec![],
45 position_sender,
46 submitter_client_addr: None,
47 enqueue_time: Instant::now(),
48 }
49 }
50}
51
52pub struct AdmissionQueueMetrics {
54 pub queue_depth: IntGauge,
55 pub queue_wait_latency: Histogram,
56 pub evictions: IntCounter,
57 pub rejections: IntCounter,
58 pub duplicate_inserts: IntCounter,
59}
60
61impl AdmissionQueueMetrics {
62 pub fn new(registry: &Registry) -> Self {
63 Self {
64 queue_depth: register_int_gauge_with_registry!(
65 "admission_queue_depth",
66 "Current number of entries in the admission priority queue",
67 registry,
68 )
69 .unwrap(),
70 queue_wait_latency: register_histogram_with_registry!(
71 "admission_queue_wait_latency",
72 "Time a transaction spends waiting in the admission queue before being drained",
73 mysten_metrics::SUBSECOND_LATENCY_SEC_BUCKETS.to_vec(),
74 registry,
75 )
76 .unwrap(),
77 evictions: register_int_counter_with_registry!(
78 "admission_queue_evictions",
79 "Number of entries evicted from the admission queue by higher gas price transactions",
80 registry,
81 )
82 .unwrap(),
83 rejections: register_int_counter_with_registry!(
84 "admission_queue_rejections",
85 "Number of transactions rejected because the queue was full and their gas price was too low",
86 registry,
87 )
88 .unwrap(),
89 duplicate_inserts: register_int_counter_with_registry!(
90 "admission_queue_duplicate_inserts",
91 "Transactions admitted to the queue whose ConsensusTransactionKey duplicated an entry already present. Tallied as spam for DoS protection.",
92 registry,
93 )
94 .unwrap(),
95 }
96 }
97
98 pub fn new_for_tests() -> Self {
99 Self::new(&Registry::new())
100 }
101}
102
103pub struct PriorityAdmissionQueue {
107 capacity: usize,
108 map: BTreeMap<u64, VecDeque<QueueEntry>>,
109 queued_keys: HashMap<ConsensusTransactionKey, u32>,
111 total_len: usize,
112 metrics: Arc<AdmissionQueueMetrics>,
113}
114
115impl PriorityAdmissionQueue {
116 pub fn new(capacity: usize, metrics: Arc<AdmissionQueueMetrics>) -> Self {
117 Self {
118 capacity,
119 map: BTreeMap::new(),
120 queued_keys: HashMap::new(),
121 total_len: 0,
122 metrics,
123 }
124 }
125
126 pub fn len(&self) -> usize {
127 self.total_len
128 }
129
130 pub fn min_gas_price(&self) -> Option<u64> {
131 self.map.first_key_value().map(|(&k, _)| k)
132 }
133
134 pub fn insert(&mut self, entry: QueueEntry) -> SuiResult<bool> {
138 let keys: Vec<_> = entry.transactions.iter().map(|t| t.key()).collect();
139 let newly_inserted = !keys.iter().any(|k| self.queued_keys.contains_key(k));
140 if !newly_inserted {
141 self.metrics.duplicate_inserts.inc();
142 }
143
144 if self.total_len < self.capacity {
145 self.push_entry(entry, keys);
146 self.metrics.queue_depth.set(self.total_len as i64);
147 return Ok(newly_inserted);
148 }
149
150 let min_price = self.min_gas_price().unwrap();
151 if entry.gas_price > min_price {
152 let evicter_price = entry.gas_price;
153 let evicted = self.evict_lowest();
154 self.push_entry(entry, keys);
155 self.metrics.evictions.inc();
156 let _ = evicted
159 .position_sender
160 .send(Err(tonic::Status::from(SuiError::from(
161 SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion {
162 min_gas_price: evicter_price,
163 },
164 ))));
165 return Ok(newly_inserted);
166 }
167
168 self.metrics.rejections.inc();
169 Err(
170 SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion {
171 min_gas_price: min_price,
172 }
173 .into(),
174 )
175 }
176
177 pub fn pop_batch(&mut self, count: usize) -> Vec<QueueEntry> {
180 let mut remaining = count.min(self.total_len);
181 let mut entries = Vec::with_capacity(remaining);
182 while remaining > 0 {
183 let Some(mut last) = self.map.last_entry() else {
184 break;
185 };
186 let deque = last.get_mut();
187 if deque.len() <= remaining {
188 remaining -= deque.len();
190 self.total_len -= deque.len();
191 entries.extend(last.remove());
192 } else {
193 self.total_len -= remaining;
195 entries.extend(deque.drain(..remaining));
196 remaining = 0;
197 }
198 }
199 for entry in &entries {
200 self.remove_keys(entry);
201 }
202 self.metrics.queue_depth.set(self.total_len as i64);
203 entries
204 }
205
206 pub fn is_empty(&self) -> bool {
207 self.total_len == 0
208 }
209
210 fn push_entry(&mut self, entry: QueueEntry, keys: Vec<ConsensusTransactionKey>) {
211 for key in keys {
212 *self.queued_keys.entry(key).or_insert(0) += 1;
213 }
214 self.map
215 .entry(entry.gas_price)
216 .or_default()
217 .push_back(entry);
218 self.total_len += 1;
219 }
220
221 fn evict_lowest(&mut self) -> QueueEntry {
222 let evicted = {
223 let mut first = self
224 .map
225 .first_entry()
226 .expect("evict_lowest called on empty queue");
227 let deque = first.get_mut();
228 let evicted = deque.pop_front().unwrap();
229 if deque.is_empty() {
230 first.remove();
231 }
232 evicted
233 };
234 self.remove_keys(&evicted);
235 self.total_len -= 1;
236 evicted
237 }
238
239 fn remove_keys(&mut self, entry: &QueueEntry) {
240 for tx in &entry.transactions {
241 let key = tx.key();
242 let std::collections::hash_map::Entry::Occupied(mut slot) = self.queued_keys.entry(key)
243 else {
244 debug_fatal!("remove_keys on absent key");
245 continue;
246 };
247 *slot.get_mut() -= 1;
248 if *slot.get() == 0 {
249 slot.remove();
250 }
251 }
252 }
253}
254
255struct InsertCommand {
257 entry: QueueEntry,
258 response: oneshot::Sender<SuiResult<bool>>,
259}
260
261#[derive(Clone)]
264pub struct AdmissionQueueHandle {
265 sender: mpsc::Sender<InsertCommand>,
266 last_drain: Arc<Mutex<Instant>>,
268 queue_depth: Arc<AtomicUsize>,
269 failover_timeout: Duration,
270}
271
272impl AdmissionQueueHandle {
273 pub fn failover_tripped(&self) -> bool {
277 if self.queue_depth.load(Ordering::Relaxed) == 0 {
278 return false;
279 }
280 self.last_drain.lock().unwrap().elapsed() > self.failover_timeout
281 }
282
283 pub async fn try_insert(
286 &self,
287 gas_price: u64,
288 transactions: Vec<ConsensusTransaction>,
289 submitter_client_addr: Option<IpAddr>,
290 ) -> SuiResult<(
291 oneshot::Receiver<Result<Vec<ConsensusPosition>, tonic::Status>>,
292 bool,
293 )> {
294 let (position_tx, position_rx) = oneshot::channel();
295 let entry = QueueEntry {
296 gas_price,
297 transactions,
298 position_sender: position_tx,
299 submitter_client_addr,
300 enqueue_time: Instant::now(),
301 };
302
303 let (resp_tx, resp_rx) = oneshot::channel();
304 let cmd = InsertCommand {
305 entry,
306 response: resp_tx,
307 };
308
309 self.sender
310 .send(cmd)
311 .await
312 .map_err(|_| SuiError::from(SuiErrorKind::TooManyTransactionsPendingConsensus))?;
313
314 let newly_inserted = resp_rx
315 .await
316 .map_err(|_| SuiError::from(SuiErrorKind::TooManyTransactionsPendingConsensus))??;
317
318 Ok((position_rx, newly_inserted))
319 }
320}
321
322pub struct AdmissionQueueManager {
326 capacity: usize,
327 failover_timeout: Duration,
328 metrics: Arc<AdmissionQueueMetrics>,
329 consensus_adapter: Arc<ConsensusAdapter>,
330 slot_freed_notify: Arc<tokio::sync::Notify>,
331}
332
333impl AdmissionQueueManager {
334 pub fn new(
335 consensus_adapter: Arc<ConsensusAdapter>,
336 metrics: Arc<AdmissionQueueMetrics>,
337 capacity_fraction: f64,
338 failover_timeout: Duration,
339 slot_freed_notify: Arc<tokio::sync::Notify>,
340 ) -> Self {
341 let max_pending = consensus_adapter.max_pending_transactions();
342 let capacity = (max_pending as f64 * capacity_fraction) as usize;
343 assert!(
344 capacity > 0,
345 "admission_queue_capacity_fraction ({capacity_fraction}) * max_pending_transactions ({max_pending}) must be > 0"
346 );
347 Self {
348 capacity,
349 failover_timeout,
350 metrics,
351 consensus_adapter,
352 slot_freed_notify,
353 }
354 }
355
356 pub fn new_for_tests(
357 consensus_adapter: Arc<ConsensusAdapter>,
358 slot_freed_notify: Arc<tokio::sync::Notify>,
359 ) -> Self {
360 Self {
361 capacity: 10_000,
362 failover_timeout: Duration::from_secs(30),
363 metrics: Arc::new(AdmissionQueueMetrics::new_for_tests()),
364 consensus_adapter,
365 slot_freed_notify,
366 }
367 }
368
369 pub fn metrics(&self) -> &Arc<AdmissionQueueMetrics> {
370 &self.metrics
371 }
372
373 pub fn spawn(&self, epoch_store: Arc<AuthorityPerEpochStore>) -> AdmissionQueueHandle {
376 let last_drain = Arc::new(Mutex::new(Instant::now()));
377 let queue_depth = Arc::new(AtomicUsize::new(0));
378
379 let (sender, receiver) = mpsc::channel(self.capacity.max(1024));
380
381 let event_loop = AdmissionQueueEventLoop {
382 receiver,
383 queue: PriorityAdmissionQueue::new(self.capacity, self.metrics.clone()),
384 consensus_adapter: self.consensus_adapter.clone(),
385 slot_freed_notify: self.slot_freed_notify.clone(),
386 epoch_store,
387 last_drain: last_drain.clone(),
388 queue_depth: queue_depth.clone(),
389 last_published_depth: 0,
390 };
391 spawn_monitored_task!(event_loop.run());
392
393 AdmissionQueueHandle {
394 sender,
395 last_drain,
396 queue_depth,
397 failover_timeout: self.failover_timeout,
398 }
399 }
400}
401
402#[derive(Clone)]
408pub struct AdmissionQueueContext {
409 manager: Arc<AdmissionQueueManager>,
410 swap: Arc<ArcSwap<AdmissionQueueHandle>>,
411}
412
413impl AdmissionQueueContext {
414 pub fn spawn(
415 manager: Arc<AdmissionQueueManager>,
416 epoch_store: Arc<AuthorityPerEpochStore>,
417 ) -> Self {
418 let initial_handle = manager.spawn(epoch_store);
419 let swap = Arc::new(ArcSwap::new(Arc::new(initial_handle)));
420 Self { manager, swap }
421 }
422
423 pub fn rotate_for_epoch(&self, epoch_store: Arc<AuthorityPerEpochStore>) {
426 self.swap.store(Arc::new(self.manager.spawn(epoch_store)));
427 }
428
429 pub(crate) fn load(&self) -> arc_swap::Guard<Arc<AdmissionQueueHandle>> {
430 self.swap.load()
431 }
432}
433
434struct AdmissionQueueEventLoop {
437 receiver: mpsc::Receiver<InsertCommand>,
438 queue: PriorityAdmissionQueue,
439 consensus_adapter: Arc<ConsensusAdapter>,
440 slot_freed_notify: Arc<tokio::sync::Notify>,
441 epoch_store: Arc<AuthorityPerEpochStore>,
442 last_drain: Arc<Mutex<Instant>>,
443 queue_depth: Arc<AtomicUsize>,
444 last_published_depth: usize,
445}
446
447impl AdmissionQueueEventLoop {
448 pub async fn run(mut self) {
449 loop {
450 self.process_pending_inserts();
451 self.publish_queue_depth();
452
453 if !handle_fail_point_if("admission_queue_disable_drain")
454 && !self.queue.is_empty()
455 && self.has_consensus_capacity()
456 {
457 self.drain_batch();
458 self.publish_queue_depth();
459 continue;
460 }
461
462 if self.queue.is_empty() {
463 match self.receiver.recv().await {
465 Some(cmd) => self.handle_insert(cmd),
466 None => {
467 debug!("Admission queue actor shutting down");
468 break;
469 }
470 }
471 continue;
472 }
473
474 let notify = self.slot_freed_notify.clone();
479 let slot_freed = notify.notified();
480 tokio::pin!(slot_freed);
481
482 self.process_pending_inserts();
483 if !handle_fail_point_if("admission_queue_disable_drain")
484 && !self.queue.is_empty()
485 && self.has_consensus_capacity()
486 {
487 continue;
488 }
489
490 tokio::select! {
491 biased;
492
493 result = self.receiver.recv() => {
494 match result {
495 Some(cmd) => self.handle_insert(cmd),
496 None => {
497 debug!("Admission queue actor shutting down");
498 break;
499 }
500 }
501 }
502
503 _ = &mut slot_freed => {}
504 }
505 }
506 }
507
508 fn publish_queue_depth(&mut self) {
509 let len = self.queue.len();
510 if len != self.last_published_depth {
511 self.queue_depth.store(len, Ordering::Relaxed);
512 self.last_published_depth = len;
513 }
514 }
515
516 fn process_pending_inserts(&mut self) {
517 while let Ok(cmd) = self.receiver.try_recv() {
518 self.handle_insert(cmd);
519 }
520 }
521
522 fn has_consensus_capacity(&self) -> bool {
523 self.consensus_adapter.num_inflight_transactions()
524 < u64::try_from(self.consensus_adapter.max_pending_transactions()).unwrap()
525 }
526
527 fn drain_batch(&mut self) {
528 let max_pending = u64::try_from(self.consensus_adapter.max_pending_transactions()).unwrap();
529 let available =
530 max_pending.saturating_sub(self.consensus_adapter.num_inflight_transactions());
531 let entries = self.queue.pop_batch(usize::try_from(available).unwrap());
532 if entries.is_empty() {
533 return;
534 }
535 for entry in entries {
536 self.queue
537 .metrics
538 .queue_wait_latency
539 .observe(entry.enqueue_time.elapsed().as_secs_f64());
540 let adapter = self.consensus_adapter.clone();
541 let es = self.epoch_store.clone();
542 spawn_monitored_task!(submit_queue_entry(entry, adapter, es));
543 }
544 *self.last_drain.lock().unwrap() = Instant::now();
545 }
546
547 fn handle_insert(&mut self, cmd: InsertCommand) {
548 let _ = cmd.response.send(self.queue.insert(cmd.entry));
549 }
550}
551
552async fn submit_queue_entry(
553 entry: QueueEntry,
554 consensus_adapter: Arc<ConsensusAdapter>,
555 epoch_store: Arc<AuthorityPerEpochStore>,
556) {
557 let _ = entry.position_sender.send(
558 consensus_adapter
559 .submit_and_get_positions(
560 entry.transactions,
561 &epoch_store,
562 entry.submitter_client_addr,
563 )
564 .await
565 .map_err(tonic::Status::from),
566 );
567}
568
569#[cfg(test)]
570mod tests {
571 use super::*;
572
573 fn make_test_entry(
574 gas_price: u64,
575 ) -> (
576 QueueEntry,
577 oneshot::Receiver<Result<Vec<ConsensusPosition>, tonic::Status>>,
578 ) {
579 let (tx, rx) = oneshot::channel();
580 (QueueEntry::new_for_test(gas_price, tx), rx)
581 }
582
583 fn build_queue(capacity: usize, gas_prices: &[u64]) -> PriorityAdmissionQueue {
584 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
585 let mut q = PriorityAdmissionQueue::new(capacity, metrics);
586 for &gp in gas_prices {
587 let (entry, _) = make_test_entry(gp);
588 q.insert(entry).unwrap();
589 }
590 q
591 }
592
593 #[test]
594 fn test_insert_within_capacity() {
595 let q = build_queue(3, &[100, 200, 50]);
596 assert_eq!(q.len(), 3);
597 }
598
599 #[test]
600 fn test_eviction_when_full() {
601 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
602 let mut q = PriorityAdmissionQueue::new(2, metrics);
603
604 let (e1, mut r1) = make_test_entry(100);
605 let (e2, _) = make_test_entry(200);
606 let (e3, _) = make_test_entry(300);
607
608 q.insert(e1).unwrap();
609 q.insert(e2).unwrap();
610 assert_eq!(q.len(), 2);
611
612 assert!(q.insert(e3).is_ok());
613 assert_eq!(q.len(), 2);
614 let r1_result = r1.try_recv().expect("evicted entry must be signalled");
616 assert!(matches!(r1_result, Err(ref status) if status.message().contains("outbid")));
617 }
618
619 #[test]
620 fn test_rejection_when_full_and_low_price() {
621 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
622 let mut q = PriorityAdmissionQueue::new(2, metrics);
623
624 let (e1, _) = make_test_entry(100);
625 let (e2, _) = make_test_entry(200);
626 let (e3, mut r3) = make_test_entry(50);
627
628 q.insert(e1).unwrap();
629 q.insert(e2).unwrap();
630
631 assert!(matches!(
632 q.insert(e3).unwrap_err().as_inner(),
633 SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price: 100 }
634 ));
635 assert_eq!(q.len(), 2);
636 assert!(r3.try_recv().is_err());
637 }
638
639 #[test]
640 fn test_pop_batch() {
641 let mut q = build_queue(5, &[100, 300, 200]);
642 let batch = q.pop_batch(2);
643 assert_eq!(batch.len(), 2);
644 assert_eq!(q.len(), 1);
645 }
646
647 #[test]
648 fn test_min_gas_price() {
649 let q = build_queue(5, &[200, 100, 300]);
650 assert_eq!(q.min_gas_price(), Some(100));
651 }
652
653 #[test]
654 fn test_gasless_tx_evicted_first() {
655 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
656 let mut q = PriorityAdmissionQueue::new(2, metrics);
657
658 let (gasless, mut r_gasless) = make_test_entry(0);
659 let (normal, _) = make_test_entry(1000);
660 let (high, _) = make_test_entry(2000);
661
662 q.insert(gasless).unwrap();
663 q.insert(normal).unwrap();
664
665 assert!(q.insert(high).is_ok());
666 let gasless_result = r_gasless
667 .try_recv()
668 .expect("evicted gasless entry must be signalled");
669 assert!(matches!(gasless_result, Err(ref status) if status.message().contains("outbid")));
670 assert_eq!(q.min_gas_price(), Some(1000));
671 }
672
673 #[test]
674 fn test_pop_batch_returns_highest_gas_price_first() {
675 let mut q = build_queue(5, &[100, 500, 200, 400, 300]);
676 let batch = q.pop_batch(5);
677 let gas_prices: Vec<u64> = batch.iter().map(|e| e.gas_price).collect();
678 assert_eq!(gas_prices, vec![500, 400, 300, 200, 100]);
679 }
680
681 #[test]
682 fn test_equal_gas_price_rejected_when_full() {
683 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
684 let mut q = PriorityAdmissionQueue::new(1, metrics);
685
686 let (e1, _) = make_test_entry(100);
687 let (e2, _) = make_test_entry(100);
688
689 q.insert(e1).unwrap();
690 assert!(matches!(
691 q.insert(e2).unwrap_err().as_inner(),
692 SuiErrorKind::TransactionRejectedDueToOutbiddingDuringCongestion { min_gas_price: 100 }
693 ));
694 }
695
696 fn make_dup_entry(
697 gas_price: u64,
698 tx: ConsensusTransaction,
699 ) -> (
700 QueueEntry,
701 oneshot::Receiver<Result<Vec<ConsensusPosition>, tonic::Status>>,
702 ) {
703 let (position_tx, position_rx) = oneshot::channel();
704 let entry = QueueEntry {
705 gas_price,
706 transactions: vec![tx],
707 position_sender: position_tx,
708 submitter_client_addr: None,
709 enqueue_time: Instant::now(),
710 };
711 (entry, position_rx)
712 }
713
714 #[test]
715 fn test_duplicate_transaction_admitted_and_flagged() {
716 use sui_types::base_types::AuthorityName;
717
718 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
719 let mut q = PriorityAdmissionQueue::new(10, metrics);
720
721 let tx = ConsensusTransaction::new_end_of_publish(AuthorityName::ZERO);
722
723 let (entry1, _rx1) = make_dup_entry(100, tx.clone());
724 assert!(q.insert(entry1).unwrap());
725 assert_eq!(q.len(), 1);
726
727 let (entry2, _rx2) = make_dup_entry(100, tx.clone());
730 assert!(!q.insert(entry2).unwrap());
731 assert_eq!(q.len(), 2);
732 }
733
734 #[test]
735 fn test_duplicate_key_counter_decrements_on_pop() {
736 use sui_types::base_types::AuthorityName;
737
738 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
739 let mut q = PriorityAdmissionQueue::new(10, metrics);
740
741 let tx = ConsensusTransaction::new_end_of_publish(AuthorityName::ZERO);
742
743 let (entry1, _rx1) = make_dup_entry(100, tx.clone());
745 q.insert(entry1).unwrap();
746 let (entry2, _rx2) = make_dup_entry(100, tx.clone());
747 assert!(!q.insert(entry2).unwrap());
748
749 let batch = q.pop_batch(1);
752 assert_eq!(batch.len(), 1);
753 let (entry3, _rx3) = make_dup_entry(100, tx.clone());
754 assert!(!q.insert(entry3).unwrap());
755 assert_eq!(q.len(), 2);
756
757 let _ = q.pop_batch(q.len());
760 assert!(q.is_empty());
761 let (entry4, _rx4) = make_dup_entry(100, tx);
762 assert!(q.insert(entry4).unwrap());
763 }
764
765 #[test]
766 fn test_duplicate_key_counter_decrements_on_evict() {
767 use sui_types::base_types::AuthorityName;
768
769 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
770 let mut q = PriorityAdmissionQueue::new(2, metrics);
771
772 let tx = ConsensusTransaction::new_end_of_publish(AuthorityName::ZERO);
773
774 let (entry1, _rx1) = make_dup_entry(100, tx.clone());
776 q.insert(entry1).unwrap();
777 let (entry2, _rx2) = make_dup_entry(100, tx.clone());
778 q.insert(entry2).unwrap();
779 assert_eq!(q.len(), 2);
780
781 let (filler, _) = make_test_entry(200);
783 q.insert(filler).unwrap();
784 assert_eq!(q.len(), 2);
785
786 let (filler2, _) = make_test_entry(300);
790 q.insert(filler2).unwrap();
791
792 let (entry3, _rx3) = make_dup_entry(500, tx);
793 assert!(q.insert(entry3).unwrap());
794 }
795
796 #[tokio::test]
797 async fn test_actor_shuts_down_when_handle_dropped() {
798 use crate::authority::test_authority_builder::TestAuthorityBuilder;
799 use crate::checkpoints::CheckpointStore;
800 use crate::consensus_adapter::ConsensusAdapterMetrics;
801 use crate::mysticeti_adapter::LazyMysticetiClient;
802 use sui_types::base_types::AuthorityName;
803
804 let state = TestAuthorityBuilder::new().build().await;
805 let epoch_store = state.epoch_store_for_testing().clone();
806 let consensus_adapter = Arc::new(ConsensusAdapter::new(
807 Arc::new(LazyMysticetiClient::new()),
808 CheckpointStore::new_for_tests(),
809 AuthorityName::ZERO,
810 100_000,
811 100_000,
812 ConsensusAdapterMetrics::new_test(),
813 Arc::new(tokio::sync::Notify::new()),
814 ));
815
816 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
817 let (sender, receiver) = mpsc::channel(100);
818 let slot_freed_notify = Arc::new(tokio::sync::Notify::new());
819
820 let event_loop = AdmissionQueueEventLoop {
821 receiver,
822 queue: PriorityAdmissionQueue::new(100, metrics),
823 consensus_adapter,
824 slot_freed_notify,
825 epoch_store,
826 last_drain: Arc::new(Mutex::new(Instant::now())),
827 queue_depth: Arc::new(AtomicUsize::new(0)),
828 last_published_depth: 0,
829 };
830
831 let handle = tokio::spawn(event_loop.run());
832
833 drop(sender);
835
836 tokio::time::timeout(std::time::Duration::from_secs(5), handle)
838 .await
839 .expect("actor did not shut down within timeout")
840 .expect("actor task panicked");
841 }
842
843 async fn build_consensus_adapter(
844 max_pending_transactions: usize,
845 ) -> (
846 Arc<ConsensusAdapter>,
847 Arc<AuthorityPerEpochStore>,
848 Arc<tokio::sync::Notify>,
849 ) {
850 use crate::authority::test_authority_builder::TestAuthorityBuilder;
851 use crate::checkpoints::CheckpointStore;
852 use crate::consensus_adapter::ConsensusAdapterMetrics;
853 use crate::mysticeti_adapter::LazyMysticetiClient;
854 use sui_types::base_types::AuthorityName;
855
856 let state = TestAuthorityBuilder::new().build().await;
857 let epoch_store = state.epoch_store_for_testing().clone();
858 let slot_freed_notify = Arc::new(tokio::sync::Notify::new());
859 let adapter = Arc::new(ConsensusAdapter::new(
860 Arc::new(LazyMysticetiClient::new()),
861 CheckpointStore::new_for_tests(),
862 AuthorityName::ZERO,
863 max_pending_transactions,
864 100_000,
865 ConsensusAdapterMetrics::new_test(),
866 slot_freed_notify.clone(),
867 ));
868 (adapter, epoch_store, slot_freed_notify)
869 }
870
871 #[tokio::test]
872 async fn test_failover_tripped_when_actor_stalls() {
873 let handle = AdmissionQueueHandle {
876 sender: mpsc::channel(1).0,
877 last_drain: Arc::new(Mutex::new(Instant::now())),
878 queue_depth: Arc::new(AtomicUsize::new(1)),
879 failover_timeout: Duration::from_millis(10),
880 };
881 assert!(!handle.failover_tripped());
882 tokio::time::sleep(Duration::from_millis(30)).await;
883 assert!(handle.failover_tripped());
884
885 handle.queue_depth.store(0, Ordering::Relaxed);
887 assert!(!handle.failover_tripped());
888 }
889
890 #[tokio::test]
891 async fn test_idle_actor_does_not_trip_failover() {
892 let (adapter, epoch_store, notify) = build_consensus_adapter(100_000).await;
895 let manager = AdmissionQueueManager::new(
896 adapter,
897 Arc::new(AdmissionQueueMetrics::new_for_tests()),
898 0.5,
899 Duration::from_millis(10),
900 notify,
901 );
902 let handle = manager.spawn(epoch_store);
903 tokio::time::sleep(Duration::from_millis(50)).await;
904 assert!(
905 !handle.failover_tripped(),
906 "idle actor with empty queue must not trip failover"
907 );
908 }
909
910 #[tokio::test]
916 async fn test_drain_batch_does_not_bump_last_drain_when_no_slots() {
917 let (adapter, epoch_store, notify) = build_consensus_adapter(0).await;
918 let metrics = Arc::new(AdmissionQueueMetrics::new_for_tests());
919 let (_sender, receiver) = mpsc::channel(10);
920
921 let mut queue = PriorityAdmissionQueue::new(10, metrics.clone());
922 let (entry, _rx) = make_test_entry(100);
923 assert!(queue.insert(entry).is_ok());
924 assert_eq!(queue.len(), 1);
925
926 let last_drain = Arc::new(Mutex::new(Instant::now()));
927 let before = *last_drain.lock().unwrap();
928
929 let mut event_loop = AdmissionQueueEventLoop {
930 receiver,
931 queue,
932 consensus_adapter: adapter,
933 slot_freed_notify: notify,
934 epoch_store,
935 last_drain: last_drain.clone(),
936 queue_depth: Arc::new(AtomicUsize::new(0)),
937 last_published_depth: 0,
938 };
939
940 tokio::time::sleep(Duration::from_millis(20)).await;
943
944 event_loop.drain_batch();
945
946 assert_eq!(event_loop.queue.len(), 1, "no entries should be drained");
947 assert_eq!(
948 *last_drain.lock().unwrap(),
949 before,
950 "last_drain must not advance when drain_batch drained nothing"
951 );
952 }
953}