Skip to main content

iggy_cli/commands/binary_system/
ping.rs

1/* Licensed to the Apache Software Foundation (ASF) under one
2 * or more contributor license agreements.  See the NOTICE file
3 * distributed with this work for additional information
4 * regarding copyright ownership.  The ASF licenses this file
5 * to you under the Apache License, Version 2.0 (the
6 * "License"); you may not use this file except in compliance
7 * with the License.  You may obtain a copy of the License at
8 *
9 *   http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing,
12 * software distributed under the License is distributed on an
13 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14 * KIND, either express or implied.  See the License for the
15 * specific language governing permissions and limitations
16 * under the License.
17 */
18
19use crate::commands::cli_command::{CliCommand, PRINT_TARGET};
20use anyhow::Context;
21use async_trait::async_trait;
22use iggy_common::Client;
23use std::fmt::{Display, Formatter, Result};
24use std::time::Duration;
25use tokio::time::{Instant, sleep};
26use tracing::{Level, event};
27
28pub struct PingCmd {
29    count: u32,
30}
31
32impl PingCmd {
33    pub fn new(count: u32) -> Self {
34        Self { count }
35    }
36}
37
38struct PingStats {
39    samples: Vec<u128>,
40}
41
42impl PingStats {
43    fn new() -> Self {
44        Self { samples: vec![] }
45    }
46
47    fn add(&mut self, ping_duration: &Duration) {
48        self.samples.push(ping_duration.as_nanos());
49    }
50
51    fn count(&self) -> usize {
52        self.samples.len()
53    }
54
55    fn get_min_avg_max(&self) -> (u128, u128, u128) {
56        let (min, max, sum) = self
57            .samples
58            .iter()
59            .fold((u128::MAX, u128::MIN, 0), |(min, max, sum), value| {
60                (min.min(*value), max.max(*value), sum + value)
61            });
62        let avg = sum / self.count() as u128;
63
64        (min, avg, max)
65    }
66
67    fn get_stats(&self) -> (u128, u128, u128, u128) {
68        let (min, avg, max) = self.get_min_avg_max();
69
70        let variance = self
71            .samples
72            .iter()
73            .map(|value| {
74                let diff = avg as f64 - (*value as f64);
75
76                diff * diff
77            })
78            .sum::<f64>()
79            / self.count() as f64;
80        let std_dev = variance.sqrt() as u128;
81
82        (min, avg, max, std_dev)
83    }
84}
85
86fn nano_to_ms(nanoseconds: u128) -> f64 {
87    nanoseconds as f64 / 1_000_000.0
88}
89
90impl Display for PingStats {
91    fn fmt(&self, f: &mut Formatter<'_>) -> Result {
92        let (min, avg, max, std_dev) = self.get_stats();
93        write!(
94            f,
95            "min/avg/max/mdev = {:.3}/{:.3}/{:.3}/{:.3} ms",
96            nano_to_ms(min),
97            nano_to_ms(avg),
98            nano_to_ms(max),
99            nano_to_ms(std_dev)
100        )
101    }
102}
103
104#[async_trait]
105impl CliCommand for PingCmd {
106    fn explain(&self) -> String {
107        "ping command".to_owned()
108    }
109
110    fn login_required(&self) -> bool {
111        false
112    }
113
114    async fn execute_cmd(&mut self, client: &dyn Client) -> anyhow::Result<(), anyhow::Error> {
115        let print_width = (self.count.ilog10() + 1) as usize;
116        let mut ping_stats = PingStats::new();
117
118        for i in 1..=self.count {
119            let time_start = Instant::now();
120            client
121                .ping()
122                .await
123                .with_context(|| "Problem sending ping command".to_owned())?;
124            let ping_duration = time_start.elapsed();
125            ping_stats.add(&ping_duration);
126            event!(target: PRINT_TARGET, Level::INFO, "Ping sequence id: {:width$} time: {:.2} ms", i, nano_to_ms(ping_duration.as_nanos()), width = print_width);
127            sleep(Duration::from_secs(1)).await;
128        }
129
130        event!(target: PRINT_TARGET, Level::INFO, "");
131        event!(target: PRINT_TARGET, Level::INFO, "Ping statistics for {} ping commands", ping_stats.count());
132        event!(target: PRINT_TARGET, Level::INFO, "{ping_stats}");
133
134        Ok(())
135    }
136}
137
138#[cfg(test)]
139mod tests {
140    use super::*;
141
142    #[test]
143    fn should_add_samples() {
144        let mut ping_stats = PingStats::new();
145
146        ping_stats.add(&Duration::from_millis(1));
147        ping_stats.add(&Duration::from_millis(2));
148        ping_stats.add(&Duration::from_millis(3));
149        ping_stats.add(&Duration::from_millis(4));
150        ping_stats.add(&Duration::from_millis(5));
151        ping_stats.add(&Duration::from_millis(6));
152
153        assert_eq!(ping_stats.count(), 6);
154    }
155
156    #[test]
157    fn should_get_min_avg_max() {
158        let mut ping_stats = PingStats::new();
159
160        ping_stats.add(&Duration::from_millis(1));
161        ping_stats.add(&Duration::from_millis(9));
162
163        assert_eq!(ping_stats.count(), 2);
164        assert_eq!(ping_stats.get_min_avg_max(), (1000000, 5000000, 9000000));
165    }
166
167    #[test]
168    fn should_return_stats() {
169        let mut ping_stats = PingStats::new();
170
171        ping_stats.add(&Duration::from_nanos(1));
172        ping_stats.add(&Duration::from_nanos(3));
173        ping_stats.add(&Duration::from_nanos(3));
174        ping_stats.add(&Duration::from_nanos(3));
175        ping_stats.add(&Duration::from_nanos(5));
176
177        assert_eq!(ping_stats.count(), 5);
178        assert_eq!(ping_stats.get_stats(), (1, 3, 5, 1));
179    }
180
181    #[test]
182    fn should_format_stats() {
183        let mut ping_stats = PingStats::new();
184
185        ping_stats.add(&Duration::from_nanos(1322444));
186        ping_stats.add(&Duration::from_nanos(3457432));
187        ping_stats.add(&Duration::from_nanos(5343270));
188        ping_stats.add(&Duration::from_nanos(7837541));
189
190        assert_eq!(
191            format!("{ping_stats}"),
192            "min/avg/max/mdev = 1.322/4.490/7.838/2.400 ms"
193        );
194    }
195}