Skip to main content

p2panda_stream/orderer/
processor.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use std::marker::PhantomData;
4
5use p2panda_core::traits::{Digest, OperationId};
6use p2panda_store::Transaction;
7use p2panda_store::operations::OperationStore;
8use p2panda_store::orderer::OrdererStore;
9use thiserror::Error;
10use tokio::sync::{Mutex, Notify};
11
12use crate::orderer::{CausalOrderer, Ordering};
13use crate::processors::Processor;
14
15pub struct Orderer<T, ID, S> {
16    inner: Mutex<CausalOrderer<ID, S>>,
17    store: S,
18    notify: Notify,
19    _marker: PhantomData<T>,
20}
21
22impl<T, ID, S> Orderer<T, ID, S>
23where
24    ID: OperationId,
25    S: Clone + Transaction + OrdererStore<ID> + OperationStore<T, ID>,
26{
27    pub fn new(store: S) -> Self {
28        let inner = CausalOrderer::new(store.clone());
29
30        Self {
31            inner: Mutex::new(inner),
32            store,
33            notify: Notify::new(),
34            _marker: PhantomData,
35        }
36    }
37}
38
39impl<T, ID, S> Processor<T> for Orderer<T, ID, S>
40where
41    T: Digest<ID> + Ordering<ID>,
42    ID: OperationId,
43    S: Transaction + OrdererStore<ID> + OperationStore<T, ID>,
44{
45    type Output = T;
46
47    type Error = (Option<T>, OrdererError<T, ID, S>);
48
49    async fn process(&self, input: T) -> Result<(), Self::Error> {
50        let permit = match self.store.begin().await {
51            Ok(permit) => permit,
52            Err(err) => return Err((Some(input), OrdererError::Transaction(err))),
53        };
54
55        let inner = self.inner.lock().await;
56        if let Err(err) = inner.process(input.hash(), input.dependencies()).await {
57            return Err((Some(input), OrdererError::OrdererStore(err)));
58        };
59
60        self.store
61            .commit(permit)
62            .await
63            .map_err(|err| (Some(input), OrdererError::Transaction(err)))?;
64
65        self.notify.notify_one(); // Wake up any pending next call
66
67        Ok(())
68    }
69
70    async fn next(&self) -> Result<Self::Output, Self::Error> {
71        loop {
72            let inner = self.inner.lock().await;
73
74            let permit = self
75                .store
76                .begin()
77                .await
78                .map_err(|err| (None, OrdererError::Transaction(err)))?;
79
80            if let Some(id) = inner
81                .next()
82                .await
83                .map_err(|err| (None, OrdererError::OrdererStore(err)))?
84            {
85                self.store
86                    .commit(permit)
87                    .await
88                    .map_err(|err| (None, OrdererError::Transaction(err)))?;
89
90                return match self
91                    .store
92                    .get_operation(&id)
93                    .await
94                    .map_err(OrdererError::OperationStore)
95                {
96                    Ok(Some(operation)) => Ok(operation),
97                    Ok(None) => Err((None, OrdererError::StoreInconsistency(id))),
98                    Err(err) => Err((None, err)),
99                };
100            }
101
102            self.notify.notified().await;
103        }
104    }
105}
106
107#[derive(Debug, Error)]
108pub enum OrdererError<T, ID, S>
109where
110    T: Ordering<ID>,
111    ID: OperationId,
112    S: Transaction + OrdererStore<ID> + OperationStore<T, ID>,
113{
114    #[error("could not find item with id {0} in operation store")]
115    StoreInconsistency(ID),
116
117    #[error("{0}")]
118    OrdererStore(<S as OrdererStore<ID>>::Error),
119
120    #[error("{0}")]
121    OperationStore(<S as OperationStore<T, ID>>::Error),
122
123    #[error("{0}")]
124    Transaction(<S as Transaction>::Error),
125}
126
127#[cfg(test)]
128mod tests {
129    use futures_util::stream;
130    use p2panda_core::{Body, Hash, Header, Operation, SigningKey, Topic};
131    use p2panda_store::operations::OperationStore;
132    use p2panda_store::{SqliteStore, tx_unwrap};
133    use serde::{Deserialize, Serialize};
134    use tokio::task;
135    use tokio_stream::StreamExt;
136
137    use crate::StreamLayerExt;
138
139    use super::{Orderer, Ordering};
140
141    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
142    struct TestExtension {
143        dependencies: Vec<Hash>,
144    }
145
146    impl Ordering<Hash> for Operation<TestExtension> {
147        fn dependencies(&self) -> &[Hash] {
148            &self.header.extensions.dependencies
149        }
150    }
151
152    #[tokio::test]
153    async fn out_of_order() {
154        // Create two operations, one by Panda and one by Icebear. Panda's operation points at
155        // Icebear's.
156        let operation_panda = {
157            let signing_key = SigningKey::generate();
158            let body: Body = b"Hi, Icebear".to_vec().into();
159
160            let header = Header::builder().body(&body).build(
161                &signing_key,
162                TestExtension {
163                    dependencies: vec![],
164                },
165            );
166
167            Operation::from_parts(header, Some(body))
168        };
169
170        let operation_icebear = {
171            let signing_key = SigningKey::generate();
172            let body: Body = b"Hello, Pandasan!".to_vec().into();
173
174            let header = Header::builder().body(&body).build(
175                &signing_key,
176                TestExtension {
177                    dependencies: vec![operation_panda.hash],
178                },
179            );
180
181            Operation::from_parts(header, Some(body))
182        };
183
184        let local = task::LocalSet::new();
185
186        local
187            .run_until(async move {
188                let store = SqliteStore::temporary().await;
189
190                // Insert operations into store.
191                tx_unwrap!(store, {
192                    let log_id = Topic::random();
193
194                    store
195                        .insert_operation(&operation_panda.hash, &operation_panda, &log_id)
196                        .await
197                        .unwrap();
198                    store
199                        .insert_operation(&operation_icebear.hash, &operation_icebear, &log_id)
200                        .await
201                        .unwrap();
202                });
203
204                // Prepare processing pipeline for message ordering.
205                let orderer = Orderer::new(store);
206
207                let mut stream = stream::iter(vec![
208                    // Process Icebear's operation first. It will arrive "out of order".
209                    operation_icebear.clone(),
210                    // Process Pandas's operation next. It will "free" Icebear's operation.
211                    operation_panda.clone(),
212                ])
213                .layer(orderer);
214
215                let operation = stream.next().await.unwrap().unwrap();
216                assert_eq!(operation, operation_panda);
217
218                let operation = stream.next().await.unwrap().unwrap();
219                assert_eq!(operation, operation_icebear);
220            })
221            .await;
222    }
223}