Skip to main content

ipfrs_network/
adaptive_timeout.rs

1//! Per-peer adaptive timeout estimation using the TCP-inspired SRTT/RTTVAR algorithm.
2//!
3//! This module dynamically adjusts per-peer operation timeouts based on observed RTT history,
4//! ensuring timeouts are neither too aggressive nor too conservative. The algorithm is modelled
5//! after RFC 6298 (TCP retransmission timer computation).
6//!
7//! # Algorithm
8//!
9//! On the **first** RTT sample for a peer:
10//! ```text
11//! SRTT   = R
12//! RTTVAR = R / 2
13//! ```
14//!
15//! On every **subsequent** sample:
16//! ```text
17//! RTTVAR = (1 - β) * RTTVAR + β * |SRTT - R|
18//! SRTT   = (1 - α) * SRTT   + α * R
19//! RTO    = SRTT + 4 * RTTVAR
20//! ```
21//!
22//! where α = 1/8 and β = 1/4 by default (RFC 6298 recommendations).
23
24use std::collections::HashMap;
25
26/// A single RTT measurement for a specific peer.
27#[derive(Debug, Clone, PartialEq, Eq)]
28pub struct RttSample {
29    /// Identifies the remote peer.
30    pub peer_id: String,
31    /// Round-trip time in microseconds.
32    pub rtt_micros: u64,
33    /// Logical clock tick at which the sample was taken (application-defined).
34    pub sampled_at_tick: u64,
35}
36
37/// The current timeout estimate and smoothed RTT state for a single peer.
38#[derive(Debug, Clone)]
39pub struct TimeoutEstimate {
40    /// Identifies the remote peer.
41    pub peer_id: String,
42    /// Exponentially-weighted moving average of the RTT (µs).
43    pub srtt_micros: f64,
44    /// Mean deviation of the RTT (µs), used as a variance proxy.
45    pub rttvar_micros: f64,
46    /// Recommended timeout in microseconds: `srtt + 4 * rttvar`, clamped to [`AdaptiveTimeoutConfig::min_timeout_micros`] .. [`AdaptiveTimeoutConfig::max_timeout_micros`].
47    pub timeout_micros: u64,
48    /// Number of RTT samples recorded for this peer.
49    pub sample_count: u32,
50}
51
52/// Configuration for [`PeerAdaptiveTimeout`].
53#[derive(Debug, Clone)]
54pub struct AdaptiveTimeoutConfig {
55    /// SRTT smoothing factor (α). RFC 6298 recommends 1/8.
56    pub alpha: f64,
57    /// RTTVAR smoothing factor (β). RFC 6298 recommends 1/4.
58    pub beta: f64,
59    /// Minimum allowed timeout in microseconds (floor). Default: 1 000 µs = 1 ms.
60    pub min_timeout_micros: u64,
61    /// Maximum allowed timeout in microseconds (ceiling). Default: 30 000 000 µs = 30 s.
62    pub max_timeout_micros: u64,
63}
64
65impl Default for AdaptiveTimeoutConfig {
66    fn default() -> Self {
67        Self {
68            alpha: 0.125,
69            beta: 0.25,
70            min_timeout_micros: 1_000,
71            max_timeout_micros: 30_000_000,
72        }
73    }
74}
75
76/// Aggregate statistics across all tracked peers.
77#[derive(Debug, Clone)]
78pub struct TimeoutStats {
79    /// Number of distinct peers currently tracked.
80    pub total_peers: usize,
81    /// Total RTT samples recorded across all peers.
82    pub total_samples: u64,
83    /// Mean recommended timeout across all tracked peers (µs). 0.0 when no peers are tracked.
84    pub avg_timeout_micros: f64,
85}
86
87/// Per-peer adaptive timeout manager.
88///
89/// Maintains a [`TimeoutEstimate`] for every peer that has provided at least one RTT sample and
90/// exposes methods to query the recommended timeout or remove stale peers.
91#[derive(Debug)]
92pub struct PeerAdaptiveTimeout {
93    /// Map from peer_id to its current timeout estimate.
94    pub estimates: HashMap<String, TimeoutEstimate>,
95    /// Algorithm configuration.
96    pub config: AdaptiveTimeoutConfig,
97    /// Total RTT samples recorded across all peers (monotonically increasing).
98    pub total_samples: u64,
99}
100
101impl PeerAdaptiveTimeout {
102    /// Create a new [`PeerAdaptiveTimeout`] with the given configuration.
103    pub fn new(config: AdaptiveTimeoutConfig) -> Self {
104        Self {
105            estimates: HashMap::new(),
106            config,
107            total_samples: 0,
108        }
109    }
110
111    /// Record a new RTT sample and update the timeout estimate for the associated peer.
112    ///
113    /// # First sample
114    ///
115    /// ```text
116    /// SRTT   = rtt
117    /// RTTVAR = rtt / 2
118    /// ```
119    ///
120    /// # Subsequent samples
121    ///
122    /// ```text
123    /// RTTVAR = (1 - β) * RTTVAR + β * |SRTT - rtt|
124    /// SRTT   = (1 - α) * SRTT   + α * rtt
125    /// ```
126    pub fn record_sample(&mut self, sample: RttSample) {
127        let rtt = sample.rtt_micros as f64;
128        let alpha = self.config.alpha;
129        let beta = self.config.beta;
130        let min_t = self.config.min_timeout_micros;
131        let max_t = self.config.max_timeout_micros;
132
133        let estimate = self
134            .estimates
135            .entry(sample.peer_id.clone())
136            .or_insert_with(|| TimeoutEstimate {
137                peer_id: sample.peer_id.clone(),
138                srtt_micros: 0.0,
139                rttvar_micros: 0.0,
140                timeout_micros: 0,
141                sample_count: 0,
142            });
143
144        if estimate.sample_count == 0 {
145            // Initialise on the very first sample.
146            estimate.srtt_micros = rtt;
147            estimate.rttvar_micros = rtt / 2.0;
148        } else {
149            // RFC 6298 §2: update RTTVAR before SRTT so the deviation uses the *previous* SRTT.
150            let diff = (estimate.srtt_micros - rtt).abs();
151            estimate.rttvar_micros = (1.0 - beta) * estimate.rttvar_micros + beta * diff;
152            estimate.srtt_micros = (1.0 - alpha) * estimate.srtt_micros + alpha * rtt;
153        }
154
155        let raw_timeout = estimate.srtt_micros + 4.0 * estimate.rttvar_micros;
156        let clamped = raw_timeout.max(min_t as f64).min(max_t as f64) as u64;
157        estimate.timeout_micros = clamped.clamp(min_t, max_t);
158
159        estimate.sample_count += 1;
160        self.total_samples += 1;
161    }
162
163    /// Returns the recommended timeout for `peer_id` if it has been tracked, or `None` otherwise.
164    pub fn timeout_for(&self, peer_id: &str) -> Option<u64> {
165        self.estimates.get(peer_id).map(|e| e.timeout_micros)
166    }
167
168    /// Returns the recommended timeout for `peer_id`, falling back to `max_timeout_micros` for
169    /// unknown peers (conservative default).
170    pub fn timeout_or_default(&self, peer_id: &str) -> u64 {
171        self.timeout_for(peer_id)
172            .unwrap_or(self.config.max_timeout_micros)
173    }
174
175    /// Remove all state for `peer_id`.
176    ///
177    /// Returns `true` if the peer was tracked and has been removed, `false` if it was unknown.
178    pub fn remove_peer(&mut self, peer_id: &str) -> bool {
179        self.estimates.remove(peer_id).is_some()
180    }
181
182    /// Compute aggregate statistics over all currently tracked peers.
183    pub fn stats(&self) -> TimeoutStats {
184        let total_peers = self.estimates.len();
185        let avg_timeout_micros = if total_peers == 0 {
186            0.0
187        } else {
188            let sum: f64 = self
189                .estimates
190                .values()
191                .map(|e| e.timeout_micros as f64)
192                .sum();
193            sum / total_peers as f64
194        };
195        TimeoutStats {
196            total_peers,
197            total_samples: self.total_samples,
198            avg_timeout_micros,
199        }
200    }
201}
202
203// ─────────────────────────────────────────────────────────────────────────────
204// Tests
205// ─────────────────────────────────────────────────────────────────────────────
206
207#[cfg(test)]
208mod tests {
209    use super::*;
210
211    fn default_pat() -> PeerAdaptiveTimeout {
212        PeerAdaptiveTimeout::new(AdaptiveTimeoutConfig::default())
213    }
214
215    fn sample(peer: &str, rtt: u64) -> RttSample {
216        RttSample {
217            peer_id: peer.to_string(),
218            rtt_micros: rtt,
219            sampled_at_tick: 0,
220        }
221    }
222
223    // ── 1. First sample initialises SRTT and RTTVAR correctly ──────────────
224
225    #[test]
226    fn test_first_sample_srtt() {
227        let mut pat = default_pat();
228        pat.record_sample(sample("peer-a", 10_000));
229        let est = pat
230            .estimates
231            .get("peer-a")
232            .expect("test: peer-a estimate should exist");
233        assert!(
234            (est.srtt_micros - 10_000.0).abs() < f64::EPSILON,
235            "SRTT should equal first RTT"
236        );
237    }
238
239    #[test]
240    fn test_first_sample_rttvar() {
241        let mut pat = default_pat();
242        pat.record_sample(sample("peer-a", 10_000));
243        let est = pat
244            .estimates
245            .get("peer-a")
246            .expect("test: peer-a estimate should exist");
247        assert!(
248            (est.rttvar_micros - 5_000.0).abs() < f64::EPSILON,
249            "RTTVAR should be RTT/2 on first sample"
250        );
251    }
252
253    #[test]
254    fn test_first_sample_timeout_formula() {
255        let mut pat = default_pat();
256        pat.record_sample(sample("peer-a", 10_000));
257        let est = pat
258            .estimates
259            .get("peer-a")
260            .expect("test: peer-a estimate should exist");
261        // timeout = srtt + 4*rttvar = 10000 + 4*5000 = 30000
262        assert_eq!(
263            est.timeout_micros, 30_000,
264            "First-sample timeout = srtt + 4*rttvar"
265        );
266    }
267
268    // ── 2. Second sample SRTT via EWMA ─────────────────────────────────────
269
270    #[test]
271    fn test_second_sample_srtt_ewma() {
272        let mut pat = default_pat();
273        pat.record_sample(sample("peer-a", 8_000));
274        pat.record_sample(sample("peer-a", 12_000));
275        let est = pat
276            .estimates
277            .get("peer-a")
278            .expect("test: peer-a estimate should exist");
279        // SRTT = (1 - 0.125) * 8000 + 0.125 * 12000 = 7000 + 1500 = 8500
280        let expected_srtt = (1.0 - 0.125_f64) * 8_000.0 + 0.125 * 12_000.0;
281        assert!(
282            (est.srtt_micros - expected_srtt).abs() < 1.0,
283            "SRTT EWMA mismatch: got {}, expected {}",
284            est.srtt_micros,
285            expected_srtt
286        );
287    }
288
289    // ── 3. Timeout = SRTT + 4*RTTVAR ──────────────────────────────────────
290
291    #[test]
292    fn test_timeout_formula_after_two_samples() {
293        let mut pat = default_pat();
294        pat.record_sample(sample("peer-a", 8_000));
295        pat.record_sample(sample("peer-a", 12_000));
296        let est = pat
297            .estimates
298            .get("peer-a")
299            .expect("test: peer-a estimate should exist");
300        let expected = (est.srtt_micros + 4.0 * est.rttvar_micros) as u64;
301        assert_eq!(
302            est.timeout_micros, expected,
303            "timeout_micros should equal srtt + 4*rttvar"
304        );
305    }
306
307    // ── 4. Min clamping ────────────────────────────────────────────────────
308
309    #[test]
310    fn test_min_timeout_clamping() {
311        let config = AdaptiveTimeoutConfig {
312            min_timeout_micros: 50_000,
313            ..Default::default()
314        };
315        let mut pat = PeerAdaptiveTimeout::new(config);
316        // Tiny RTT — raw timeout will be 1 µs + 4*0.5 µs = 3 µs < 50_000
317        pat.record_sample(sample("peer-a", 1));
318        let t = pat
319            .timeout_for("peer-a")
320            .expect("test: peer-a timeout should exist after sample");
321        assert_eq!(t, 50_000, "Timeout should be clamped to min_timeout_micros");
322    }
323
324    // ── 5. Max clamping ────────────────────────────────────────────────────
325
326    #[test]
327    fn test_max_timeout_clamping() {
328        let config = AdaptiveTimeoutConfig {
329            max_timeout_micros: 5_000,
330            ..Default::default()
331        };
332        let mut pat = PeerAdaptiveTimeout::new(config);
333        // Huge RTT — raw timeout far exceeds 5_000
334        pat.record_sample(sample("peer-a", 1_000_000));
335        let t = pat
336            .timeout_for("peer-a")
337            .expect("test: peer-a timeout should exist after sample");
338        assert_eq!(t, 5_000, "Timeout should be clamped to max_timeout_micros");
339    }
340
341    // ── 6. remove_peer returns true when peer existed ──────────────────────
342
343    #[test]
344    fn test_remove_peer_existing() {
345        let mut pat = default_pat();
346        pat.record_sample(sample("peer-a", 1_000));
347        assert!(
348            pat.remove_peer("peer-a"),
349            "Should return true for known peer"
350        );
351        assert!(
352            !pat.estimates.contains_key("peer-a"),
353            "Peer should be gone after removal"
354        );
355    }
356
357    // ── 7. remove_peer returns false for unknown peer ──────────────────────
358
359    #[test]
360    fn test_remove_peer_unknown() {
361        let mut pat = default_pat();
362        assert!(
363            !pat.remove_peer("ghost"),
364            "Should return false for unknown peer"
365        );
366    }
367
368    // ── 8. timeout_or_default returns max for unknown peer ─────────────────
369
370    #[test]
371    fn test_timeout_or_default_unknown() {
372        let pat = default_pat();
373        assert_eq!(
374            pat.timeout_or_default("nobody"),
375            pat.config.max_timeout_micros,
376            "Unknown peer should receive max timeout"
377        );
378    }
379
380    // ── 9. timeout_for returns None for unknown peer ───────────────────────
381
382    #[test]
383    fn test_timeout_for_unknown() {
384        let pat = default_pat();
385        assert!(pat.timeout_for("nobody").is_none());
386    }
387
388    // ── 10. stats avg_timeout_micros ──────────────────────────────────────
389
390    #[test]
391    fn test_stats_avg_timeout() {
392        let mut pat = default_pat();
393        pat.record_sample(sample("peer-a", 10_000));
394        pat.record_sample(sample("peer-b", 20_000));
395        let stats = pat.stats();
396        let ta = pat.estimates["peer-a"].timeout_micros as f64;
397        let tb = pat.estimates["peer-b"].timeout_micros as f64;
398        let expected_avg = (ta + tb) / 2.0;
399        assert!(
400            (stats.avg_timeout_micros - expected_avg).abs() < 1.0,
401            "avg_timeout_micros mismatch"
402        );
403    }
404
405    // ── 11. stats total_peers ─────────────────────────────────────────────
406
407    #[test]
408    fn test_stats_total_peers() {
409        let mut pat = default_pat();
410        pat.record_sample(sample("peer-a", 5_000));
411        pat.record_sample(sample("peer-b", 5_000));
412        assert_eq!(pat.stats().total_peers, 2);
413    }
414
415    // ── 12. stats total_samples ───────────────────────────────────────────
416
417    #[test]
418    fn test_stats_total_samples() {
419        let mut pat = default_pat();
420        pat.record_sample(sample("peer-a", 1_000));
421        pat.record_sample(sample("peer-a", 2_000));
422        pat.record_sample(sample("peer-b", 3_000));
423        assert_eq!(pat.stats().total_samples, 3);
424    }
425
426    // ── 13. Multiple peers tracked independently ──────────────────────────
427
428    #[test]
429    fn test_multiple_peers_independent() {
430        let mut pat = default_pat();
431        pat.record_sample(sample("peer-a", 1_000));
432        pat.record_sample(sample("peer-b", 100_000));
433        let ta = pat
434            .timeout_for("peer-a")
435            .expect("test: peer-a timeout should exist");
436        let tb = pat
437            .timeout_for("peer-b")
438            .expect("test: peer-b timeout should exist");
439        assert!(
440            ta < tb,
441            "Peer with lower RTT should have lower timeout: {} vs {}",
442            ta,
443            tb
444        );
445    }
446
447    // ── 14. sample_count increments correctly ────────────────────────────
448
449    #[test]
450    fn test_sample_count_increments() {
451        let mut pat = default_pat();
452        for i in 0..5u64 {
453            pat.record_sample(sample("peer-a", 1_000 * (i + 1)));
454        }
455        assert_eq!(pat.estimates["peer-a"].sample_count, 5);
456    }
457
458    // ── 15. total_samples increments across peers ─────────────────────────
459
460    #[test]
461    fn test_total_samples_across_peers() {
462        let mut pat = default_pat();
463        pat.record_sample(sample("peer-a", 1_000));
464        pat.record_sample(sample("peer-b", 2_000));
465        pat.record_sample(sample("peer-c", 3_000));
466        assert_eq!(pat.total_samples, 3);
467    }
468
469    // ── 16. Stable RTT converges timeout toward RTT ───────────────────────
470
471    #[test]
472    fn test_stable_rtt_converges() {
473        let mut pat = default_pat();
474        let stable_rtt = 20_000u64;
475        // After many identical samples SRTT → rtt, RTTVAR → 0, timeout → rtt (floored at min)
476        for _ in 0..200 {
477            pat.record_sample(sample("peer-a", stable_rtt));
478        }
479        let est = pat
480            .estimates
481            .get("peer-a")
482            .expect("test: peer-a estimate should exist after convergence");
483        let srtt_err = (est.srtt_micros - stable_rtt as f64).abs();
484        assert!(
485            srtt_err < 1.0,
486            "SRTT should converge to stable RTT; error={srtt_err}"
487        );
488        assert!(
489            est.rttvar_micros < 10.0,
490            "RTTVAR should converge to near-zero; got {}",
491            est.rttvar_micros
492        );
493        assert!(
494            est.timeout_micros < stable_rtt + 100,
495            "Timeout should converge near RTT; got {}",
496            est.timeout_micros
497        );
498    }
499
500    // ── 17. Custom alpha/beta configuration ──────────────────────────────
501
502    #[test]
503    fn test_custom_alpha_beta() {
504        let config = AdaptiveTimeoutConfig {
505            alpha: 0.5,
506            beta: 0.5,
507            ..Default::default()
508        };
509        let mut pat = PeerAdaptiveTimeout::new(config);
510        pat.record_sample(sample("peer-a", 10_000));
511        pat.record_sample(sample("peer-a", 20_000));
512        let est = pat
513            .estimates
514            .get("peer-a")
515            .expect("test: peer-a estimate should exist");
516        // SRTT = (1-0.5)*10000 + 0.5*20000 = 15000
517        assert!((est.srtt_micros - 15_000.0).abs() < 1.0);
518    }
519
520    // ── 18. Remove peer then re-add resets state ──────────────────────────
521
522    #[test]
523    fn test_remove_then_readd_resets_state() {
524        let mut pat = default_pat();
525        for _ in 0..10 {
526            pat.record_sample(sample("peer-a", 5_000));
527        }
528        pat.remove_peer("peer-a");
529        pat.record_sample(sample("peer-a", 100_000));
530        let est = pat
531            .estimates
532            .get("peer-a")
533            .expect("test: peer-a estimate should exist after re-add");
534        // Should be initialised fresh: srtt = 100_000, rttvar = 50_000
535        assert_eq!(est.sample_count, 1);
536        assert!((est.srtt_micros - 100_000.0).abs() < 1.0);
537        assert!((est.rttvar_micros - 50_000.0).abs() < 1.0);
538    }
539
540    // ── 19. Empty tracker stats ───────────────────────────────────────────
541
542    #[test]
543    fn test_empty_stats() {
544        let pat = default_pat();
545        let stats = pat.stats();
546        assert_eq!(stats.total_peers, 0);
547        assert_eq!(stats.total_samples, 0);
548        assert!((stats.avg_timeout_micros - 0.0).abs() < f64::EPSILON);
549    }
550
551    // ── 20. timeout_or_default for known peer returns peer timeout ─────────
552
553    #[test]
554    fn test_timeout_or_default_known() {
555        let mut pat = default_pat();
556        pat.record_sample(sample("peer-a", 5_000));
557        let expected = pat
558            .timeout_for("peer-a")
559            .expect("test: peer-a timeout should exist");
560        assert_eq!(pat.timeout_or_default("peer-a"), expected);
561    }
562
563    // ── 21. Monotonic total_samples ───────────────────────────────────────
564
565    #[test]
566    fn test_total_samples_monotone() {
567        let mut pat = default_pat();
568        for i in 0..10u64 {
569            pat.record_sample(sample("peer-a", 1_000 * (i + 1)));
570            assert_eq!(pat.total_samples, i + 1);
571        }
572    }
573
574    // ── 22. High-RTT variance is captured in RTTVAR ───────────────────────
575
576    #[test]
577    fn test_high_variance_rttvar() {
578        let mut pat = default_pat();
579        pat.record_sample(sample("peer-a", 1_000));
580        pat.record_sample(sample("peer-a", 100_000));
581        let est = pat
582            .estimates
583            .get("peer-a")
584            .expect("test: peer-a estimate should exist");
585        // The deviation between 1_000 and 100_000 is large — RTTVAR should be substantial.
586        assert!(
587            est.rttvar_micros > 10_000.0,
588            "RTTVAR should reflect high variance; got {}",
589            est.rttvar_micros
590        );
591    }
592
593    // ── 23. Minimum timeout respected even for tiny RTT ──────────────────
594
595    #[test]
596    fn test_min_floor_tiny_rtt() {
597        let mut pat = default_pat(); // min = 1_000
598        pat.record_sample(sample("peer-a", 1)); // rtt = 1 µs
599        assert!(
600            pat.timeout_for("peer-a")
601                .expect("test: peer-a timeout should be present")
602                >= 1_000,
603            "Timeout must not fall below min"
604        );
605    }
606
607    // ── 24. Successive samples do not regress below min ───────────────────
608
609    #[test]
610    fn test_timeout_never_below_min_over_many_samples() {
611        let mut pat = default_pat();
612        for i in 0..50u64 {
613            pat.record_sample(sample("peer-a", i + 1));
614        }
615        let t = pat
616            .timeout_for("peer-a")
617            .expect("test: peer-a timeout should be present");
618        assert!(t >= pat.config.min_timeout_micros);
619    }
620
621    // ── 25. Successive samples do not exceed max ──────────────────────────
622
623    #[test]
624    fn test_timeout_never_above_max_over_many_samples() {
625        let config = AdaptiveTimeoutConfig {
626            max_timeout_micros: 10_000,
627            ..Default::default()
628        };
629        let mut pat = PeerAdaptiveTimeout::new(config.clone());
630        for i in 0..50u64 {
631            pat.record_sample(sample("peer-a", 1_000_000 * (i + 1)));
632        }
633        let t = pat
634            .timeout_for("peer-a")
635            .expect("test: peer-a timeout should exist");
636        assert!(
637            t <= config.max_timeout_micros,
638            "Timeout must not exceed max; got {t}"
639        );
640    }
641
642    // ── 26. Stats after remove_peer reflect the removal ──────────────────
643
644    #[test]
645    fn test_stats_after_remove() {
646        let mut pat = default_pat();
647        pat.record_sample(sample("peer-a", 5_000));
648        pat.record_sample(sample("peer-b", 5_000));
649        pat.remove_peer("peer-a");
650        let stats = pat.stats();
651        assert_eq!(stats.total_peers, 1, "Only one peer should remain");
652        // total_samples should not change after removal
653        assert_eq!(stats.total_samples, 2, "total_samples is cumulative");
654    }
655
656    // ── 27. RTTVAR update order: deviation uses previous SRTT ─────────────
657
658    #[test]
659    fn test_rttvar_update_uses_prev_srtt() {
660        let mut pat = default_pat();
661        pat.record_sample(sample("peer-a", 8_000));
662        // After first: srtt = 8000, rttvar = 4000
663        pat.record_sample(sample("peer-a", 12_000));
664        let est = pat
665            .estimates
666            .get("peer-a")
667            .expect("test: peer-a estimate should exist");
668        // |srtt_prev - rtt| = |8000 - 12000| = 4000
669        // rttvar = (1-0.25)*4000 + 0.25*4000 = 4000 exactly (no change when diff = rttvar)
670        let expected_rttvar = (1.0 - 0.25_f64) * 4_000.0 + 0.25 * (8_000.0 - 12_000.0_f64).abs();
671        assert!(
672            (est.rttvar_micros - expected_rttvar).abs() < 1.0,
673            "RTTVAR mismatch: {} vs {}",
674            est.rttvar_micros,
675            expected_rttvar
676        );
677    }
678}