Skip to main content

p2panda_stream/orderer/
processor.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3use std::borrow::Borrow;
4use std::cell::RefCell;
5use std::collections::VecDeque;
6use std::marker::PhantomData;
7
8use p2panda_core::traits::Digest;
9use p2panda_core::{Extensions, Hash, Operation};
10use p2panda_store::Transaction;
11use p2panda_store::operations::OperationStore;
12use p2panda_store::orderer::OrdererStore;
13use p2panda_store::processor::ProcessorStore;
14use serde::{Deserialize, Serialize};
15use thiserror::Error;
16use tokio::sync::{Mutex, Notify};
17
18use crate::Processor;
19use crate::orderer::CausalOrderer;
20
21pub trait OrdererMetadata<E> {
22    /// Metadata attached to an input event we want to persist in database next to ordering info.
23    type Metadata: Serialize + for<'de> Deserialize<'de>;
24
25    /// Extract metadata from input.
26    fn metadata(&self) -> Self::Metadata;
27
28    /// Re-construct input from operation and persisted metadata.
29    fn from_operation(operation: Operation<E>, meta: Self::Metadata) -> Self;
30}
31
32#[derive(Clone, Default, Debug, PartialEq, Eq)]
33pub enum OrdererArgs {
34    Process {
35        dependencies: Vec<Hash>,
36    },
37    #[default]
38    Ignore,
39}
40
41#[derive(Clone, Debug)]
42pub enum OrdererResult {
43    /// Item has unmet dependencies, is buffered and now in "pending" state.
44    Pending,
45
46    /// Item is "ready" and was a dependency for one or more operations which are "freed" now.
47    ///
48    /// This input may trigger zero to many ReadyOutput items to follow.
49    ReadyInput { dependent_operations: Vec<Hash> },
50
51    /// Item was buffered by orderer and is now marked as "ready" by another "input" item, to be
52    /// finally forwarded in correct order.
53    ReadyOutput,
54
55    /// Item was ignored as specified in input arguments.
56    Ignored,
57}
58
59impl OrdererResult {
60    pub fn is_pending(&self) -> bool {
61        matches!(self, OrdererResult::Pending)
62    }
63}
64
65pub struct Orderer<S, T, E> {
66    inner: Mutex<CausalOrderer<Hash, S>>,
67    store: S,
68    notify: Notify,
69    // We're keeping an unbound, in-memory buffer of all "freed" events here. This means that if an
70    // event frees n events, we will need to keep all n of them here. This shouldn't be a problem as
71    // long as n stays low (<100 items).
72    //
73    // The alternative is to move the logic polling from the inner orderer into "next" which allows
74    // a more atomic and memory-efficient streaming design where we only look at one item at a time.
75    // We still need a way to tell the pipeline how many items got "freed" by it to allow the
76    // correct input stream ordering to take place (see docs in pipeline.rs of p2panda crate), this
77    // functionality should be possible to add to the inner orderer.
78    queue: RefCell<VecDeque<(T, OrdererResult)>>,
79    _marker: PhantomData<E>,
80}
81
82impl<S, T, E> Orderer<S, T, E>
83where
84    S: Clone + Transaction + OrdererStore<Hash> + OperationStore<Operation<E>, Hash>,
85{
86    pub fn new(store: S) -> Self {
87        Self::from_namespace(store, "default")
88    }
89
90    pub fn from_namespace(store: S, namespace: impl AsRef<str>) -> Self {
91        let inner = CausalOrderer::new(store.clone()).with_namespace(namespace);
92
93        Self {
94            inner: Mutex::new(inner),
95            store,
96            notify: Notify::new(),
97            queue: RefCell::new(VecDeque::new()),
98            _marker: PhantomData,
99        }
100    }
101}
102
103impl<S, T, E> Processor<T> for Orderer<S, T, E>
104where
105    S: Transaction
106        + OrdererStore<Hash>
107        + OperationStore<Operation<E>, Hash>
108        + ProcessorStore<T::Metadata>,
109    T: OrdererMetadata<E> + Borrow<OrdererArgs> + Digest<Hash>,
110    E: Extensions,
111{
112    type Output = (T, OrdererResult);
113
114    type Error = (T, OrdererError);
115
116    async fn process(&self, input: T) -> Result<(), Self::Error> {
117        let args = input.borrow();
118
119        if let OrdererArgs::Process { dependencies } = args {
120            let inner = self.inner.lock().await;
121
122            let permit = match self.store.begin().await {
123                Ok(permit) => permit,
124                Err(err) => return Err((input, OrdererError::Transaction(err.to_string()))),
125            };
126
127            match inner.process(input.hash(), dependencies).await {
128                // a) Item has all dependencies met, we can directly mark it as "ready".
129                Ok(true) => {
130                    let mut dependent_operations = Vec::new();
131
132                    loop {
133                        match inner.next().await {
134                            Ok(Some(id)) => {
135                                // Ignore our own input.
136                                if id != input.hash() {
137                                    dependent_operations.push(id);
138                                }
139
140                                continue;
141                            }
142                            Ok(None) => {
143                                break;
144                            }
145                            Err(err) => {
146                                return Err((input, OrdererError::OrdererStore(err.to_string())));
147                            }
148                        }
149                    }
150
151                    let mut to_queue = Vec::new();
152
153                    for id in &dependent_operations {
154                        let operation = match self
155                            .store
156                            .get_operation_tx(id)
157                            .await
158                            .map_err(|err| OrdererError::OperationStore(err.to_string()))
159                        {
160                            Ok(Some(operation)) => operation,
161                            Ok(None) => {
162                                return Err((input, OrdererError::StoreInconsistency(*id)));
163                            }
164                            Err(err) => return Err((input, err)),
165                        };
166
167                        let metadata = match self.store.get_event_tx(&inner.namespace(), id).await {
168                            Ok(Some(metadata)) => metadata,
169                            Ok(None) => {
170                                return Err((input, OrdererError::StoreInconsistency(*id)));
171                            }
172                            Err(err) => {
173                                return Err((input, OrdererError::ProcessorStore(err.to_string())));
174                            }
175                        };
176
177                        to_queue.push((
178                            T::from_operation(operation, metadata),
179                            OrdererResult::ReadyOutput,
180                        ));
181                    }
182
183                    if let Err(err) = self.store.commit(permit).await {
184                        return Err((input, OrdererError::Transaction(err.to_string())));
185                    }
186
187                    // Always forward the current input first.
188                    self.queue.borrow_mut().push_back((
189                        input,
190                        OrdererResult::ReadyInput {
191                            dependent_operations,
192                        },
193                    ));
194
195                    // .. followed by all items which have been marked as "ready" by input.
196                    for item in to_queue {
197                        self.queue.borrow_mut().push_back(item);
198                    }
199                }
200
201                // b) Item doesn't have dependencies met yet, mark it as "pending", it is buffered now.
202                Ok(false) => {
203                    if let Err(err) = self
204                        .store
205                        .set_event_tx(&inner.namespace(), &input.hash(), &input.metadata())
206                        .await
207                    {
208                        return Err((input, OrdererError::ProcessorStore(err.to_string())));
209                    };
210
211                    if let Err(err) = self.store.commit(permit).await {
212                        return Err((input, OrdererError::Transaction(err.to_string())));
213                    }
214
215                    self.queue
216                        .borrow_mut()
217                        .push_back((input, OrdererResult::Pending));
218                }
219                Err(err) => return Err((input, OrdererError::OrdererStore(err.to_string()))),
220            }
221        } else {
222            self.queue
223                .borrow_mut()
224                .push_back((input, OrdererResult::Ignored));
225        }
226
227        self.notify.notify_one(); // Wake up any pending next call
228
229        Ok(())
230    }
231
232    async fn next(&self) -> Result<Self::Output, Self::Error> {
233        // TODO: If we decide to handle all logic in the "process" part (less memory efficient
234        // approach, see comment above) we should consider replacing the Processor trait with a
235        // Stream as "next" is not required anymore.
236        loop {
237            if let Some(output) = self.queue.borrow_mut().pop_front() {
238                return Ok(output);
239            }
240
241            self.notify.notified().await;
242        }
243    }
244}
245
246#[derive(Clone, Debug, Error)]
247pub enum OrdererError {
248    #[error("could not find item with id {0} in operation store")]
249    StoreInconsistency(Hash),
250
251    #[error("{0}")]
252    OrdererStore(String),
253
254    #[error("{0}")]
255    OperationStore(String),
256
257    #[error("{0}")]
258    ProcessorStore(String),
259
260    #[error("{0}")]
261    Transaction(String),
262}
263
264#[cfg(test)]
265mod tests {
266    use std::assert_matches;
267    use std::borrow::Borrow;
268
269    use futures_util::stream;
270    use p2panda_core::traits::Digest;
271    use p2panda_core::{Body, Hash, Header, Operation, SigningKey, Topic};
272    use p2panda_store::operations::OperationStore;
273    use p2panda_store::{SqliteStore, tx_unwrap};
274    use serde::{Deserialize, Serialize};
275    use tokio::task;
276    use tokio_stream::StreamExt;
277
278    use crate::{Processor, StreamLayerExt};
279
280    use super::{Orderer, OrdererArgs, OrdererMetadata, OrdererResult};
281
282    #[derive(Clone, Debug, Default, Serialize, Deserialize)]
283    struct TestExtension {
284        dependencies: Vec<Hash>,
285    }
286
287    #[derive(Clone, Debug, PartialEq, Eq)]
288    struct Event {
289        orderer_args: OrdererArgs,
290        operation: Operation<TestExtension>,
291    }
292
293    impl Borrow<OrdererArgs> for Event {
294        fn borrow(&self) -> &OrdererArgs {
295            &self.orderer_args
296        }
297    }
298
299    impl Digest<Hash> for Event {
300        fn hash(&self) -> Hash {
301            self.operation.hash
302        }
303    }
304
305    #[derive(Clone, Debug, Serialize, Deserialize)]
306    struct EventMetadata;
307
308    impl OrdererMetadata<TestExtension> for Event {
309        type Metadata = EventMetadata;
310
311        fn metadata(&self) -> Self::Metadata {
312            EventMetadata
313        }
314
315        // The `meta` parameter is a requirement of the trait function signature but we do not
316        // require it for this test implementation, hence the lint bypass.
317        #[allow(unused_variables)]
318        fn from_operation(operation: Operation<TestExtension>, meta: Self::Metadata) -> Self {
319            Self {
320                orderer_args: OrdererArgs::Process {
321                    dependencies: operation.header.extensions.dependencies.clone(),
322                },
323                operation,
324            }
325        }
326    }
327
328    #[tokio::test]
329    async fn out_of_order() {
330        // Create two operations, one by Panda and one by Icebear. Panda's operation points at
331        // Icebear's.
332        let operation_panda = {
333            let signing_key = SigningKey::generate();
334            let body: Body = b"Hi, Icebear".to_vec().into();
335
336            let header = Header::builder().body(&body).build(
337                &signing_key,
338                TestExtension {
339                    dependencies: vec![],
340                },
341            );
342
343            Operation::from_parts(header, Some(body))
344        };
345
346        let event_panda = Event::from_operation(operation_panda.clone(), EventMetadata);
347
348        let operation_icebear = {
349            let signing_key = SigningKey::generate();
350            let body: Body = b"Hello, Pandasan!".to_vec().into();
351
352            let header = Header::builder().body(&body).build(
353                &signing_key,
354                TestExtension {
355                    dependencies: vec![operation_panda.hash],
356                },
357            );
358
359            Operation::from_parts(header, Some(body))
360        };
361
362        let event_icebear = Event::from_operation(operation_icebear.clone(), EventMetadata);
363
364        let local = task::LocalSet::new();
365
366        local
367            .run_until(async move {
368                let store = SqliteStore::temporary().await;
369
370                // Insert operations into store.
371                tx_unwrap!(store, {
372                    let log_id = Topic::random();
373
374                    store
375                        .insert_operation_tx(&operation_panda.hash, &operation_panda, &log_id)
376                        .await
377                        .unwrap();
378                    store
379                        .insert_operation_tx(&operation_icebear.hash, &operation_icebear, &log_id)
380                        .await
381                        .unwrap();
382                });
383
384                // Prepare processing pipeline for message ordering.
385                let orderer_1 = Orderer::from_namespace(store.clone(), "first");
386
387                let mut stream = stream::iter(vec![
388                    // Process Icebear's operation first. It will arrive "out of order".
389                    event_icebear.clone(),
390                    // Process Pandas's operation next. It will "free" Icebear's operation.
391                    event_panda.clone(),
392                ])
393                .layer(orderer_1);
394
395                // Icebear's event has a dependency on Panda's event so it remains in a pending
396                // state for now.
397                let (event, result) = stream.next().await.unwrap().unwrap();
398                assert!(result.is_pending());
399                assert_eq!(event, event_icebear);
400
401                // Panda's event is released. It frees up Icebear's event which is now ready.
402                let (event, result) = stream.next().await.unwrap().unwrap();
403                assert_matches!(
404                    result,
405                    OrdererResult::ReadyInput {
406                        dependent_operations: _
407                    }
408                );
409                assert_eq!(event, event_panda);
410
411                // Icebear's event has it's dependencies met and is released.
412                let (event, result) = stream.next().await.unwrap().unwrap();
413                assert_matches!(result, OrdererResult::ReadyOutput);
414                assert_eq!(event, event_icebear);
415
416                // Make sure another, namespaced orderer is not affected:
417                let orderer_2 = Orderer::from_namespace(store, "second");
418                orderer_2.process(event_icebear.clone()).await.unwrap();
419                let (_event, result) = orderer_2.next().await.unwrap();
420                assert_matches!(
421                    result,
422                    OrdererResult::Pending,
423                    "panda's event was not processed yet for second orderer"
424                );
425            })
426            .await;
427    }
428}