ant_networking/
config.rs

1// Copyright 2024 MaidSafe.net limited.
2//
3// This SAFE Network Software is licensed to you under The General Public License (GPL), version 3.
4// Unless required by applicable law or agreed to in writing, the SAFE Network Software distributed
5// under the GPL Licence is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
6// KIND, either express or implied. Please review the Licences for the specific language governing
7// permissions and limitations relating to use of the SAFE Network Software.
8
9use ant_protocol::{
10    messages::{ChunkProof, Nonce},
11    PrettyPrintRecordKey, CLOSE_GROUP_SIZE,
12};
13use core::fmt::{self, Debug};
14use exponential_backoff::Backoff;
15use libp2p::{
16    kad::{Quorum, Record},
17    PeerId,
18};
19use std::{collections::HashSet, num::NonZeroUsize, time::Duration};
20
21use crate::close_group_majority;
22
23/// A strategy that translates into a configuration for exponential backoff.
24/// The first retry is done after 2 seconds, after which the backoff is roughly doubled each time.
25/// The interval does not go beyond 32 seconds. So the intervals increase from 2 to 4, to 8, to 16, to 32 seconds and
26/// all attempts are made at most 32 seconds apart.
27///
28/// The exact timings depend on jitter, which is set to 0.2, meaning the intervals can deviate quite a bit
29/// from the ones listed in the docs.
30///
31/// The default strategy is `Balanced`.
32#[derive(Clone, Debug, Copy, Default)]
33pub enum RetryStrategy {
34    /// Attempt once (no retries)
35    None,
36    /// Retry 3 times (waits 2s, 4s and lastly 8s; max total time ~14s)
37    Quick,
38    /// Retry 5 times (waits 2s, 4s, 8s, 16s and lastly 32s; max total time ~62s)
39    #[default]
40    Balanced,
41    /// Retry 9 times (waits 2s, 4s, 8s, 16s, 32s, 32s, 32s, 32s and lastly 32s; max total time ~190s)
42    Persistent,
43    /// Attempt a specific number of times
44    N(NonZeroUsize),
45}
46
47impl RetryStrategy {
48    pub fn attempts(&self) -> usize {
49        match self {
50            RetryStrategy::None => 1,
51            RetryStrategy::Quick => 4,
52            RetryStrategy::Balanced => 6,
53            RetryStrategy::Persistent => 10,
54            RetryStrategy::N(x) => x.get(),
55        }
56    }
57
58    pub fn backoff(&self) -> Backoff {
59        let mut backoff = Backoff::new(
60            self.attempts() as u32,
61            Duration::from_secs(1), // First interval is double of this (see https://github.com/yoshuawuyts/exponential-backoff/issues/23)
62            Some(Duration::from_secs(32)),
63        );
64        backoff.set_factor(2); // Default.
65        backoff.set_jitter(0.2); // Default is 0.3.
66        backoff
67    }
68}
69
70impl fmt::Display for RetryStrategy {
71    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
72        write!(f, "{self:?}")
73    }
74}
75
76/// Specifies the minimum number of distinct nodes that must be successfully contacted in order for a query to succeed.
77#[derive(Debug, Copy, Clone, PartialEq, Eq)]
78pub enum ResponseQuorum {
79    One,
80    Majority,
81    All,
82    N(NonZeroUsize),
83}
84
85impl std::str::FromStr for ResponseQuorum {
86    type Err = String;
87
88    fn from_str(s: &str) -> Result<Self, Self::Err> {
89        match s {
90            "one" => Ok(ResponseQuorum::One),
91            "majority" => Ok(ResponseQuorum::Majority),
92            "all" => Ok(ResponseQuorum::All),
93            _ => {
94                if let Ok(n) = s.parse::<usize>() {
95                    let n = NonZeroUsize::new(n);
96                    match n {
97                        Some(n) => Ok(ResponseQuorum::N(n)),
98                        None => Err("Quorum value must be greater than 0".to_string()),
99                    }
100                } else {
101                    Err("Invalid quorum value".to_string())
102                }
103            }
104        }
105    }
106}
107
108impl ResponseQuorum {
109    pub(crate) fn get_kad_quorum(&self) -> Quorum {
110        match self {
111            ResponseQuorum::One => Quorum::One,
112            ResponseQuorum::Majority => Quorum::Majority,
113            ResponseQuorum::All => Quorum::All,
114            ResponseQuorum::N(n) => Quorum::N(*n),
115        }
116    }
117
118    /// Get the value of the provided Quorum
119    pub fn get_value(&self) -> usize {
120        match self {
121            ResponseQuorum::Majority => close_group_majority(),
122            ResponseQuorum::All => CLOSE_GROUP_SIZE,
123            ResponseQuorum::N(v) => v.get(),
124            ResponseQuorum::One => 1,
125        }
126    }
127}
128
129/// The various settings to apply to when fetching a record from network
130#[derive(Clone)]
131pub struct GetRecordCfg {
132    /// The query will result in an error if we get records less than the provided Quorum
133    pub get_quorum: ResponseQuorum,
134    /// If enabled, the provided `RetryStrategy` is used to retry if a GET attempt fails.
135    pub retry_strategy: RetryStrategy,
136    /// Only return if we fetch the provided record.
137    pub target_record: Option<Record>,
138    /// Logs if the record was not fetched from the provided set of peers.
139    pub expected_holders: HashSet<PeerId>,
140}
141
142impl GetRecordCfg {
143    pub fn does_target_match(&self, record: &Record) -> bool {
144        if let Some(ref target_record) = self.target_record {
145            target_record == record
146        } else {
147            // Not have target_record to check with
148            true
149        }
150    }
151}
152
153impl Debug for GetRecordCfg {
154    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
155        let mut f = f.debug_struct("GetRecordCfg");
156        f.field("get_quorum", &self.get_quorum)
157            .field("retry_strategy", &self.retry_strategy);
158
159        match &self.target_record {
160            Some(record) => {
161                let pretty_key = PrettyPrintRecordKey::from(&record.key);
162                f.field("target_record", &pretty_key);
163            }
164            None => {
165                f.field("target_record", &"None");
166            }
167        };
168
169        f.field("expected_holders", &self.expected_holders).finish()
170    }
171}
172
173/// The various settings related to writing a record to the network.
174#[derive(Debug, Clone)]
175pub struct PutRecordCfg {
176    /// The quorum used by KAD PUT. KAD still sends out the request to all the peers set by the `replication_factor`, it
177    /// just makes sure that we get atleast `n` successful responses defined by the Quorum.
178    /// Our nodes currently send `Ok()` response for every KAD PUT. Thus this field does not do anything atm.
179    pub put_quorum: ResponseQuorum,
180    /// If enabled, the provided `RetryStrategy` is used to retry if a PUT attempt fails.
181    pub retry_strategy: RetryStrategy,
182    /// Use the `kad::put_record_to` to PUT the record only to the specified peers. If this option is set to None, we
183    /// will be using `kad::put_record` which would PUT the record to all the closest members of the record.
184    pub use_put_record_to: Option<Vec<PeerId>>,
185    /// Enables verification after writing. The VerificationKind is used to determine the method to use.
186    pub verification: Option<(VerificationKind, GetRecordCfg)>,
187}
188
189/// The methods in which verification on a PUT can be carried out.
190#[derive(Debug, Clone)]
191pub enum VerificationKind {
192    /// Uses the default KAD GET to perform verification.
193    Network,
194    /// Uses the default KAD GET to perform verification, but don't error out on split records
195    Crdt,
196    /// Uses the hash based verification for chunks.
197    ChunkProof {
198        expected_proof: ChunkProof,
199        nonce: Nonce,
200    },
201}
202
203#[test]
204fn verify_retry_strategy_intervals() {
205    let intervals = |strategy: RetryStrategy| -> Vec<u32> {
206        let mut backoff = strategy.backoff();
207        backoff.set_jitter(0.01); // Make intervals deterministic.
208        backoff
209            .into_iter()
210            .flatten()
211            .map(|duration| duration.as_secs_f64().round() as u32)
212            .collect()
213    };
214
215    assert_eq!(intervals(RetryStrategy::None), Vec::<u32>::new());
216    assert_eq!(intervals(RetryStrategy::Quick), vec![2, 4, 8]);
217    assert_eq!(intervals(RetryStrategy::Balanced), vec![2, 4, 8, 16, 32]);
218    assert_eq!(
219        intervals(RetryStrategy::Persistent),
220        vec![2, 4, 8, 16, 32, 32, 32, 32, 32]
221    );
222    assert_eq!(
223        intervals(RetryStrategy::N(NonZeroUsize::new(12).unwrap())),
224        vec![2, 4, 8, 16, 32, 32, 32, 32, 32, 32, 32]
225    );
226}