1
//! Channel padding
2
//!
3
//! Tor spec `padding-spec.txt` section 2.
4
//!
5
//! # Overview of channel padding control arrangements
6
//!
7
//!  1. `tor_chanmgr::mgr::map` collates information about dormancy, netdir,
8
//!     and overall client configuration, to maintain a
9
//!     [`ChannelPaddingInstructions`](crate::channel::ChannelPaddingInstructions)
10
//!     which is to be used for all relevant[^relevant] channels.
11
//!     This is distributed to channel frontends (`Channel`s)
12
//!     by calling `Channel::reparameterize`.
13
//!
14
//!  2. Circuit and channel `get_or_launch` methods all take a `ChannelUsage`.
15
//!     This is plumbed through the layers to `AbstractChanMgr::get_or_launch`,
16
//!     which passes it to the channel frontend via `Channel::note_usage`.
17
//!
18
//!  3. The `Channel` collates this information, and maintains an idea
19
//!     of whether padding is relevant for this channel (`PaddingControlState`).
20
//!     For channels where it *is* relevant, it sends `CtrlMsg::ConfigUpdate`
21
//!     to the reactor.
22
//!
23
//!  4. The reactor handles `CtrlMsg::ConfigUpdate` by reconfiguring is padding timer;
24
//!     and by sending PADDING_NEGOTIATE cell(s).
25
//!
26
//! [^relevant]: A "relevant" channel is one which is not excluded by the rules about
27
//! padding in padding-spec 2.2.  Arti does not currently support acting as a relay,
28
//! so all our channels are client-to-guard or client-to-directory.
29

            
30
use std::pin::Pin;
31
// TODO, coarsetime maybe?  But see arti#496 and also we want to use the mockable SleepProvider
32
use web_time_compat::{Duration, Instant};
33

            
34
use derive_builder::Builder;
35
use educe::Educe;
36
use futures::FutureExt;
37
use futures::future::{self, FusedFuture};
38
use pin_project::pin_project;
39
use rand::distr::Distribution;
40
use tracing::error;
41

            
42
use tor_cell::chancell::msg::{Padding, PaddingNegotiate};
43
use tor_config::impl_standard_builder;
44
use tor_error::into_internal;
45
use tor_rtcompat::SleepProvider;
46
use tor_units::IntegerMilliseconds;
47

            
48
/// Timer that organises wakeups when channel padding should be sent
49
///
50
/// Use [`next()`](Timer::next) to find when to send padding, and
51
/// [`note_cell_sent()`](Timer::note_cell_sent) to reset the timeout when data flows.
52
///
53
/// A `Timer` can be in "disabled" state, in which case `next()` never completes.
54
///
55
/// `Timer` must be pinned before use
56
/// (this allows us to avoid involving the allocator when we reschedule).
57
#[pin_project(project = PaddingTimerProj)]
58
pub(crate) struct Timer<R: SleepProvider> {
59
    /// [`SleepProvider`]
60
    sleep_prov: R,
61

            
62
    /// Parameters controlling distribution of padding time intervals
63
    ///
64
    /// Can be `None` to mean the timing parameters are set to infinity.
65
    parameters: Option<PreparedParameters>,
66

            
67
    /// Gap that we intend to leave between last sent cell, and the padding
68
    ///
69
    /// We only resample this (calculating a new random delay) after the previous
70
    /// timeout actually expired.
71
    ///
72
    /// `None` if the timer is disabled.
73
    /// (This can be done explicitly, but also occurs on time calculation overflow.)
74
    ///
75
    /// Invariants: this field may be `Some` or `None` regardless of the values
76
    /// of other fields.  If this field is `None` then the values in `trigger_at`
77
    /// and `waker` are unspecified.
78
    selected_timeout: Option<Duration>,
79

            
80
    /// Absolute time at which we should send padding
81
    ///
82
    /// `None` if cells more recently sent than we were polled.
83
    /// That would mean that we are currently moving data out through this channel.
84
    /// The absolute timeout will need to be recalculated when the data flow pauses.
85
    ///
86
    /// `Some` means our `next` has been demanded recently.
87
    /// Then `trigger_at` records the absolute timeout at which we should send padding,
88
    /// which was calculated the first time we were polled (after data).
89
    ///
90
    /// Invariants: the value in this field is meaningful only if `selected_timeout`
91
    /// is `Some`.
92
    ///
93
    /// If `selected_timeout` is `Some`, and `trigger_at` is therefore valid,
94
    /// it is (obviously) no later than `selected_timeout` from now.
95
    ///
96
    /// See also `waker`.
97
    trigger_at: Option<Instant>,
98

            
99
    /// Actual waker from the `SleepProvider`
100
    ///
101
    /// This is created and updated lazily, because we suspect that with some runtimes
102
    /// setting timeouts may be slow.
103
    /// Lazy updating means that with intermittent data traffic, we do not keep scheduling,
104
    /// descheduling, and adjusting, a wakeup time.
105
    ///
106
    /// Invariants:
107
    ///
108
    /// If `selected_timeout` is `Some`,
109
    /// the time at which this waker will trigger here is never *later* than `trigger_at`,
110
    /// and never *later* than `selected_timeout` from now.
111
    ///
112
    /// The wakeup time here may well be earlier than `trigger_at`,
113
    /// and sooner than `selected_timeout` from now.  It may even be in the past.
114
    /// When we wake up and discover this situation, we reschedule a new waker.
115
    ///
116
    /// If `selected_timeout` is `None`, the value is unspecified.
117
    /// We may retain a `Some` in this case so that if `SleepProvider` is enhanced to
118
    /// support rescheduling, we can do that without making a new `SleepFuture`
119
    /// (and without completely reorganising this the `Timer` state structure.)
120
    #[pin]
121
    waker: Option<R::SleepFuture>,
122
}
123

            
124
/// Timing parameters, as described in `padding-spec.txt`
125
#[derive(Debug, Copy, Clone, Eq, PartialEq, Builder)]
126
#[builder(build_fn(error = "ParametersError", private, name = "build_inner"))]
127
pub struct Parameters {
128
    /// Low end of the distribution of `X`
129
    #[builder(default = "1500.into()")]
130
    pub(crate) low: IntegerMilliseconds<u32>,
131
    /// High end of the distribution of `X` (inclusive)
132
    #[builder(default = "9500.into()")]
133
    pub(crate) high: IntegerMilliseconds<u32>,
134
}
135

            
136
/// An error that occurred whil e constructing padding parameters.
137
#[derive(Clone, Debug, thiserror::Error)]
138
#[non_exhaustive]
139
pub enum ParametersError {
140
    /// Could not construct a range: there were no members between low and high.
141
    #[error("Cannot construct padding parameters: low bound was above the high bound.")]
142
    InvalidRange,
143
}
144

            
145
impl ParametersBuilder {
146
    /// Try to construct a [`Parameters`] from this builder.
147
    ///
148
    /// returns an error if the distribution is ill-defined.
149
10871
    pub fn build(&self) -> Result<Parameters, ParametersError> {
150
10871
        let parameters = self.build_inner()?;
151
10871
        if parameters.low > parameters.high {
152
2
            return Err(ParametersError::InvalidRange);
153
10869
        }
154

            
155
10869
        Ok(parameters)
156
10871
    }
157
}
158

            
159
impl_standard_builder! { Parameters: !Deserialize + !Builder + !Default }
160

            
161
impl Parameters {
162
    /// Return a `PADDING_NEGOTIATE START` cell specifying precisely these parameters
163
    ///
164
    /// This function does not take account of the need to avoid sending particular
165
    /// parameters, and instead sending zeroes, if the requested padding is the consensus
166
    /// default.  The caller must take care of that.
167
    pub fn padding_negotiate_cell(&self) -> Result<PaddingNegotiate, tor_error::Bug> {
168
        let get = |input: IntegerMilliseconds<u32>| {
169
            input
170
                .try_map(TryFrom::try_from)
171
                .map_err(into_internal!("padding negotiate out of range"))
172
        };
173
        Ok(PaddingNegotiate::start(get(self.low)?, get(self.high)?))
174
    }
175

            
176
    /// Make a Parameters containing the specification-defined default parameters
177
    pub fn default_padding() -> Self {
178
        Parameters::builder().build().expect("build succeeded")
179
    }
180

            
181
    /// Make a Parameters sentinel value, with both fields set to zero, which means "no padding"
182
4488
    pub fn disabled() -> Self {
183
4488
        Parameters {
184
4488
            low: 0.into(),
185
4488
            high: 0.into(),
186
4488
        }
187
4488
    }
188
}
189

            
190
/// Timing parameters, "compiled" into a form which can be sampled more efficiently
191
///
192
/// According to the docs for [`rand::RngExt::random_range`],
193
/// it is better to construct a distribution,
194
/// than to call `random_range` repeatedly on the same range.
195
#[derive(Debug, Clone)]
196
struct PreparedParameters {
197
    /// The distribution of `X` (not of the ultimate delay, which is `max(X1,X2)`)
198
    x_distribution_ms: rand::distr::Uniform<u32>,
199
}
200

            
201
/// Return value from `prepare_to_sleep`: instructions for what caller ought to do
202
#[derive(Educe)]
203
#[educe(Debug)]
204
enum SleepInstructions<'f, R: SleepProvider> {
205
    /// Caller should send padding immediately
206
    Immediate {
207
        /// The current `Instant`, returned so that the caller need not call `now` again
208
        now: Instant,
209
    },
210
    /// Caller should wait forever
211
    Forever,
212
    /// Caller should `await` this
213
    Waker(#[educe(Debug(ignore))] Pin<&'f mut R::SleepFuture>),
214
}
215

            
216
impl<R: SleepProvider> Timer<R> {
217
    /// Create a new `Timer`
218
    #[allow(dead_code)]
219
4
    pub(crate) fn new(sleep_prov: R, parameters: Parameters) -> crate::Result<Self> {
220
4
        let parameters = parameters.prepare()?;
221
4
        let selected_timeout = parameters.select_timeout();
222
        // Too different to new_disabled to share its code, sadly.
223
4
        Ok(Timer {
224
4
            sleep_prov,
225
4
            parameters: Some(parameters),
226
4
            selected_timeout: Some(selected_timeout),
227
4
            trigger_at: None,
228
4
            waker: None,
229
4
        })
230
4
    }
231

            
232
    /// Create a new `Timer` which starts out disabled
233
524
    pub(crate) fn new_disabled(
234
524
        sleep_prov: R,
235
524
        parameters: Option<Parameters>,
236
524
    ) -> crate::Result<Self> {
237
        Ok(Timer {
238
524
            sleep_prov,
239
524
            parameters: parameters.map(|p| p.prepare()).transpose()?,
240
524
            selected_timeout: None,
241
524
            trigger_at: None,
242
524
            waker: None,
243
        })
244
524
    }
245

            
246
    /// Disable this `Timer`
247
    ///
248
    /// Idempotent.
249
4
    pub(crate) fn disable(self: &mut Pin<&mut Self>) {
250
4
        *self.as_mut().project().selected_timeout = None;
251
4
    }
252

            
253
    /// Enable this `Timer`
254
    ///
255
    /// (If the timer was disabled, the timeout will only start to run when `next()`
256
    /// is next polled.)
257
    ///
258
    /// Idempotent.
259
4
    pub(crate) fn enable(self: &mut Pin<&mut Self>) {
260
4
        if !self.is_enabled() {
261
4
            self.as_mut().select_fresh_timeout();
262
4
        }
263
4
    }
264

            
265
    /// Set this `Timer`'s parameters
266
    ///
267
    /// Will not enable or disable the timer; that must be done separately if desired.
268
    ///
269
    /// The effect may not be immediate: if we are already in a gap between cells,
270
    /// that existing gap may not be adjusted.
271
    /// (We don't *restart* the timer since that would very likely result in a gap
272
    /// longer than either of the configured values.)
273
    ///
274
    /// Idempotent.
275
    pub(crate) fn reconfigure(
276
        self: &mut Pin<&mut Self>,
277
        parameters: &Parameters,
278
    ) -> crate::Result<()> {
279
        *self.as_mut().project().parameters = Some(parameters.prepare()?);
280
        Ok(())
281
    }
282

            
283
    /// Enquire whether this `Timer` is currently enabled
284
12
    pub(crate) fn is_enabled(&self) -> bool {
285
12
        self.selected_timeout.is_some()
286
12
    }
287

            
288
    /// Select a fresh timeout (and enable, if possible)
289
12
    fn select_fresh_timeout(self: Pin<&mut Self>) {
290
12
        let mut self_ = self.project();
291
12
        let timeout = self_.parameters.as_ref().map(|p| p.select_timeout());
292
12
        *self_.selected_timeout = timeout;
293
        // This is no longer valid; recalculate it on next poll
294
12
        *self_.trigger_at = None;
295
        // Timeout might be earlier, so we will need a new waker too.
296
12
        self_.waker.set(None);
297
12
    }
298

            
299
    /// Note that data has been sent (ie, reset the timeout, delaying the next padding)
300
4428
    pub(crate) fn note_cell_sent(self: &mut Pin<&mut Self>) {
301
        // Fast path, does not need to do anything but clear the absolute expiry time
302
4428
        let self_ = self.as_mut().project();
303
4428
        *self_.trigger_at = None;
304
4428
    }
305

            
306
    /// Calculate when to send padding, and return a suitable waker
307
    ///
308
    /// In the usual case returns [`SleepInstructions::Waker`].
309
1290
    fn prepare_to_sleep(mut self: Pin<&mut Self>, now: Option<Instant>) -> SleepInstructions<R> {
310
1290
        let mut self_ = self.as_mut().project();
311

            
312
1290
        let timeout = match self_.selected_timeout {
313
1254
            None => return SleepInstructions::Forever,
314
36
            Some(t) => *t,
315
        };
316

            
317
36
        if self_.waker.is_some() {
318
            // We need to do this with is_some and expect because we need to consume self
319
            // to get a return value with the right lifetimes.
320
12
            let waker = self
321
12
                .project()
322
12
                .waker
323
12
                .as_pin_mut()
324
12
                .expect("None but we just checked");
325
12
            return SleepInstructions::Waker(waker);
326
24
        }
327

            
328
24
        let now = now.unwrap_or_else(|| self_.sleep_prov.now());
329

            
330
24
        let trigger_at = match self_.trigger_at {
331
8
            Some(t) => t,
332
16
            None => self_.trigger_at.insert(match now.checked_add(timeout) {
333
                None => {
334
2
                    error!("bug: timeout overflowed computing next channel padding. Disabling.");
335
2
                    self.disable();
336
2
                    return SleepInstructions::Forever;
337
                }
338
14
                Some(r) => r,
339
            }),
340
        };
341

            
342
22
        let remaining = trigger_at.checked_duration_since(now).unwrap_or_default();
343
22
        if remaining.is_zero() {
344
8
            return SleepInstructions::Immediate { now };
345
14
        }
346

            
347
        //dbg!(timeout, remaining, now, trigger_at);
348

            
349
        // There is no Option::get_pin_mut_or_set_with
350
14
        if self_.waker.is_none() {
351
14
            self_.waker.set(Some(self_.sleep_prov.sleep(remaining)));
352
14
        }
353
14
        let waker = self
354
14
            .project()
355
14
            .waker
356
14
            .as_pin_mut()
357
14
            .expect("None but we just inserted!");
358
14
        SleepInstructions::Waker(waker)
359
1290
    }
360

            
361
    /// Wait until we should next send padding, and then return the padding message
362
    ///
363
    /// Should be used as a low-priority branch within `select_biased!`.
364
    ///
365
    /// (`next()` has to be selected on, along with other possible events, in the
366
    /// main loop, so that the padding timer runs concurrently with other processing;
367
    /// and it should be in a low-priority branch of `select_biased!` as an optimisation:
368
    /// that avoids calculating timeouts etc. until necessary,
369
    /// i.e. it calculates them only when the main loop would otherwise block.)
370
    ///
371
    /// The returned future is async-cancel-safe,
372
    /// but once it yields, the padding must actually be sent.
373
5124
    pub(crate) fn next(self: Pin<&mut Self>) -> impl FusedFuture<Output = Padding> + '_ {
374
5124
        self.next_inner().fuse()
375
5124
    }
376

            
377
    /// Wait until we should next send padding (not `FusedFuture`)
378
    ///
379
    /// Callers wants a [`FusedFuture`] because `select!` needs one.
380
5124
    async fn next_inner(mut self: Pin<&mut Self>) -> Padding {
381
8
        let now = loop {
382
1282
            match self.as_mut().prepare_to_sleep(None) {
383
1256
                SleepInstructions::Forever => future::pending().await,
384
8
                SleepInstructions::Immediate { now } => break now,
385
18
                SleepInstructions::Waker(waker) => waker.await,
386
            }
387

            
388
            // This timer has fired and has therefore been used up.
389
            // When we go round again we will make a new one.
390
            //
391
            // TODO: have SleepProviders provide a reschedule function, and use it.
392
            // That is likely to be faster where supported.
393
8
            self.as_mut().project().waker.set(None);
394
        };
395

            
396
        // It's time to send padding.
397

            
398
        // Firstly, calculate the new timeout for the *next* padding,
399
        // so that we leave the `Timer` properly programmed.
400
8
        self.as_mut().select_fresh_timeout();
401

            
402
        // Bet that we will be going to sleep again, and set up the new trigger time
403
        // and waker now.  This will save us a future call to Instant::get.
404
8
        self.as_mut().prepare_to_sleep(Some(now));
405

            
406
8
        Padding::new()
407
8
    }
408
}
409

            
410
impl Parameters {
411
    /// "Compile" the parameters into a form which can be quickly sampled
412
14
    fn prepare(self) -> Result<PreparedParameters, tor_error::Bug> {
413
        Ok(PreparedParameters {
414
14
            x_distribution_ms: rand::distr::Uniform::new_inclusive(
415
14
                self.low.as_millis(),
416
14
                self.high.as_millis(),
417
            )
418
14
            .map_err(into_internal!("Parameters were not a valid range."))?,
419
        })
420
14
    }
421
}
422

            
423
impl PreparedParameters {
424
    /// Randomly select a timeout (as per `padding-spec.txt`)
425
2000214
    fn select_timeout(&self) -> Duration {
426
2000214
        let mut rng = rand::rng();
427
2000214
        let ms = std::cmp::max(
428
2000214
            self.x_distribution_ms.sample(&mut rng),
429
2000214
            self.x_distribution_ms.sample(&mut rng),
430
        );
431
2000214
        Duration::from_millis(ms.into())
432
2000214
    }
433
}
434

            
435
#[cfg(test)]
436
mod test {
437
    // @@ begin test lint list maintained by maint/add_warning @@
438
    #![allow(clippy::bool_assert_comparison)]
439
    #![allow(clippy::clone_on_copy)]
440
    #![allow(clippy::dbg_macro)]
441
    #![allow(clippy::mixed_attributes_style)]
442
    #![allow(clippy::print_stderr)]
443
    #![allow(clippy::print_stdout)]
444
    #![allow(clippy::single_char_pattern)]
445
    #![allow(clippy::unwrap_used)]
446
    #![allow(clippy::unchecked_time_subtraction)]
447
    #![allow(clippy::useless_vec)]
448
    #![allow(clippy::needless_pass_by_value)]
449
    #![allow(clippy::string_slice)] // See arti#2571
450
    //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
451

            
452
    use super::*;
453
    use futures::future::ready;
454
    use futures::select_biased;
455
    use itertools::{Itertools, izip};
456
    use statrs::distribution::ContinuousCDF;
457
    use tokio::pin;
458
    use tokio_crate::{self as tokio};
459
    use tor_rtcompat::*;
460

            
461
    async fn assert_not_ready<R: Runtime>(timer: &mut Pin<&mut Timer<R>>) {
462
        select_biased! {
463
            _ = timer.as_mut().next() => panic!("unexpectedly ready"),
464
            _ = ready(()) => { },
465
        };
466
    }
467

            
468
    async fn assert_is_ready<R: Runtime>(timer: &mut Pin<&mut Timer<R>>) {
469
        let _: Padding = select_biased! {
470
            p = timer.as_mut().next() => p,
471
            _ = ready(()) => panic!("pad timer failed to yield"),
472
        };
473
    }
474

            
475
    #[test]
476
    fn timer_impl() {
477
        let runtime = tor_rtcompat::tokio::TokioNativeTlsRuntime::create().unwrap();
478
        #[allow(deprecated)] // TODO #1885
479
        let runtime = tor_rtmock::MockSleepRuntime::new(runtime);
480

            
481
        let parameters = Parameters {
482
            low: 1000.into(),
483
            high: 1000.into(),
484
        };
485

            
486
        let () = runtime.block_on(async {
487
            let timer = Timer::new(runtime.clone(), parameters).unwrap();
488
            pin!(timer);
489
            assert_eq! { true, timer.is_enabled() }
490

            
491
            // expiry time not yet calculated
492
            assert_eq! { timer.as_mut().trigger_at, None };
493

            
494
            // ---------- timeout value ----------
495

            
496
            // Just created, not ready yet
497
            assert_not_ready(&mut timer).await;
498

            
499
            runtime.advance(Duration::from_millis(999)).await;
500
            // Not quite ready
501
            assert_not_ready(&mut timer).await;
502

            
503
            runtime.advance(Duration::from_millis(1)).await;
504
            // Should go off precisely now
505
            assert_is_ready(&mut timer).await;
506

            
507
            assert_not_ready(&mut timer).await;
508
            runtime.advance(Duration::from_millis(1001)).await;
509
            // Should go off 1ms ago, fine
510
            assert_is_ready(&mut timer).await;
511

            
512
            // ---------- various resets ----------
513

            
514
            runtime.advance(Duration::from_millis(500)).await;
515
            timer.note_cell_sent();
516
            assert_eq! { timer.as_mut().trigger_at, None };
517

            
518
            // This ought not to cause us to actually calculate the expiry time
519
            let () = select_biased! {
520
                _ = ready(()) => { },
521
                _ = timer.as_mut().next() => panic!(),
522
            };
523
            assert_eq! { timer.as_mut().trigger_at, None };
524

            
525
            // ---------- disable/enable ----------
526

            
527
            timer.disable();
528
            runtime.advance(Duration::from_millis(2000)).await;
529
            assert_eq! { timer.as_mut().selected_timeout, None };
530
            assert_eq! { false, timer.is_enabled() }
531
            assert_not_ready(&mut timer).await;
532

            
533
            timer.enable();
534
            runtime.advance(Duration::from_millis(3000)).await;
535
            assert_eq! { true, timer.is_enabled() }
536
            // Shouldn't be already ready, since we haven't polled yet
537
            assert_not_ready(&mut timer).await;
538

            
539
            runtime.advance(Duration::from_millis(1000)).await;
540
            // *Now*
541
            assert_is_ready(&mut timer).await;
542
        });
543

            
544
        let () = runtime.block_on(async {
545
            let timer = Timer::new(runtime.clone(), parameters).unwrap();
546
            pin!(timer);
547

            
548
            assert! { timer.as_mut().selected_timeout.is_some() };
549
            assert! { timer.as_mut().trigger_at.is_none() };
550
            // Force an overflow by guddling
551
            *timer.as_mut().project().selected_timeout = Some(Duration::MAX);
552

            
553
            assert_not_ready(&mut timer).await;
554
            dbg!(timer.as_mut().project().trigger_at);
555
            assert_eq! { false, timer.is_enabled() }
556
        });
557

            
558
        let () = runtime.block_on(async {
559
            let timer = Timer::new_disabled(runtime.clone(), None).unwrap();
560
            assert! { timer.parameters.is_none() };
561
            pin!(timer);
562
            assert_not_ready(&mut timer).await;
563
            assert! { timer.as_mut().selected_timeout.is_none() };
564
            assert! { timer.as_mut().trigger_at.is_none() };
565
        });
566

            
567
        let () = runtime.block_on(async {
568
            let timer = Timer::new_disabled(runtime.clone(), Some(parameters)).unwrap();
569
            assert! { timer.parameters.is_some() };
570
            pin!(timer);
571
            assert_not_ready(&mut timer).await;
572
            runtime.advance(Duration::from_millis(3000)).await;
573
            assert_not_ready(&mut timer).await;
574
            timer.as_mut().enable();
575
            assert_not_ready(&mut timer).await;
576
            runtime.advance(Duration::from_millis(3000)).await;
577
            assert_is_ready(&mut timer).await;
578
        });
579
    }
580

            
581
    #[test]
582
    #[allow(clippy::print_stderr)]
583
    fn timeout_distribution() {
584
        // Test that the distribution of padding intervals is as we expect.  This is not so
585
        // straightforward.  We need to deal with true randomness (since we can't plumb a
586
        // testing RNG into the padding timer, and perhaps don't even *want* to make that a
587
        // mockable interface).  Measuring a distribution of random variables involves some
588
        // statistics.
589

            
590
        // The overall approach is:
591
        //    Use a fixed (but nontrivial) low to high range
592
        //    Sample N times into n equal sized buckets
593
        //    Calculate the expected number of samples in each bucket
594
        //    Do a chi^2 test.  If it doesn't spot a potential difference, declare OK.
595
        //    If the chi^2 test does definitely declare a difference, declare failure.
596
        //    Otherwise increase N and go round again.
597
        //
598
        // This allows most runs to be fast without having an appreciable possibility of a
599
        // false test failure and while being able to detect even quite small deviations.
600

            
601
        // Notation from
602
        // https://en.wikipedia.org/wiki/Pearson%27s_chi-squared_test#Calculating_the_test-statistic
603
        // I haven't done a formal power calculation but empirically
604
        // this detects the following most of the time:
605
        //  deviation of the CDF power from B^2 to B^1.98
606
        //  wrong minimum value by 25ms out of 12s, low_ms = min + 25
607
        //  wrong maximum value by 10ms out of 12s, high_ms = max -1 - 10
608

            
609
        #[allow(non_snake_case)]
610
        let mut N = 100_0000;
611

            
612
        #[allow(non_upper_case_globals)]
613
        const n: usize = 100;
614

            
615
        const P_GOOD: f64 = 0.05; // Investigate further 5% of times (if all is actually well)
616
        const P_BAD: f64 = 1e-12;
617

            
618
        loop {
619
            eprintln!("padding distribution test, n={} N={}", n, N);
620

            
621
            let min = 5000;
622
            let max = 17000; // Exclusive
623
            assert_eq!(0, (max - min) % (n as u32)); // buckets must match up to integer boundaries
624

            
625
            let cdf = (0..=n)
626
                .map(|bi| {
627
                    let b = (bi as f64) / (n as f64);
628
                    // expected distribution:
629
                    // with B = bi / n
630
                    //   P(X) < B == B
631
                    //   P(max(X1,X1)) < B = B^2
632
                    b.powi(2)
633
                })
634
                .collect_vec();
635

            
636
            let pdf = cdf
637
                .iter()
638
                .cloned()
639
                .tuple_windows()
640
                .map(|(p, q)| q - p)
641
                .collect_vec();
642
            let exp = pdf.iter().cloned().map(|p| p * f64::from(N)).collect_vec();
643

            
644
            // chi-squared test only valid if every cell expects at least 5
645
            assert!(exp.iter().cloned().all(|ei| ei >= 5.));
646

            
647
            let mut obs = [0_u32; n];
648

            
649
            let params = Parameters {
650
                low: min.into(),
651
                high: (max - 1).into(), // convert exclusive to inclusive
652
            }
653
            .prepare()
654
            .unwrap();
655

            
656
            for _ in 0..N {
657
                let xx = params.select_timeout();
658
                let ms = xx.as_millis();
659
                let ms = u32::try_from(ms).unwrap();
660
                assert!(ms >= min);
661
                assert!(ms < max);
662
                // Integer arithmetic ensures that we classify exactly
663
                let bi = ((ms - min) * (n as u32)) / (max - min);
664
                obs[bi as usize] += 1;
665
            }
666

            
667
            let chi2 = izip!(&obs, &exp)
668
                .map(|(&oi, &ei)| (f64::from(oi) - ei).powi(2) / ei)
669
                .sum::<f64>();
670

            
671
            // n degrees of freedom, one-tailed test
672
            // (since distro parameters are all fixed, not estimated from the sample)
673
            let chi2_distr = statrs::distribution::ChiSquared::new(n as _).unwrap();
674

            
675
            // probability of good code generating a result at least this bad
676
            let p = 1. - chi2_distr.cdf(chi2);
677

            
678
            eprintln!(
679
                "padding distribution test, n={} N={} chi2={} p={}",
680
                n, N, chi2, p
681
            );
682

            
683
            if p >= P_GOOD {
684
                break;
685
            }
686

            
687
            for (i, (&oi, &ei)) in izip!(&obs, &exp).enumerate() {
688
                eprintln!("bi={:4} OI={:4} EI={}", i, oi, ei);
689
            }
690

            
691
            if p < P_BAD {
692
                panic!("distribution is wrong (p < {:e})", P_BAD);
693
            }
694

            
695
            // This is statistically rather cheaty: we keep trying until we get a definite
696
            // answer!  But we radically increase the power of the test each time.
697
            // If the distribution is really wrong, this test ought to find it soon enough,
698
            // especially since we run this repeatedly in CI.
699
            N *= 10;
700
        }
701
    }
702

            
703
    #[test]
704
    fn parameters_range() {
705
        let ms100 = IntegerMilliseconds::new(100);
706
        let ms1000 = IntegerMilliseconds::new(1000);
707
        let ms1500 = IntegerMilliseconds::new(1500);
708
        let ms9500 = IntegerMilliseconds::new(9500);
709

            
710
        // default
711
        let p = Parameters::builder().build().unwrap();
712
        assert_eq!(
713
            p,
714
            Parameters {
715
                low: ms1500,
716
                high: ms9500
717
            }
718
        );
719
        assert!(p.prepare().is_ok());
720

            
721
        // low < high
722
        let mut pb = Parameters::builder();
723
        pb.low(ms100);
724
        pb.high(ms1000);
725
        let p = pb.build().unwrap();
726
        assert_eq!(
727
            p,
728
            Parameters {
729
                low: ms100,
730
                high: ms1000
731
            }
732
        );
733
        let p = p.prepare().unwrap();
734
        let range = Duration::try_from(ms100).unwrap()..=Duration::try_from(ms1000).unwrap();
735
        for _ in 1..100 {
736
            assert!(range.contains(&p.select_timeout()));
737
        }
738

            
739
        // low == high
740
        let mut pb = Parameters::builder();
741
        pb.low(ms1000);
742
        pb.high(ms1000);
743
        let p = pb.build().unwrap();
744
        assert!(p.prepare().is_ok());
745

            
746
        // low > high (error case)
747
        let mut pb = Parameters::builder();
748
        pb.low(ms1000);
749
        pb.high(ms100);
750
        let e = pb.build().unwrap_err();
751
        assert!(matches!(e, ParametersError::InvalidRange));
752
    }
753
}