Skip to main content

iota_core/
overload_monitor.rs

1// Copyright (c) Mysten Labs, Inc.
2// Modifications Copyright (c) 2024 IOTA Stiftung
3// SPDX-License-Identifier: Apache-2.0
4
5use std::{
6    cmp::{max, min},
7    hash::Hasher,
8    sync::{
9        Weak,
10        atomic::{AtomicBool, AtomicU32, Ordering},
11    },
12    time::{Duration, SystemTime, UNIX_EPOCH},
13};
14
15use iota_config::node::AuthorityOverloadConfig;
16use iota_metrics::monitored_scope;
17use iota_sdk_types::TransactionDigest;
18use iota_types::{
19    error::{IotaError, IotaResult},
20    fp_bail,
21};
22use tokio::time::sleep;
23use tracing::{debug, info};
24use twox_hash::XxHash64;
25
26use crate::{
27    authority::AuthorityState, consensus_adapter::ConsensusAdapter,
28    execution_scheduler::ExecutionSchedulerAPI,
29};
30
31#[derive(Default)]
32pub struct AuthorityOverloadInfo {
33    /// Whether the authority is overloaded.
34    pub is_overload: AtomicBool,
35
36    /// The locally computed percentage of transactions this authority would
37    /// drop. This is the *output* of this authority's overload monitor (the max
38    /// of the latency-, queue-, and cache-based signals); it is distinct from
39    /// the quorum-determined percentage actually enforced in the post-consensus
40    /// load-shedding path.
41    pub local_load_shedding_percentage: AtomicU32,
42
43    /// The latency-based controller's own previous output, kept separately so
44    /// it can be fed back as that controller's input on the next iteration.
45    pub latency_load_shedding_percentage: AtomicU32,
46}
47
48impl AuthorityOverloadInfo {
49    pub fn set_overload(&self, local_load_shedding_percentage: u32) {
50        self.is_overload.store(true, Ordering::Relaxed);
51        self.local_load_shedding_percentage
52            .store(min(local_load_shedding_percentage, 100), Ordering::Relaxed);
53    }
54
55    pub fn clear_overload(&self) {
56        self.is_overload.store(false, Ordering::Relaxed);
57        self.local_load_shedding_percentage
58            .store(0, Ordering::Relaxed);
59        self.latency_load_shedding_percentage
60            .store(0, Ordering::Relaxed);
61    }
62}
63
64const STEADY_OVERLOAD_REDUCTION_PERCENTAGE: u32 = 10;
65const EXECUTION_RATE_RATIO_FOR_COMPARISON: f64 = 0.95;
66const ADDITIONAL_LOAD_SHEDDING: f64 = 0.02;
67
68// The update interval of the random seed used to determine whether a txn should
69// be rejected.
70const SEED_UPDATE_DURATION_SECS: u64 = 30;
71
72// Monitors the overload signals in `authority_state` periodically, and updates
73// its `overload_info` when the signals indicates overload.
74pub async fn overload_monitor(
75    authority_state: Weak<AuthorityState>,
76    config: AuthorityOverloadConfig,
77) {
78    info!("Starting system overload monitor.");
79
80    loop {
81        let authority_exist = check_execution_overload(&authority_state, &config);
82        if !authority_exist {
83            // `authority_state` doesn't exist anymore. Quit overload monitor.
84            break;
85        }
86        sleep(config.overload_monitor_interval).await;
87    }
88
89    info!("Shut down system overload monitor.");
90}
91
92/// Periodically refreshes the `consensus_queue_load_shedding_percentage`
93/// metric so it tracks the current consensus queue depth even when no
94/// gRPC traffic is arriving (which would otherwise be the only path that
95/// updates the metric, via `check_consensus_queue_graduated_limits` on
96/// `AuthorityState`). Used only in the certificate-less (P-COOL)
97/// mode.
98pub async fn consensus_queue_overload_monitor(
99    authority_state: Weak<AuthorityState>,
100    consensus_adapter: Weak<ConsensusAdapter>,
101    interval: Duration,
102) {
103    info!("Starting consensus queue overload monitor.");
104
105    loop {
106        let (Some(state), Some(adapter)) = (authority_state.upgrade(), consensus_adapter.upgrade())
107        else {
108            // Either `authority_state` or `consensus_adapter` doesn't exist
109            // anymore. Quit monitor.
110            break;
111        };
112
113        let num_inflight_txs = adapter.num_inflight_transactions() as usize;
114        let shedding_pct = compute_graduated_load_shedding_percentage(
115            num_inflight_txs,
116            adapter.max_pending_transactions(),
117            adapter.graduated_load_shedding_soft_limit_pct(),
118        );
119        state
120            .metrics
121            .consensus_queue_load_shedding_percentage
122            .set(shedding_pct as i64);
123
124        sleep(interval).await;
125    }
126
127    info!("Shut down consensus queue overload monitor.");
128}
129
130// Checks authority overload signals, and updates authority's `overload_info`.
131// Returns whether the authority state exists.
132fn check_execution_overload(
133    authority_state: &Weak<AuthorityState>,
134    config: &AuthorityOverloadConfig,
135) -> bool {
136    let _scope = monitored_scope("OverloadMonitor::check_authority_overload");
137    let authority_arc = authority_state.upgrade();
138    if authority_arc.is_none() {
139        // `authority_state` doesn't exist anymore.
140        return false;
141    }
142
143    let authority = authority_arc.unwrap();
144    let queueing_latency = authority
145        .metrics
146        .execution_queueing_latency
147        .latency()
148        .unwrap_or_default();
149    let txn_ready_rate = authority.metrics.txn_ready_rate_tracker.lock().rate();
150    let execution_rate = authority.metrics.execution_rate_tracker.lock().rate();
151    let inflight_queue_len = authority.execution_scheduler().num_pending_transactions();
152    let cache_pending_count = authority
153        .get_cache_commit()
154        .approximate_pending_transaction_count() as usize;
155
156    debug!(
157        "Check authority overload signal, queueing latency {:?}, ready rate {:?}, execution rate {:?}, inflight queue len {:?}, cache pending count {:?}.",
158        queueing_latency, txn_ready_rate, execution_rate, inflight_queue_len, cache_pending_count
159    );
160
161    // Feedback term: the latency controller's *own* previous output.
162    let previous_latency_based_percentage = authority
163        .overload_info
164        .latency_load_shedding_percentage
165        .load(Ordering::Relaxed);
166
167    let (_, latency_based_percentage) = compute_latency_load_shedding_percentage(
168        config,
169        previous_latency_based_percentage,
170        queueing_latency,
171        txn_ready_rate,
172        execution_rate,
173    );
174
175    // Persist the latency controller's own output for next iteration's feedback,
176    // independent of the combined max stored via `set_overload` below.
177    authority
178        .overload_info
179        .latency_load_shedding_percentage
180        .store(latency_based_percentage, Ordering::Relaxed);
181
182    let queue_based_percentage = compute_graduated_load_shedding_percentage(
183        inflight_queue_len,
184        config.max_transaction_manager_queue_length,
185        config.max_transaction_manager_queue_length_soft_limit_pct(),
186    );
187
188    let cache_config = &authority.config.execution_cache_config.writeback_cache;
189    let cache_based_percentage = compute_graduated_load_shedding_percentage(
190        cache_pending_count,
191        cache_config.backpressure_threshold() as usize,
192        cache_config.backpressure_soft_limit_pct(),
193    );
194
195    // The final load shedding percentage combines three signals:
196    //   - latency/rate-based, from execution queueing latency,
197    //   - queue-length-based, from the txn manager's inflight queue,
198    //   - cache-backpressure-based, from the writeback cache's pending count.
199    //
200    // All three are correlated in steady state — by Little's Law,
201    // `inflight_queue_len ≈ txn_ready_rate × queueing_latency`; cache pending
202    // count tracks uncommitted writes which accumulate when execution outpaces
203    // checkpoint flush — so they are combined with `max` rather than summed,
204    // to avoid double-counting. Under transients they diverge: queue length
205    // reacts to arrival bursts before averaged latency does, latency catches
206    // sustained slow execution even when queue depth is modest, and cache
207    // pressure shows up when checkpoint flush stalls even if execution itself
208    // is keeping up. Each therefore guards a different failure mode.
209    let load_shedding_percentage = max(
210        max(latency_based_percentage, queue_based_percentage),
211        cache_based_percentage,
212    );
213    let is_overload = load_shedding_percentage > 0;
214
215    if is_overload {
216        authority
217            .overload_info
218            .set_overload(load_shedding_percentage);
219    } else {
220        authority.overload_info.clear_overload();
221    }
222
223    authority
224        .metrics
225        .authority_overload_status
226        .set(is_overload as i64);
227    authority
228        .metrics
229        .local_post_consensus_load_shedding_percentage
230        .set(load_shedding_percentage as i64);
231    true
232}
233
234// Calculates the percentage of transactions to drop in order to reduce
235// execution queue. Returns the integer percentage between 0 and 100.
236fn calculate_load_shedding_percentage(txn_ready_rate: f64, execution_rate: f64) -> u32 {
237    // When transaction ready rate is practically 0, we aren't adding more load to
238    // the execution driver, so no shedding.
239    // TODO: consensus handler or transaction manager can also be overloaded.
240    if txn_ready_rate < 1e-10 {
241        return 0;
242    }
243
244    // Deflate the execution rate to account for the case that execution_rate is
245    // close to txn_ready_rate.
246    if execution_rate * EXECUTION_RATE_RATIO_FOR_COMPARISON > txn_ready_rate {
247        return 0;
248    }
249
250    // In order to maintain execution queue length, we need to drop at least (1 -
251    // executionRate / readyRate). To reduce the queue length, here we add 10%
252    // more transactions to drop.
253    (((1.0 - execution_rate * EXECUTION_RATE_RATIO_FOR_COMPARISON / txn_ready_rate)
254        + ADDITIONAL_LOAD_SHEDDING)
255        .min(1.0)
256        * 100.0)
257        .round() as u32
258}
259
260// Given overload signals (`queueing_latency`, `txn_ready_rate`,
261// `execution_rate`), return whether the authority server should enter load
262// shedding mode, and how much percentage of transactions to drop. Note that the
263// final load shedding percentage should also take the current load shedding
264// percentage into consideration. If we are already shedding 40% load, based on
265// the current txn_ready_rate and execution_rate, we need to shed 10% more, the
266// outcome is that we need to shed 40% + (1 - 40%) * 10% = 46%.
267// When txn_ready_rate is less than execution_rate, we gradually reduce load
268// shedding percentage until the queueing latency is back to normal.
269fn compute_latency_load_shedding_percentage(
270    config: &AuthorityOverloadConfig,
271    current_load_shedding_percentage: u32,
272    queueing_latency: Duration,
273    txn_ready_rate: f64,
274    execution_rate: f64,
275) -> (bool, u32) {
276    // First, we calculate based on the current `txn_ready_rate` and
277    // `execution_rate`, what's the percentage of traffic to shed from
278    // `txn_ready_rate`.
279
280    let additional_load_shedding_percentage =
281        if queueing_latency > config.execution_queue_latency_hard_limit {
282            let calculated_load_shedding_percentage =
283                calculate_load_shedding_percentage(txn_ready_rate, execution_rate);
284
285            if calculated_load_shedding_percentage > 0
286                || txn_ready_rate >= config.safe_transaction_ready_rate as f64
287            {
288                max(
289                    calculated_load_shedding_percentage,
290                    config.min_load_shedding_percentage_above_hard_limit,
291                )
292            } else {
293                0
294            }
295        } else if queueing_latency > config.execution_queue_latency_soft_limit {
296            calculate_load_shedding_percentage(txn_ready_rate, execution_rate)
297        } else {
298            0
299        };
300
301    // Next, we calculate the new load shedding percentage.
302    let load_shedding_percentage = if additional_load_shedding_percentage > 0 {
303        // When we need to shed more load, since the `txn_ready_rate` is already
304        // influenced by `current_load_shedding_percentage`, we need to
305        // calculate the new load shedding percentage from
306        // `current_load_shedding_percentage` and
307        // `additional_load_shedding_percentage`.
308        current_load_shedding_percentage
309            + (100 - current_load_shedding_percentage) * additional_load_shedding_percentage / 100
310    } else if txn_ready_rate > config.safe_transaction_ready_rate as f64
311        && current_load_shedding_percentage > 10
312    {
313        // We don't need to shed more load. However, the enqueue rate is still not
314        // minimal. We gradually reduce load shedding percentage (10% at a time)
315        // to gracefully accept more load.
316        current_load_shedding_percentage - STEADY_OVERLOAD_REDUCTION_PERCENTAGE
317    } else {
318        // The current transaction ready rate is considered very low. Turn off load
319        // shedding mode.
320        0
321    };
322
323    let load_shedding_percentage = min(
324        load_shedding_percentage,
325        config.max_load_shedding_percentage,
326    );
327    let overload_status = load_shedding_percentage > 0;
328    (overload_status, load_shedding_percentage)
329}
330
331/// Return true if we should reject the txn with `tx_digest`.
332pub(crate) fn should_reject_tx(
333    load_shedding_percentage: u32,
334    tx_digest: TransactionDigest,
335    temporal_seed: u64,
336) -> bool {
337    // TODO: we also need to add a secret salt (e.g. first consensus commit in the
338    // current epoch), to prevent gaming the system.
339    let mut hasher = XxHash64::with_seed(temporal_seed);
340    hasher.write(tx_digest.bytes());
341    let value = hasher.finish();
342    value % 100 < load_shedding_percentage as u64
343}
344
345/// Checks if we can accept the transaction with `tx_digest`.
346pub fn overload_monitor_accept_tx(
347    load_shedding_percentage: u32,
348    tx_digest: TransactionDigest,
349) -> IotaResult {
350    // Derive a random seed from the epoch time for transaction selection. Changing
351    // the seed every `SEED_UPDATE_DURATION_SECS` interval allows rejected
352    // transaction's retry to have a chance to go through in the future.
353    // Also, using the epoch time instead of randomly generating a seed allows that
354    // all validators makes the same decision.
355    let temporal_seed = SystemTime::now()
356        .duration_since(UNIX_EPOCH)
357        .expect("IOTA did not exist prior to 1970")
358        .as_secs()
359        / SEED_UPDATE_DURATION_SECS;
360
361    if should_reject_tx(load_shedding_percentage, tx_digest, temporal_seed) {
362        // TODO: using `SEED_UPDATE_DURATION_SECS` is a safe suggestion that the time
363        // based seed is definitely different by then. However, a shorter
364        // suggestion may be available.
365        fp_bail!(IotaError::ValidatorOverloadedRetryAfter {
366            retry_after_secs: SEED_UPDATE_DURATION_SECS
367        });
368    }
369    Ok(())
370}
371
372/// Computes the graduated load shedding percentage based on the current value
373/// relative to its hard limit. Returns 0 if `current` is at or below the soft
374/// limit (computed as `hard_limit * soft_limit_pct / 100`), linearly scales
375/// from 0% to 100% between soft and hard limits, and returns 100% if `current`
376/// is at or above `hard_limit`.
377///
378/// `soft_limit_pct` is expected to be in `[0, 100]`. Values above 100 are
379/// clamped to 100 in release builds and trigger a debug assertion in debug
380/// builds.
381///
382/// Setting `soft_limit_pct = 100` degenerates into a hard binary cutoff: no
383/// shedding below `hard_limit`, full (100%) shedding at and above it.
384///
385/// NOTE: `soft_limit` is computed via integer division `hard_limit *
386/// soft_limit_pct / 100`, so it floors. This is negligible for typical
387/// queue sizes (thousands).
388pub(crate) fn compute_graduated_load_shedding_percentage(
389    current: usize,
390    hard_limit: usize,
391    soft_limit_pct: u32,
392) -> u32 {
393    debug_assert!(
394        soft_limit_pct <= 100,
395        "soft_limit_pct must be <= 100, got {soft_limit_pct}"
396    );
397    // Clamp `soft_limit_pct` to 100% to be safe in release builds.
398    let soft_limit_pct = soft_limit_pct.min(100);
399    // Convert soft limit percentage to absolute soft limit.
400    let soft_limit = hard_limit * soft_limit_pct as usize / 100;
401
402    // At or above hard limit, shed at maximum percentage.
403    // WARN: this hard limit check must come BEFORE the soft limit check.
404    // When `soft_limit_pct == 100`, soft_limit == hard_limit, and at `current ==
405    // hard_limit`, we want 100% shedding (binary cutoff behavior), not 0%.
406    // Swapping the order would incorrectly return 0 in this degenerate case.
407    if current >= hard_limit {
408        return 100;
409    }
410
411    // No shedding below or at soft limit.
412    if current <= soft_limit {
413        return 0;
414    }
415
416    // The two early returns above imply that at this point,
417    // `soft_limit < current < hard_limit`, so the following two
418    // subtraction results are guaranteed to be strictly > 0.
419    let range = hard_limit - soft_limit;
420    let excess = current - soft_limit;
421
422    // Linear interpolation: 0% at `soft_limit`, 100% at `hard_limit`.
423    (excess * 100 / range) as u32
424}
425
426#[cfg(test)]
427#[expect(clippy::disallowed_methods)] // allow unbounded_channel() since tests are simulating txn manager execution
428// driver interaction.
429mod tests {
430    use std::sync::Arc;
431
432    use iota_macros::sim_test;
433    use rand::{
434        Rng, SeedableRng,
435        rngs::{OsRng, StdRng},
436    };
437    use tokio::{
438        sync::{
439            mpsc::{UnboundedReceiver, UnboundedSender, unbounded_channel},
440            oneshot,
441        },
442        task::JoinHandle,
443        time::{Instant, MissedTickBehavior, interval},
444    };
445
446    use super::*;
447    use crate::authority::test_authority_builder::TestAuthorityBuilder;
448
449    #[test]
450    fn test_authority_overload_info() {
451        let overload_info = AuthorityOverloadInfo::default();
452        assert!(!overload_info.is_overload.load(Ordering::Relaxed));
453        assert_eq!(
454            overload_info
455                .local_load_shedding_percentage
456                .load(Ordering::Relaxed),
457            0
458        );
459
460        {
461            overload_info.set_overload(20);
462            assert!(overload_info.is_overload.load(Ordering::Relaxed));
463            assert_eq!(
464                overload_info
465                    .local_load_shedding_percentage
466                    .load(Ordering::Relaxed),
467                20
468            );
469        }
470
471        // Tests that load shedding percentage can't go beyond 100%.
472        {
473            overload_info.set_overload(110);
474            assert!(overload_info.is_overload.load(Ordering::Relaxed));
475            assert_eq!(
476                overload_info
477                    .local_load_shedding_percentage
478                    .load(Ordering::Relaxed),
479                100
480            );
481        }
482
483        // `clear_overload` also resets the latency controller's feedback term,
484        // keeping it aligned with `local_load_shedding_percentage`.
485        {
486            overload_info
487                .latency_load_shedding_percentage
488                .store(30, Ordering::Relaxed);
489            overload_info.clear_overload();
490            assert!(!overload_info.is_overload.load(Ordering::Relaxed));
491            assert_eq!(
492                overload_info
493                    .local_load_shedding_percentage
494                    .load(Ordering::Relaxed),
495                0
496            );
497            assert_eq!(
498                overload_info
499                    .latency_load_shedding_percentage
500                    .load(Ordering::Relaxed),
501                0
502            );
503        }
504    }
505
506    #[test]
507    fn test_calculate_load_shedding_ratio() {
508        assert_eq!(calculate_load_shedding_percentage(95.0, 100.1), 0);
509        assert_eq!(calculate_load_shedding_percentage(95.0, 100.0), 2);
510        assert_eq!(calculate_load_shedding_percentage(100.0, 100.0), 7);
511        assert_eq!(calculate_load_shedding_percentage(110.0, 100.0), 16);
512        assert_eq!(calculate_load_shedding_percentage(180.0, 100.0), 49);
513        assert_eq!(calculate_load_shedding_percentage(100.0, 0.0), 100);
514        assert_eq!(calculate_load_shedding_percentage(0.0, 1.0), 0);
515    }
516
517    #[test]
518    fn test_check_overload_signals() {
519        let config = AuthorityOverloadConfig {
520            execution_queue_latency_hard_limit: Duration::from_secs(10),
521            execution_queue_latency_soft_limit: Duration::from_secs(1),
522            max_load_shedding_percentage: 90,
523            ..Default::default()
524        };
525
526        // When execution queueing latency is within soft limit, don't start overload
527        // protection.
528        assert_eq!(
529            compute_latency_load_shedding_percentage(
530                &config,
531                0,
532                Duration::from_millis(500),
533                1000.0,
534                10.0
535            ),
536            (false, 0)
537        );
538
539        // When execution queueing latency hits soft limit and execution rate is higher,
540        // don't start overload protection.
541        assert_eq!(
542            compute_latency_load_shedding_percentage(
543                &config,
544                0,
545                Duration::from_secs(2),
546                100.0,
547                120.0
548            ),
549            (false, 0)
550        );
551
552        // When execution queueing latency hits soft limit, but not hard limit, start
553        // overload protection.
554        assert_eq!(
555            compute_latency_load_shedding_percentage(
556                &config,
557                0,
558                Duration::from_secs(2),
559                100.0,
560                100.0
561            ),
562            (true, 7)
563        );
564
565        // When execution queueing latency hits hard limit, start more aggressive
566        // overload protection.
567        assert_eq!(
568            compute_latency_load_shedding_percentage(
569                &config,
570                0,
571                Duration::from_secs(11),
572                100.0,
573                100.0
574            ),
575            (true, 50)
576        );
577
578        // When execution queueing latency hits hard limit and calculated shedding
579        // percentage is higher than
580        // min_load_shedding_percentage_above_hard_limit.
581        assert_eq!(
582            compute_latency_load_shedding_percentage(
583                &config,
584                0,
585                Duration::from_secs(11),
586                240.0,
587                100.0
588            ),
589            (true, 62)
590        );
591
592        // When execution queueing latency hits hard limit, but transaction ready rate
593        // is within safe_transaction_ready_rate, don't start overload protection.
594        assert_eq!(
595            compute_latency_load_shedding_percentage(
596                &config,
597                0,
598                Duration::from_secs(11),
599                20.0,
600                100.0
601            ),
602            (false, 0)
603        );
604
605        // Maximum transactions shed is cap by `max_load_shedding_percentage` config.
606        assert_eq!(
607            compute_latency_load_shedding_percentage(
608                &config,
609                0,
610                Duration::from_secs(11),
611                100.0,
612                0.0
613            ),
614            (true, 90)
615        );
616
617        // When the system is already shedding 50% of load, and the current txn ready
618        // rate and execution rate require another 20%, the final shedding rate
619        // is 60%.
620        assert_eq!(
621            compute_latency_load_shedding_percentage(
622                &config,
623                50,
624                Duration::from_secs(2),
625                116.0,
626                100.0
627            ),
628            (true, 60)
629        );
630
631        // Load shedding percentage is gradually reduced when txn ready rate is lower
632        // than execution rate.
633        assert_eq!(
634            compute_latency_load_shedding_percentage(
635                &config,
636                90,
637                Duration::from_secs(2),
638                200.0,
639                300.0
640            ),
641            (true, 80)
642        );
643
644        // When queueing delay is above hard limit, we shed additional 50% every time.
645        assert_eq!(
646            compute_latency_load_shedding_percentage(
647                &config,
648                50,
649                Duration::from_secs(11),
650                100.0,
651                100.0
652            ),
653            (true, 75)
654        );
655    }
656
657    /// Tests [`compute_graduated_load_shedding_percentage`]:
658    /// - 0% at or below the soft limit (`hard_limit * soft_limit_pct / 100`)
659    /// - linear scaling between soft and hard limits
660    /// - 100% at or above the hard limit
661    /// - degenerate cases for `soft_limit_pct = 0` and `soft_limit_pct = 100`
662    #[test]
663    fn test_compute_graduated_load_shedding_percentage() {
664        let hard_limit = 20_000;
665        let soft_limit_pct = 50;
666        let soft_limit = hard_limit * soft_limit_pct as usize / 100; // 10_000
667
668        // Below and at soft limit: no shedding.
669        for current in [0, soft_limit - 1, soft_limit] {
670            assert_eq!(
671                compute_graduated_load_shedding_percentage(current, hard_limit, soft_limit_pct),
672                0,
673                "no shedding expected at or below soft limit ({current} <= {soft_limit})",
674            );
675        }
676
677        // Linear scaling between soft and hard limits:
678        //  - At 25% of range (12_500): 100 * 2_500 / 10_000 = 25
679        //  - At midpoint (15_000):     100 * 5_000 / 10_000 = 50
680        //  - At 75% of range (17_500): 100 * 7_500 / 10_000 = 75
681        //  - Just below hard limit:    100 * 9_999 / 10_000 = 99
682        for (current, expected_pct) in [
683            (12_500, 25),
684            (15_000, 50),
685            (17_500, 75),
686            (hard_limit - 1, 99),
687        ] {
688            assert_eq!(
689                compute_graduated_load_shedding_percentage(current, hard_limit, soft_limit_pct),
690                expected_pct,
691                "expected shedding percentage to be {expected_pct}% at current={current}",
692            );
693        }
694
695        // At and above hard limit: 100%.
696        for current in [hard_limit, hard_limit + 1, 30_000] {
697            assert_eq!(
698                compute_graduated_load_shedding_percentage(current, hard_limit, soft_limit_pct),
699                100,
700                "expected 100% shedding at/above hard limit ({current} >= {hard_limit})",
701            );
702        }
703
704        // Degenerate: soft_limit_pct = 100 acts as a binary cutoff:
705        // - below hard_limit: 0%; at/above: 100%.
706        for current in [0, hard_limit - 1] {
707            assert_eq!(
708                compute_graduated_load_shedding_percentage(current, hard_limit, 100),
709                0,
710                "soft_limit_pct=100: no shedding expected below hard limit ({current} < {hard_limit})",
711            );
712        }
713        for current in [hard_limit, hard_limit + 1] {
714            assert_eq!(
715                compute_graduated_load_shedding_percentage(current, hard_limit, 100),
716                100,
717                "soft_limit_pct=100: full shedding expected at/above hard limit ({current} >= \
718                    {hard_limit})",
719            );
720        }
721
722        // Degenerate: soft_limit_pct = 0 means soft_limit = 0; any current > 0 sheds.
723        assert_eq!(
724            compute_graduated_load_shedding_percentage(0, hard_limit, 0),
725            0,
726            "soft_limit_pct=0: at current=0, no shedding expected (current <= soft_limit=0)",
727        );
728        assert_eq!(
729            compute_graduated_load_shedding_percentage(hard_limit / 2, hard_limit, 0),
730            50,
731            "soft_limit_pct=0: at midpoint of hard_limit, 50% shedding expected",
732        );
733        assert_eq!(
734            compute_graduated_load_shedding_percentage(hard_limit, hard_limit, 0),
735            100,
736            "soft_limit_pct=0: at hard_limit, 100% shedding expected",
737        );
738    }
739
740    #[tokio::test(flavor = "current_thread")]
741    async fn test_check_authority_overload() {
742        telemetry_subscribers::init_for_testing();
743
744        let config = AuthorityOverloadConfig {
745            safe_transaction_ready_rate: 0,
746            ..Default::default()
747        };
748        let state = TestAuthorityBuilder::new()
749            .with_authority_overload_config(config.clone())
750            .build()
751            .await;
752
753        // Initialize latency reporter.
754        for _ in 0..1000 {
755            state
756                .metrics
757                .execution_queueing_latency
758                .report(Duration::from_secs(20));
759        }
760
761        // Creates a simple case to see if authority state overload_info can be updated
762        // correctly by check_authority_overload.
763        let authority = Arc::downgrade(&state);
764        assert!(check_execution_overload(&authority, &config));
765        assert!(state.overload_info.is_overload.load(Ordering::Relaxed));
766        assert_eq!(
767            state
768                .overload_info
769                .local_load_shedding_percentage
770                .load(Ordering::Relaxed),
771            config.min_load_shedding_percentage_above_hard_limit
772        );
773
774        // Checks that check_authority_overload should return false when the input
775        // authority state doesn't exist.
776        let authority = Arc::downgrade(&state);
777        drop(state);
778        assert!(!check_execution_overload(&authority, &config));
779    }
780
781    /// Wires `WritebackCacheConfig`'s backpressure thresholds into
782    /// `compute_graduated_load_shedding_percentage` and verifies the
783    /// post-consensus cache-pressure signal at cache-relevant scales.
784    /// Catches regressions in either the config getter defaults or the
785    /// call signature used in `check_execution_overload`.
786    #[test]
787    fn test_writeback_cache_backpressure_soft_limit_pct() {
788        use iota_config::node::WritebackCacheConfig;
789
790        // Default getter: 50% with no explicit value or env override.
791        let default_config = WritebackCacheConfig::default();
792        assert_eq!(default_config.backpressure_soft_limit_pct(), 50);
793        assert_eq!(default_config.backpressure_threshold(), 100_000);
794
795        // Explicit config: soft_limit_pct of 75 against a 1000-pending-tx
796        // hard limit. Soft limit = 1000 * 75 / 100 = 750.
797        let config = WritebackCacheConfig {
798            backpressure_threshold: Some(1000),
799            backpressure_soft_limit_pct: Some(75),
800            ..Default::default()
801        };
802        assert_eq!(config.backpressure_threshold(), 1000);
803        assert_eq!(config.backpressure_soft_limit_pct(), 75);
804
805        // Below soft limit: no shedding.
806        for pending in [0u64, 100, 750] {
807            assert_eq!(
808                compute_graduated_load_shedding_percentage(
809                    pending as usize,
810                    config.backpressure_threshold() as usize,
811                    config.backpressure_soft_limit_pct(),
812                ),
813                0,
814                "no shedding expected at pending={pending} <= soft_limit=750",
815            );
816        }
817
818        // Halfway between soft (750) and hard (1000): 50% shedding.
819        assert_eq!(
820            compute_graduated_load_shedding_percentage(
821                875,
822                config.backpressure_threshold() as usize,
823                config.backpressure_soft_limit_pct(),
824            ),
825            50,
826        );
827
828        // At and above hard limit: 100% shedding.
829        for pending in [1000u64, 1500, 100_000] {
830            assert_eq!(
831                compute_graduated_load_shedding_percentage(
832                    pending as usize,
833                    config.backpressure_threshold() as usize,
834                    config.backpressure_soft_limit_pct(),
835                ),
836                100,
837                "100% shedding expected at pending={pending} >= hard_limit=1000",
838            );
839        }
840
841        // Out-of-range soft_limit_pct is clamped to 100 by the getter.
842        let clamped = WritebackCacheConfig {
843            backpressure_soft_limit_pct: Some(150),
844            ..Default::default()
845        };
846        assert_eq!(clamped.backpressure_soft_limit_pct(), 100);
847    }
848
849    /// Verifies that the cache-pressure signal flows end-to-end from
850    /// `WritebackCacheConfig` → `check_execution_overload` →
851    /// `overload_info.local_load_shedding_percentage` and the
852    /// `local_post_consensus_load_shedding_percentage` metric.
853    ///
854    /// We can't easily inject a non-zero
855    /// `approximate_pending_transaction_count` into the test cache without
856    /// an additional test hook, so this test instead degenerates the
857    /// threshold by setting `backpressure_threshold = 0`, which makes
858    /// `compute_graduated_load_shedding_percentage` return 100% even with
859    /// `pending_count = 0` (the `current >= hard_limit` branch with `0 >=
860    /// 0`). That exercises the full wiring while keeping the test
861    /// self-contained.
862    #[tokio::test(flavor = "current_thread")]
863    async fn test_check_execution_overload_cache_signal_drives_shedding() {
864        use iota_config::node::{ExecutionCacheConfig, WritebackCacheConfig};
865
866        telemetry_subscribers::init_for_testing();
867
868        let cache_config = ExecutionCacheConfig {
869            writeback_cache: WritebackCacheConfig {
870                backpressure_threshold: Some(0),
871                ..Default::default()
872            },
873        };
874        let overload_config = AuthorityOverloadConfig::default();
875        let state = TestAuthorityBuilder::new()
876            .with_authority_overload_config(overload_config.clone())
877            .with_cache_config(cache_config)
878            .build()
879            .await;
880
881        let authority = Arc::downgrade(&state);
882        assert!(check_execution_overload(&authority, &overload_config));
883
884        // Cache signal alone should drive 100% shedding via the degenerate
885        // threshold; latency and queue signals contribute 0 in a fresh state.
886        assert!(state.overload_info.is_overload.load(Ordering::Relaxed));
887        assert_eq!(
888            state
889                .overload_info
890                .local_load_shedding_percentage
891                .load(Ordering::Relaxed),
892            100,
893        );
894        assert_eq!(
895            state
896                .metrics
897                .local_post_consensus_load_shedding_percentage
898                .get(),
899            100,
900        );
901    }
902
903    // Creates an AuthorityState and starts an overload monitor that monitors its
904    // metrics.
905    async fn start_overload_monitor() -> (Arc<AuthorityState>, JoinHandle<()>) {
906        let overload_config = AuthorityOverloadConfig::default();
907        let state = TestAuthorityBuilder::new()
908            .with_authority_overload_config(overload_config.clone())
909            .build()
910            .await;
911        let authority_state = Arc::downgrade(&state);
912        let monitor_handle = tokio::spawn(async move {
913            overload_monitor(authority_state, overload_config).await;
914        });
915        (state, monitor_handle)
916    }
917
918    // Starts a load generator that generates a steady workload, and also allow it
919    // to accept burst of request through `burst_rx`.
920    // Request tracking is done by the overload monitor inside `authority`.
921    fn start_load_generator(
922        steady_rate: f64,
923        tx: UnboundedSender<Instant>,
924        mut burst_rx: UnboundedReceiver<u32>,
925        authority: Arc<AuthorityState>,
926        enable_load_shedding: bool,
927        total_requests_arc: Arc<AtomicU32>,
928        dropped_requests_arc: Arc<AtomicU32>,
929    ) -> JoinHandle<()> {
930        tokio::spawn(async move {
931            let mut interval = interval(Duration::from_secs_f64(1.0 / steady_rate));
932            let mut rng = StdRng::from_rng(&mut OsRng).unwrap();
933            let mut total_requests: u32 = 0;
934            let mut total_dropped_requests: u32 = 0;
935
936            // Helper function to check whether we should send a request.
937            let mut do_send =
938                |enable_load_shedding: bool, authority: Arc<AuthorityState>| -> bool {
939                    if enable_load_shedding {
940                        let shedding_percentage = authority
941                            .overload_info
942                            .local_load_shedding_percentage
943                            .load(Ordering::Relaxed);
944                        !(shedding_percentage > 0 && rng.gen_range(0..100) < shedding_percentage)
945                    } else {
946                        true
947                    }
948                };
949
950            loop {
951                tokio::select! {
952                    now = interval.tick() => {
953                        total_requests += 1;
954                        if do_send(enable_load_shedding, authority.clone()) {
955                            if tx.send(now).is_err() {
956                                info!("Load generator stopping. Total requests {:?}, total dropped requests {:?}.", total_requests, total_dropped_requests);
957                                total_requests_arc.store(total_requests, Ordering::SeqCst);
958                                dropped_requests_arc.store(total_dropped_requests, Ordering::SeqCst);
959                                return;
960                            }
961                            authority.metrics.txn_ready_rate_tracker.lock().record();
962                        } else {
963                            total_dropped_requests += 1;
964                        }
965                    }
966                    Some(burst) = burst_rx.recv() => {
967                        let now = Instant::now();
968                        total_requests += burst;
969                        for _ in 0..burst {
970                            if do_send(enable_load_shedding, authority.clone()) {
971                                if tx.send(now).is_err() {
972                                    info!("Load generator stopping. Total requests {:?}, total dropped requests {:?}.", total_requests, total_dropped_requests);
973                                    total_requests_arc.store(total_requests, Ordering::SeqCst);
974                                    dropped_requests_arc.store(total_dropped_requests, Ordering::SeqCst);
975                                    return;
976                                }
977                                authority.metrics.txn_ready_rate_tracker.lock().record();
978                            } else {
979                                total_dropped_requests += 1;
980                            }
981                        }
982                    }
983                }
984            }
985        })
986    }
987
988    // Starts a request executor that can consume request based on `execution_rate`.
989    // Request tracking is done by the overload monitor inside `authority`.
990    fn start_executor(
991        execution_rate: f64,
992        mut rx: UnboundedReceiver<Instant>,
993        mut stop_rx: oneshot::Receiver<()>,
994        authority: Arc<AuthorityState>,
995    ) -> JoinHandle<()> {
996        tokio::spawn(async move {
997            let mut interval = interval(Duration::from_secs_f64(1.0 / execution_rate));
998            interval.set_missed_tick_behavior(MissedTickBehavior::Skip);
999            loop {
1000                tokio::select! {
1001                    Some(start_time) = rx.recv() => {
1002                        authority.metrics.execution_rate_tracker.lock().record();
1003                        authority.metrics.execution_queueing_latency.report(start_time.elapsed());
1004                        interval.tick().await;
1005                    }
1006                    _ = &mut stop_rx => {
1007                        info!("Executor stopping");
1008                        return;
1009                    }
1010                }
1011            }
1012        })
1013    }
1014
1015    // Helper fundtion to periodically print the current overload info.
1016    async fn sleep_and_print_stats(state: Arc<AuthorityState>, seconds: u32) {
1017        for _ in 0..seconds {
1018            info!(
1019                "Overload: {:?}. Shedding percentage: {:?}. Queue: {:?}, Ready rate: {:?}. Exec rate: {:?}.",
1020                state.overload_info.is_overload.load(Ordering::Relaxed),
1021                state
1022                    .overload_info
1023                    .local_load_shedding_percentage
1024                    .load(Ordering::Relaxed),
1025                state.metrics.execution_queueing_latency.latency(),
1026                state.metrics.txn_ready_rate_tracker.lock().rate(),
1027                state.metrics.execution_rate_tracker.lock().rate(),
1028            );
1029            sleep(Duration::from_secs(1)).await;
1030        }
1031    }
1032
1033    // Running a workload with consistent steady `generator_rate` and
1034    // `executor_rate`. It checks that the dropped requests should in between
1035    // min_dropping_rate and max_dropping_rate.
1036    async fn run_consistent_workload_test(
1037        generator_rate: f64,
1038        executor_rate: f64,
1039        min_dropping_rate: f64,
1040        max_dropping_rate: f64,
1041    ) {
1042        let (state, monitor_handle) = start_overload_monitor().await;
1043
1044        let (tx, rx) = unbounded_channel();
1045        let (_burst_tx, burst_rx) = unbounded_channel();
1046        let total_requests = Arc::new(AtomicU32::new(0));
1047        let dropped_requests = Arc::new(AtomicU32::new(0));
1048        let load_generator = start_load_generator(
1049            generator_rate,
1050            tx.clone(),
1051            burst_rx,
1052            state.clone(),
1053            true,
1054            total_requests.clone(),
1055            dropped_requests.clone(),
1056        );
1057
1058        let (stop_tx, stop_rx) = oneshot::channel();
1059        let executor = start_executor(executor_rate, rx, stop_rx, state.clone());
1060
1061        sleep_and_print_stats(state.clone(), 300).await;
1062
1063        stop_tx.send(()).unwrap();
1064        let _ = tokio::join!(load_generator, executor);
1065
1066        let dropped_ratio = dropped_requests.load(Ordering::SeqCst) as f64
1067            / total_requests.load(Ordering::SeqCst) as f64;
1068        assert!(min_dropping_rate <= dropped_ratio);
1069        assert!(dropped_ratio <= max_dropping_rate);
1070
1071        monitor_handle.abort();
1072        let _ = monitor_handle.await;
1073    }
1074
1075    // Tests that when request generation rate is slower than execution rate, no
1076    // requests should be dropped.
1077    #[tokio::test(flavor = "current_thread", start_paused = true)]
1078    async fn test_workload_consistent_no_overload() {
1079        telemetry_subscribers::init_for_testing();
1080        run_consistent_workload_test(900.0, 1000.0, 0.0, 0.0).await;
1081    }
1082
1083    // Tests that when request generation rate is slightly above execution rate, a
1084    // small portion of requests should be dropped.
1085    #[tokio::test(flavor = "current_thread", start_paused = true)]
1086    async fn test_workload_consistent_slightly_overload() {
1087        telemetry_subscribers::init_for_testing();
1088        // Dropping rate should be around 15%.
1089        run_consistent_workload_test(1100.0, 1000.0, 0.05, 0.25).await;
1090    }
1091
1092    // Tests that when request generation rate is much higher than execution rate, a
1093    // large portion of requests should be dropped.
1094    #[tokio::test(flavor = "current_thread", start_paused = true)]
1095    async fn test_workload_consistent_overload() {
1096        telemetry_subscribers::init_for_testing();
1097        // Dropping rate should be around 70%.
1098        run_consistent_workload_test(3000.0, 1000.0, 0.6, 0.8).await;
1099    }
1100
1101    // Tests that when there is a very short single spike, no request should be
1102    // dropped.
1103    #[tokio::test(flavor = "current_thread", start_paused = true)]
1104    async fn test_workload_single_spike() {
1105        telemetry_subscribers::init_for_testing();
1106        let (state, monitor_handle) = start_overload_monitor().await;
1107
1108        let (tx, rx) = unbounded_channel();
1109        let (burst_tx, burst_rx) = unbounded_channel();
1110        let total_requests = Arc::new(AtomicU32::new(0));
1111        let dropped_requests = Arc::new(AtomicU32::new(0));
1112        let load_generator = start_load_generator(
1113            10.0,
1114            tx.clone(),
1115            burst_rx,
1116            state.clone(),
1117            true,
1118            total_requests.clone(),
1119            dropped_requests.clone(),
1120        );
1121
1122        let (stop_tx, stop_rx) = oneshot::channel();
1123        let executor = start_executor(1000.0, rx, stop_rx, state.clone());
1124
1125        sleep_and_print_stats(state.clone(), 10).await;
1126        // Send out a burst of 5000 requests.
1127        burst_tx.send(5000).unwrap();
1128        sleep_and_print_stats(state.clone(), 20).await;
1129
1130        stop_tx.send(()).unwrap();
1131        let _ = tokio::join!(load_generator, executor);
1132
1133        // No requests should be dropped.
1134        assert_eq!(dropped_requests.load(Ordering::SeqCst), 0);
1135
1136        monitor_handle.abort();
1137        let _ = monitor_handle.await;
1138    }
1139
1140    // Tests that when there are regular spikes that keep queueing latency
1141    // consistently high, overload monitor should kick in and shed load.
1142    #[tokio::test(flavor = "current_thread", start_paused = true)]
1143    async fn test_workload_consistent_short_spike() {
1144        telemetry_subscribers::init_for_testing();
1145        let (state, monitor_handle) = start_overload_monitor().await;
1146
1147        let (tx, rx) = unbounded_channel();
1148        let (burst_tx, burst_rx) = unbounded_channel();
1149        let total_requests = Arc::new(AtomicU32::new(0));
1150        let dropped_requests = Arc::new(AtomicU32::new(0));
1151        let load_generator = start_load_generator(
1152            10.0,
1153            tx.clone(),
1154            burst_rx,
1155            state.clone(),
1156            true,
1157            total_requests.clone(),
1158            dropped_requests.clone(),
1159        );
1160
1161        let (stop_tx, stop_rx) = oneshot::channel();
1162        let executor = start_executor(1000.0, rx, stop_rx, state.clone());
1163
1164        sleep_and_print_stats(state.clone(), 15).await;
1165        for _ in 0..16 {
1166            // Regularly send out a burst of request.
1167            burst_tx.send(10000).unwrap();
1168            sleep_and_print_stats(state.clone(), 5).await;
1169        }
1170
1171        stop_tx.send(()).unwrap();
1172        let _ = tokio::join!(load_generator, executor);
1173        let dropped_ratio = dropped_requests.load(Ordering::SeqCst) as f64
1174            / total_requests.load(Ordering::SeqCst) as f64;
1175
1176        // We should drop about 50% of request because the burst throughput is about 2x
1177        // of execution rate.
1178        assert!(0.4 < dropped_ratio);
1179        assert!(dropped_ratio < 0.6);
1180
1181        monitor_handle.abort();
1182        let _ = monitor_handle.await;
1183    }
1184
1185    // Tests that the ratio of rejected transactions created randomly matches load
1186    // shedding percentage in the overload monitor.
1187    #[test]
1188    fn test_txn_rejection_rate() {
1189        for rejection_percentage in 0..=100 {
1190            let mut reject_count = 0;
1191            for _ in 0..10000 {
1192                let digest = TransactionDigest::random();
1193                if should_reject_tx(rejection_percentage, digest, 28455473) {
1194                    reject_count += 1;
1195                }
1196            }
1197
1198            debug!(
1199                "Rejection percentage: {:?}, reject count: {:?}.",
1200                rejection_percentage, reject_count
1201            );
1202            // Give it a 3% fluctuation.
1203            assert!(rejection_percentage as f32 / 100.0 - 0.03 < reject_count as f32 / 10000.0);
1204            assert!(reject_count as f32 / 10000.0 < rejection_percentage as f32 / 100.0 + 0.03);
1205        }
1206    }
1207
1208    // Tests that rejected transaction will have a chance to be accepted in the
1209    // future.
1210    #[sim_test]
1211    async fn test_txn_rejection_over_time() {
1212        let start_time = Instant::now();
1213        let mut digest = TransactionDigest::random();
1214        let mut temporal_seed = 1708108277 / SEED_UPDATE_DURATION_SECS;
1215        let load_shedding_percentage = 50;
1216
1217        // Find a rejected transaction with 50% rejection rate.
1218        while !should_reject_tx(load_shedding_percentage, digest, temporal_seed)
1219            && start_time.elapsed() < Duration::from_secs(30)
1220        {
1221            digest = TransactionDigest::random();
1222        }
1223
1224        // It should always be rejected using the current temporal_seed.
1225        for _ in 0..100 {
1226            assert!(should_reject_tx(
1227                load_shedding_percentage,
1228                digest,
1229                temporal_seed
1230            ));
1231        }
1232
1233        // It will be accepted in the future.
1234        temporal_seed += 1;
1235        while should_reject_tx(load_shedding_percentage, digest, temporal_seed)
1236            && start_time.elapsed() < Duration::from_secs(30)
1237        {
1238            temporal_seed += 1;
1239        }
1240
1241        // Make sure that the tests can finish within 30 seconds.
1242        assert!(start_time.elapsed() < Duration::from_secs(30));
1243    }
1244}