Skip to main content

objectstore_inventory_tracker/
producer.rs

1//! The transport abstraction records are handed to.
2//!
3//! The abstraction is very thin and narrow, but it allows us to avoid needing to add a
4//! "build `librdkafka` with `cmake`" step to tests or local development builds.
5//!
6//! [`Producer`] carries an associated error type, so it is not usable as `dyn Producer`.
7//! See [`SharedProducer`] for handing one producer to several trackers.
8
9use std::future::Future;
10use std::sync::Arc;
11use std::time::Duration;
12
13/// Sends serialized inventory records somewhere durable.
14pub trait Producer {
15    /// What can go wrong when sending.
16    type Error;
17
18    /// Enqueues one record.
19    ///
20    /// `key` controls which partition receives the message.
21    ///
22    /// `Ok()` does not necessarily mean the message will be sent successfully. It just
23    /// means the message has been enqueued.
24    fn send(&self, key: &[u8], payload: Vec<u8>) -> Result<(), Self::Error>;
25
26    /// Blocks until enqueued records have been delivered, or `timeout` elapses.
27    ///
28    /// Prefer [`join`](Self::join) from async code.
29    fn join_blocking(&self, timeout: Duration) -> Result<(), Self::Error>;
30
31    /// Waits for enqueued records to be delivered, or until `timeout` elapses.
32    ///
33    /// Runs [`join_blocking`](Self::join_blocking) on a blocking thread. Returns a future
34    /// rather than being `async fn` so it does not borrow `self`, which a `dyn` caller's
35    /// `async_trait` boxing requires.
36    fn join(
37        &self,
38        timeout: Duration,
39    ) -> impl Future<Output = Result<(), Self::Error>> + Send + use<Self>
40    where
41        Self: Sized + Clone + Send + Sync + 'static,
42        Self::Error: Send + 'static,
43    {
44        let producer = self.clone();
45        async move {
46            // spawn_blocking panics outside a runtime, which would lose the records.
47            if tokio::runtime::Handle::try_current().is_err() {
48                return producer.join_blocking(timeout);
49            }
50
51            match tokio::task::spawn_blocking(move || producer.join_blocking(timeout)).await {
52                Ok(result) => result,
53                // Cancelled or panicked; nothing left to report to during shutdown.
54                Err(_) => Ok(()),
55            }
56        }
57    }
58
59    /// Erases this producer's transport and error type, so one producer can serve
60    /// several trackers.
61    ///
62    /// ```
63    /// use objectstore_inventory_tracker::{InventoryTracker, NoopProducer, Producer};
64    ///
65    /// let producer = NoopProducer.shared();
66    /// let tracker = InventoryTracker::new(producer.clone(), "my_gcs_bucket", 1.0);
67    /// ```
68    fn shared(self) -> SharedProducer
69    where
70        Self: Sized + Send + Sync + 'static,
71        Self::Error: std::error::Error + Send + Sync + 'static,
72    {
73        Arc::new(BoxErrors(self))
74    }
75}
76
77impl<P: Producer + ?Sized> Producer for Box<P> {
78    type Error = P::Error;
79
80    fn send(&self, key: &[u8], payload: Vec<u8>) -> Result<(), Self::Error> {
81        (**self).send(key, payload)
82    }
83
84    fn join_blocking(&self, timeout: Duration) -> Result<(), Self::Error> {
85        (**self).join_blocking(timeout)
86    }
87}
88
89impl<P: Producer + ?Sized> Producer for Arc<P> {
90    type Error = P::Error;
91
92    fn send(&self, key: &[u8], payload: Vec<u8>) -> Result<(), Self::Error> {
93        (**self).send(key, payload)
94    }
95
96    fn join_blocking(&self, timeout: Duration) -> Result<(), Self::Error> {
97        (**self).join_blocking(timeout)
98    }
99}
100
101/// The error a [`SharedProducer`] reports.
102pub type BoxError = Box<dyn std::error::Error + Send + Sync>;
103
104/// A [`Producer`] whose transport and error type have both been erased.
105///
106/// Built with [`Producer::shared`]. The `Arc` is what keeps [`Producer::join`] usable:
107/// it needs a `Clone + 'static` value for the blocking thread.
108pub type SharedProducer = Arc<dyn Producer<Error = BoxError> + Send + Sync>;
109
110/// Adapts a [`Producer`] to report [`BoxError`], so it can become a [`SharedProducer`].
111struct BoxErrors<P>(P);
112
113impl<P: Producer> Producer for BoxErrors<P>
114where
115    P::Error: std::error::Error + Send + Sync + 'static,
116{
117    type Error = BoxError;
118
119    fn send(&self, key: &[u8], payload: Vec<u8>) -> Result<(), Self::Error> {
120        self.0.send(key, payload).map_err(Into::into)
121    }
122
123    fn join_blocking(&self, timeout: Duration) -> Result<(), Self::Error> {
124        self.0.join_blocking(timeout).map_err(Into::into)
125    }
126}
127
128/// A [`Producer`] that discards everything.
129#[derive(Clone, Copy, Debug, Default)]
130pub struct NoopProducer;
131
132impl Producer for NoopProducer {
133    type Error = std::convert::Infallible;
134
135    fn send(&self, _key: &[u8], _payload: Vec<u8>) -> Result<(), Self::Error> {
136        Ok(())
137    }
138
139    fn join_blocking(&self, _timeout: Duration) -> Result<(), Self::Error> {
140        Ok(())
141    }
142}
143
144#[cfg(test)]
145mod tests {
146    use super::*;
147    use crate::test_utils::DummyProducer;
148
149    #[tokio::test]
150    async fn an_erased_producer_still_reaches_its_transport() {
151        let dummy = DummyProducer::default();
152        let producer = dummy.clone().shared();
153
154        producer.send(b"key", b"payload".to_vec()).unwrap();
155        producer.join(Duration::from_secs(1)).await.unwrap();
156
157        assert_eq!(dummy.raw(), [(b"key".to_vec(), b"payload".to_vec())]);
158    }
159
160    #[test]
161    fn joining_outside_a_runtime_still_drains() {
162        let dummy = DummyProducer::default();
163        let producer = dummy.clone().shared();
164
165        producer.send(b"key", b"payload".to_vec()).unwrap();
166        futures::executor::block_on(producer.join(Duration::from_secs(1))).unwrap();
167
168        assert_eq!(dummy.raw(), [(b"key".to_vec(), b"payload".to_vec())]);
169    }
170
171    #[test]
172    fn one_erased_producer_serves_many_trackers() {
173        let dummy = DummyProducer::default();
174        let producer = dummy.clone().shared();
175
176        for resource in ["bigtable_objectstore", "gcs_objectstore"] {
177            let tracker = crate::InventoryTracker::new(producer.clone(), resource, 1.0);
178            tracker
179                .delete(
180                    "attachments/objects/abc",
181                    "attachments",
182                    std::time::SystemTime::now(),
183                )
184                .unwrap();
185        }
186
187        let resources: Vec<_> = dummy
188            .records()
189            .into_iter()
190            .map(|record| record.shared_resource_id)
191            .collect();
192        assert_eq!(resources, ["bigtable_objectstore", "gcs_objectstore"]);
193    }
194}