summaryrefslogtreecommitdiff
path: root/crates/tor-dirmgr/src/event.rs
blob: 4a40ddd9c5b66688d6d9b57133a7a5e0d6062f57 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
//! Code for notifying other modules about changes in the directory.

use futures::stream::Stream;
use postage::{broadcast, sink::Sink as _};

/// An event that a DirMgr can broadcast to indicate that a change in
/// the status of its directory.
#[derive(Debug, Clone)]
#[non_exhaustive]
pub enum DirEvent {
    /// A new consensus has been received, and has enough information
    /// to be used.
    NewConsensus,

    /// New descriptors have been received for the latest consensus.
    NewDescriptors,

    /// A dummy event that's only used when we're testing.
    #[cfg(test)]
    Dummy,
}

/// Length of the event queue to use in publishers.
///
/// Chosen arbitrarily.
const QUEUE_LEN: usize = 64;

/// A handle to use in publishing [`DirEvent`]s.
///
/// Cloning a Publisher gives a new handle to the same queue; any event sent
/// with the clone of a Publisher behaves as if it were sent with the original
/// Publisher.
///
/// This handle is implemented as a light facade around
/// [`postage::broadcast`].
///
/// TODO: Eventually we should probably move this into a more generic
/// crate, once we have a few crates that want this kind of thing.
#[derive(Clone)]
pub(crate) struct Publisher {
    /// Sender to use in publishing events.
    send: broadcast::Sender<DirEvent>,
}

impl Publisher {
    /// Create a new Publisher.
    #[allow(clippy::new_without_default)]
    pub(crate) fn new() -> Self {
        let (send, _recv) = broadcast::channel(QUEUE_LEN);
        Publisher { send }
    }

    /// Broadcast the provided [`DirEvent`] to every subscribed listener.
    ///
    /// If there are no subscribed listeners, just drop the event.
    ///
    /// This future can block if some subscriber isn't consuming its events
    /// quickly enough.
    pub(crate) async fn send(&self, ev: DirEvent) {
        // Clone the sender to incref it and get a mutable copy.
        // (It is an Arc internally.)
        let mut sender = self.send.clone();
        // Ignore the results of the send: it will be an error if there are
        // no subscribers, but we don't care.
        let _ignore = sender.send(ev).await;
    }

    /// Return a new [`Stream`] of events.
    ///
    /// This stream will receive every event that is sent on this publisher
    /// _after_ it was created.
    ///
    /// When the last handle for a Publisher is dropped, all Streams
    /// subscribed to that publisher will receive a close.
    ///
    pub(crate) fn subscribe(&self) -> impl Stream<Item = DirEvent> {
        self.send.subscribe()
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use futures_await_test::async_test;
    //use tor_rtcompat::test_with_all_runtimes;
    use futures::stream::StreamExt;

    // Check publishing with no subscribers: events should just
    // get dropped.
    #[async_test]
    async fn drop_test() {
        let publ = Publisher::new();
        for _ in 0..100000 {
            publ.send(DirEvent::NewConsensus).await;
        }
    }

    #[async_test]
    async fn publish_test() {
        let publ = Publisher::new();
        for _ in 0..100_usize {
            // no subscribers, so these should be dropped.
            publ.send(DirEvent::Dummy).await;
        }
        // This subscribes early and sees two events.
        let sub1 = publ.subscribe();
        publ.send(DirEvent::NewConsensus).await;
        // This subscribes late and only sees one event.
        let sub2 = publ.subscribe();
        publ.send(DirEvent::NewConsensus).await;
        drop(publ);
        let lst1: Vec<_> = sub1.collect().await;
        let lst2: Vec<_> = sub2.collect().await;

        assert_eq!(lst1.len(), 2);
        assert_eq!(lst2.len(), 1);
        assert!(matches!(lst1[0], DirEvent::NewConsensus));
        assert!(matches!(lst1[1], DirEvent::NewConsensus));
        assert!(matches!(lst2[0], DirEvent::NewConsensus));
    }
}