willow_data_model/store.rs
1use std::{
2 cell::RefCell,
3 collections::VecDeque,
4 error::Error,
5 fmt::{Debug, Display},
6 future::Future,
7 rc::Rc,
8};
9
10use either::Either::{self, Left, Right};
11use slab::Slab;
12use ufotofu::{BulkProducer, Producer};
13use wb_async_utils::TakeCell;
14
15use crate::{entry::AuthorisedEntry, grouping::Area, Entry, LengthyAuthorisedEntry, Path};
16
17#[cfg(feature = "dev")]
18use arbitrary::Arbitrary;
19
20/// Returned when an entry could be ingested into a [`Store`].
21#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
22pub enum EntryIngestionSuccess<const MCL: usize, const MCC: usize, const MPL: usize, N, S, PD, AT> {
23 /// The entry was successfully ingested.
24 Success,
25 /// The entry was not ingested because a newer entry with same
26 Obsolete {
27 /// The obsolete entry which was not ingested.
28 obsolete: AuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
29 /// The newer entry which was not overwritten.
30 newer: AuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
31 },
32}
33
34/// Returned when an entry cannot be ingested into a [`Store`].
35#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
36pub enum EntryIngestionError<OE> {
37 /// The ingestion would have triggered prefix pruning when that was not desired.
38 PruningPrevented,
39 /// Something specific to this store implementation went wrong.
40 OperationsError(OE),
41}
42
43impl<OE: Display + Error> Display for EntryIngestionError<OE> {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 EntryIngestionError::PruningPrevented => {
47 write!(f, "Entry ingestion would have triggered undesired pruning.")
48 }
49 EntryIngestionError::OperationsError(err) => Display::fmt(err, f),
50 }
51 }
52}
53
54impl<OE: Display + Error> Error for EntryIngestionError<OE> {}
55
56/// Returned when a payload is successfully appended to the [`Store`].
57#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Copy)]
58pub enum PayloadAppendSuccess {
59 /// The payload was appended to but not completed.
60 Appended,
61 /// The payload was completed by the appendment.
62 Completed,
63}
64
65/// Returned when a payload fails to be appended into the [`Store`].
66#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Copy)]
67pub enum PayloadAppendError<PayloadSourceError, OE> {
68 /// No entry for the given subspace and path exists in this store.
69 NoSuchEntry,
70 /// The operation supplied an expected payload_digest, but it did not match the digest of the entry.
71 WrongEntry,
72 /// The payload source produced more bytes than were expected for this payload.
73 TooManyBytes,
74 /// The completed payload's digest is not what was expected.
75 DigestMismatch,
76 /// The source that provided the payload bytes emitted an error.
77 SourceError {
78 source_error: PayloadSourceError,
79 /// Returns how many bytes of payload the store now stores for this entry.
80 total_length_now_available: u64,
81 },
82 /// Something specific to this store implementation went wrong.
83 OperationError(OE),
84}
85
86impl<PayloadSourceError: Display + Error, OE: Display + Error> Display
87 for PayloadAppendError<PayloadSourceError, OE>
88{
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 match self {
91 PayloadAppendError::NoSuchEntry => {
92 write!(
93 f,
94 "No entry for the given subspace and path exists in this store"
95 )
96 }
97 PayloadAppendError::WrongEntry => {
98 write!(
99 f,
100 "The entry to whose payload to append to had an unexpected payload_digest."
101 )
102 }
103 PayloadAppendError::TooManyBytes => write!(
104 f,
105 "The payload source produced more bytes than were expected for this payload."
106 ),
107 PayloadAppendError::DigestMismatch => {
108 write!(f, "The complete payload's digest is not what was expected.")
109 }
110 PayloadAppendError::SourceError { source_error, .. } => {
111 write!(f, "The payload source emitted an error: {}", source_error)
112 }
113 PayloadAppendError::OperationError(err) => std::fmt::Display::fmt(err, f),
114 }
115 }
116}
117
118impl<PayloadSourceError: Display + Error, OE: Display + Error> Error
119 for PayloadAppendError<PayloadSourceError, OE>
120{
121}
122
123/// Returned when forgetting an entry fails.
124#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Copy)]
125pub enum ForgetEntryError<OE> {
126 /// The operation supplied an expected payload_digest, but it did not match the digest of the entry.
127 WrongEntry,
128 /// Something specific to this store implementation went wrong.
129 OperationError(OE),
130}
131
132impl<OE: Display + Error> Display for ForgetEntryError<OE> {
133 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
134 match self {
135 ForgetEntryError::WrongEntry => {
136 write!(
137 f,
138 "The entry to whose payload to append to had an unexpected payload_digest."
139 )
140 }
141 ForgetEntryError::OperationError(err) => std::fmt::Display::fmt(err, f),
142 }
143 }
144}
145
146impl<OE: Display + Error> Error for ForgetEntryError<OE> {}
147
148/// Returned when forgetting a payload fails.
149#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Copy)]
150pub enum ForgetPayloadError<OE> {
151 /// No entry for the given subspace and path exists in this store.
152 NoSuchEntry,
153 /// The operation supplied an expected payload_digest, but it did not match the digest of the entry.
154 WrongEntry,
155 /// Something specific to this store implementation went wrong.
156 OperationError(OE),
157}
158
159impl<OE: Display + Error> Display for ForgetPayloadError<OE> {
160 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161 match self {
162 ForgetPayloadError::NoSuchEntry => {
163 write!(
164 f,
165 "No entry for the given subspace and path exists in this store"
166 )
167 }
168 ForgetPayloadError::WrongEntry => {
169 write!(
170 f,
171 "The entry to whose payload to append to had an unexpected payload_digest."
172 )
173 }
174 ForgetPayloadError::OperationError(err) => std::fmt::Display::fmt(err, f),
175 }
176 }
177}
178
179impl<OE: Display + Error> Error for ForgetPayloadError<OE> {}
180
181/// Returned when retrieving a payload fails.
182#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Copy)]
183pub enum PayloadError<OE> {
184 /// The operation supplied an expected payload_digest, but it did not match the digest of the entry.
185 WrongEntry,
186 /// Something specific to this store implementation went wrong.
187 OperationError(OE),
188}
189
190impl<OE: Display + Error> Display for PayloadError<OE> {
191 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
192 match self {
193 PayloadError::WrongEntry => {
194 write!(
195 f,
196 "The entry to whose payload to append to had an unexpected payload_digest."
197 )
198 }
199 PayloadError::OperationError(err) => std::fmt::Display::fmt(err, f),
200 }
201 }
202}
203
204impl<OE: Display + Error> Error for PayloadError<OE> {}
205
206/// A notification about changes in a [`Store`]. You can obtain a producer of these via the [`Store::subscribe_area`] method.
207///
208/// An event subscription takes two parameters: the [`Area`] within events should be reported (any store mutations outside that area will not be reported to that subscription), and some optional `QueryIgnoreParams` for optionally filtering events based on whether they correspond to entries whose payload is the empty string and/or whose payload is not fully available in the local store. A more detailed description of how these ignore options impact events is given in the docs for each enum variant, but the general intuition is for the subscription to act as if it was on a store that did not inlcude ignored entries in the first place.
209///
210/// In the description of the enum variants, we write `sub_area` for the area of the subscription, and `ignores` for the subscription `QueryIgnoreParams`.
211#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
212pub enum StoreEvent<const MCL: usize, const MCC: usize, const MPL: usize, N, S, PD, AT> {
213 /// Emitted when an entry is inserted in `area`.
214 ///
215 /// - If `ignores.ignore_empty_payloads`, this is not emitted if the payload of the entry is the empty payload.
216 /// - If `ignores.ignore_incomplete_payloads`, this event is not emitted upon entry insertion, but only once its payload has been fully added to the store. In this case, the ingestion event is guaranteed to be emitted *before* the corresponding payload append event.
217 Ingested {
218 /// The entry that was inserted.
219 entry: AuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
220 /// A tag that determines whether we ourselves *created* this entry, or whether it arrived from some other data source. In the latter case, the data source is identified by a u64 id. This is not necessarily intented for application-dev-facing APIs, but rather for efficiently implementing replication services (where you want to forward new entries to other peers, but not to those from which you have just received them).
221 origin: EntryOrigin,
222 },
223 /// Emitted whenever an entry is inserted into the store that *might* cause pruning inside `area`. It is possible that no entry was actually pruned form the area, if nothing got overwritten.
224 ///
225 /// When the inserted entry falls into `area`, then the corresponding `PruneAlert` is always delivered *before* the corresponding `Ingested` event.
226 PruneAlert {
227 /// The entry that caused the pruning.
228 cause: AuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
229 },
230 /// An existing entry inside `area` received a portion of its corresponding payload.
231 ///
232 /// If `ignores.ignore_incomplete_payloads`, this is only emitted when the payload is now fully available. In this case, the corresponding `Ingested` event is guaranteed to be emitted before this `Appended` event.
233 Appended(LengthyAuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>),
234 /// Emitted whenever a non-ignored entry in `area` is forgotten via `Store::forget_entry`. No corresponding `PayloadForgotten` event is emitted in this case.
235 EntryForgotten {
236 /// The entry that was forgotten.
237 entry: AuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
238 },
239 /// Emitted whenever a call to `Store::forget_area` might affect `area`. No corresponding `AreaPayloadForgotten` event is emitted in this case.
240 AreaForgotten {
241 /// The area that was forgotten.
242 area: Area<MCL, MCC, MPL, S>,
243 /// A subarea that was retained (if any).
244 protected: Option<Area<MCL, MCC, MPL, S>>,
245 },
246 /// Emitted whenever the payload of a non-ignored entry in `area` is forgotten via `Store::forget_payload`. Emitted even if no payload bytes had been available to forget in the first place.
247 PayloadForgotten(AuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>),
248 /// Emitted whenever a call to `Store::forget_area_payloads` might affect `area`.
249 AreaPayloadsForgotten {
250 /// The area whose payloads were forgotten.
251 area: Area<MCL, MCC, MPL, S>,
252 /// A subarea whose payloads were retained (if any).
253 protected: Option<Area<MCL, MCC, MPL, S>>,
254 },
255}
256
257/// Describes which entries to ignore during a query.
258///
259/// The `Default::default()` ignores nothing.
260#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Copy, Default)]
261#[cfg_attr(feature = "dev", derive(Arbitrary))]
262pub struct QueryIgnoreParams {
263 /// Omit entries with locally incomplete corresponding payloads.
264 pub ignore_incomplete_payloads: bool,
265 /// Omit entries whose payload is the empty string.
266 pub ignore_empty_payloads: bool,
267}
268
269impl QueryIgnoreParams {
270 // An entry for which no payload bytes are available.
271 fn ignores_fresh_entry<const MCL: usize, const MCC: usize, const MPL: usize, N, S, PD>(
272 &self,
273 entry: &Entry<MCL, MCC, MPL, N, S, PD>,
274 ) -> bool
275 where
276 S: PartialEq,
277 {
278 // Ignore if necessary if empty payload
279 if self.ignore_empty_payloads && entry.payload_length() == 0 {
280 return true;
281 }
282
283 // Ignore if necessary if the entry has an incomplete payload (always unless the expected payload length is zero).
284 if self.ignore_incomplete_payloads && entry.payload_length() != 0 {
285 return true;
286 }
287
288 false
289 }
290
291 fn ignores_lengthy_authorised_entry<
292 const MCL: usize,
293 const MCC: usize,
294 const MPL: usize,
295 N,
296 S,
297 PD,
298 AT,
299 >(
300 &self,
301 entry: &LengthyAuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
302 ) -> bool
303 where
304 S: PartialEq,
305 {
306 // Ignore if necessary if empty payload
307 if self.ignore_empty_payloads && entry.entry().entry().payload_length() == 0 {
308 return true;
309 }
310
311 // Ignore if necessary if the entry has an incomplete payload (always unless the expected payload length is zero).
312 if self.ignore_incomplete_payloads
313 && entry.entry().entry().payload_length() != entry.available()
314 {
315 return true;
316 }
317
318 false
319 }
320}
321
322/// The origin of an entry ingestion event.
323#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Copy)]
324#[cfg_attr(feature = "dev", derive(Arbitrary))]
325pub enum EntryOrigin {
326 /// The entry was probably created on this machine.
327 Local,
328 /// The entry was sourced from another source with an ID assigned by us.
329 /// This is useful if you want to suppress the forwarding of entries to the peers from which the entry was originally sourced.
330 Remote(u64),
331}
332
333/// A [`Store`] is a set of [`AuthorisedEntry`] belonging to a single namespace, and a (possibly partial) corresponding set of payloads.
334pub trait Store<const MCL: usize, const MCC: usize, const MPL: usize, N, S, PD, AT> {
335 type Error: Display + Error + PartialEq;
336
337 /// Returns the [namespace](https://willowprotocol.org/specs/data-model/index.html#namespace) which all of this store's [`AuthorisedEntry`] belong to.
338 fn namespace_id(&self) -> &N;
339
340 /// Attempts to ingest an [`AuthorisedEntry`] into the [`Store`].
341 ///
342 /// Will fail if the entry belonged to a different namespace than the store's, or if the `prevent_pruning` param is `true` and an ingestion would have triggered [prefix pruning](https://willowprotocol.org/specs/data-model/index.html#prefix_pruning).
343 fn ingest_entry(
344 &self,
345 authorised_entry: AuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
346 prevent_pruning: bool,
347 origin: EntryOrigin,
348 ) -> impl Future<
349 Output = Result<
350 EntryIngestionSuccess<MCL, MCC, MPL, N, S, PD, AT>,
351 EntryIngestionError<Self::Error>,
352 >,
353 >;
354
355 /// Attempts to append part of a payload for an entry at a given SubspaceId-Path-pair.
356 ///
357 /// Will report an error if:
358 ///
359 /// - There is no entry for the given SubspaceId-Path pair.
360 /// - The payload digest of the entry at the given subspace_id and path is not equal to the supplied `expected_digest` (*if* one was supplied).
361 /// - The payload source produced more bytes than were expected for this payload.
362 /// - The payload source yielded an error.
363 /// - The final payload's digest did not match the expected digest
364 /// - Something else went wrong, e.g. there was no space for the payload on disk.
365 ///
366 /// This method **does not** and **cannot** verify the integrity of partial payloads. This means that arbitrary (and possibly malicious) payloads smaller than the expected size will be stored unless partial verification is implemented upstream (e.g. as part of a sync protocol).
367 fn append_payload<Producer, PayloadSourceError>(
368 &self,
369 subspace_id: &S,
370 path: &Path<MCL, MCC, MPL>,
371 expected_digest: Option<PD>,
372 payload_source: &mut Producer,
373 ) -> impl Future<
374 Output = Result<PayloadAppendSuccess, PayloadAppendError<PayloadSourceError, Self::Error>>,
375 >
376 where
377 Producer: BulkProducer<Item = u8, Error = PayloadSourceError>;
378
379 /// Locally forgets an entry with a given [`Path`] and [subspace](https://willowprotocol.org/specs/data-model/index.html#subspace) id, returning the forgotten entry, or an error if no entry with that path and subspace ID are held by this store. If an `expected_digest` is supplied and the entry turns out to not have that digest, then this method does nothing and reports an `ForgetEntryError::WrongEntry` error.
380 ///
381 /// If the entry in question is the last remaining reference in the store to a particular [`crate::PayloadDigest`], that payload will be forgotten from the store (if present).
382 ///
383 /// Forgetting is not the same as [pruning](https://willowprotocol.org/specs/data-model/index.html#prefix_pruning)! Subsequent joins with other [`Store`]s may bring the forgotten entry back.
384 fn forget_entry(
385 &self,
386 subspace_id: &S,
387 path: &Path<MCL, MCC, MPL>,
388 expected_digest: Option<PD>,
389 ) -> impl Future<Output = Result<(), ForgetEntryError<Self::Error>>>;
390
391 /// Locally forgets all [`AuthorisedEntry`] [included](https://willowprotocol.org/specs/grouping-entries/index.html#area_include) by a given [`crate::grouping::Area`], returning the number of forgotten entries.
392 ///
393 /// If forgetting many entries causes no there to be no remaining references to certain payload digests, those payloads will be removed (if present).
394 ///
395 /// If `protected` is `Some`, then all entries [included](https://willowprotocol.org/specs/grouping-entries/index.html#area_include) by that [`Area`] will be prevented from being forgotten, even though they are included by `area`.
396 ///
397 /// Forgetting is not the same as [pruning](https://willowprotocol.org/specs/data-model/index.html#prefix_pruning)! Subsequent joins with other [`Store`]s may bring the forgotten entries back.
398 fn forget_area(
399 &self,
400 area: &Area<MCL, MCC, MPL, S>,
401 protected: Option<&Area<MCL, MCC, MPL, S>>,
402 ) -> impl Future<Output = Result<usize, Self::Error>>;
403
404 /// Locally forgets the corresponding payload of the entry with a given path and subspace, panics if no entry with that path and subspace ID is held by this store. If an `expected_digest` is supplied and the entry turns out to not have that digest, then this method does nothing and reports a `ForgetPayloadError::WrongEntry` error.
405 ///
406 /// Forgetting is not the same as [pruning](https://willowprotocol.org/specs/data-model/index.html#prefix_pruning)! Subsequent joins with other [`Store`]s may bring the forgotten payload back.
407 fn forget_payload(
408 &self,
409 subspace_id: &S,
410 path: &Path<MCL, MCC, MPL>,
411 expected_digest: Option<PD>,
412 ) -> impl Future<Output = Result<(), ForgetPayloadError<Self::Error>>>;
413
414 /// Locally forgets all payloads with corresponding ['AuthorisedEntry'] [included](https://willowprotocol.org/specs/grouping-entries/index.html#area_include) by a given [`crate::grouping::Area`], returning a count of forgotten payloads. Payloads corresponding to entries *outside* of the given `area` param will be be prevented from being forgotten.
415 ///
416 /// If `protected` is `Some`, then all payloads corresponding to entries [included](https://willowprotocol.org/specs/grouping-entries/index.html#area_include) by that [`Area`] will be prevented from being forgotten, even though they are included by `area`.
417 ///
418 /// Forgetting is not the same as [pruning](https://willowprotocol.org/specs/data-model/index.html#prefix_pruning)! Subsequent joins with other [`Store`]s may bring the forgotten payloads back.
419 fn forget_area_payloads(
420 &self,
421 area: &Area<MCL, MCC, MPL, S>,
422 protected: Option<&Area<MCL, MCC, MPL, S>>,
423 ) -> impl Future<Output = Result<usize, Self::Error>>;
424
425 /// Forces persistence of all previous mutations
426 fn flush(&self) -> impl Future<Output = Result<(), Self::Error>>;
427
428 /// Returns a [`ufotofu::Producer`] of bytes for the payload corresponding to the given subspace id and path. If an `expected_digest` is supplied and the entry turns out to not have that digest, then this method does nothing and reports a `PayloadError::WrongEntry` error.
429 fn payload(
430 &self,
431 subspace_id: &S,
432 path: &Path<MCL, MCC, MPL>,
433 expected_digest: Option<PD>,
434 ) -> impl Future<
435 Output = Result<
436 Option<impl BulkProducer<Item = u8, Final = (), Error = Self::Error>>,
437 PayloadError<Self::Error>,
438 >,
439 >;
440
441 /// Returns a [`LengthyAuthorisedEntry`] with the given [`Path`] and [subspace](https://willowprotocol.org/specs/data-model/index.html#subspace) ID, if present.
442 fn entry(
443 &self,
444 subspace_id: &S,
445 path: &Path<MCL, MCC, MPL>,
446 ignore: QueryIgnoreParams,
447 ) -> impl Future<
448 Output = Result<Option<LengthyAuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>>, Self::Error>,
449 >;
450
451 /// Queries which entries are [included](https://willowprotocol.org/specs/grouping-entries/index.html#area_include) by an [`Area`], returning a producer of [`LengthyAuthorisedEntry`] **produced in an arbitrary order decided by the store implementation**.
452 fn query_area(
453 &self,
454 area: &Area<MCL, MCC, MPL, S>,
455 ignore: QueryIgnoreParams,
456 ) -> impl Future<
457 Output = Result<
458 impl Producer<Item = LengthyAuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>, Final = ()>,
459 Self::Error,
460 >,
461 >;
462
463 /// Subscribes to events concerning entries [included](https://willowprotocol.org/specs/grouping-entries/index.html#area_include) by an [`crate::grouping::Area`], returning a producer of `StoreEvent`s which occurred since the moment of calling this function.
464 fn subscribe_area(
465 &self,
466 area: &Area<MCL, MCC, MPL, S>,
467 ignore: QueryIgnoreParams,
468 ) -> impl Future<
469 Output = impl Producer<
470 Item = StoreEvent<MCL, MCC, MPL, N, S, PD, AT>,
471 Final = (),
472 Error = Self::Error,
473 >,
474 >;
475}
476
477//---------------------------//
478// In-Memory Event Queue //
479//---------------------------//
480
481// What follows is one possible technique for implementing the event subscription service offered by stores. This technique maintains a queue of (relevant) store operations. Subscribers maintain an offset into this queue; producing events works by advancing through the queue, ignoring irrelevant operations, and emitting events whenever appropriate. The queue has a maximum capacity, if it is reached, but some subscriber has not yet processed the oldest operation, then either the queue blocks or the subscriber is removed.
482
483// A more sophisticated implementation could go beyond a mere queue and remove operations that have been obsoleted by later operations. We don't do this here. A later implementation that stores the queue on persistent storage *must* implement such optimisations however, since "deleted" data must also disappear from the operations queue.
484
485/// An operation, as stored in the operations queue. Note that there is no op corresponding to the `PruneAlert` event, since those are generated from insertion ops. Note also that we use LengthyAuthorisedEntries instead of merely AuthorisedEntries for EntryForgotten and PayloadForgotten. This is so that we can respect QueryIgnoreParams.
486#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
487enum QueuedOp<const MCL: usize, const MCC: usize, const MPL: usize, N, S, PD, AT> {
488 Insertion {
489 entry: AuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
490 origin: EntryOrigin,
491 },
492 Appended {
493 lengthy_entry: LengthyAuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
494 },
495 EntryForgotten {
496 entry: LengthyAuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
497 },
498 AreaForgotten {
499 area: Area<MCL, MCC, MPL, S>,
500 protected: Option<Area<MCL, MCC, MPL, S>>,
501 },
502 PayloadForgotten {
503 entry: LengthyAuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
504 },
505 AreaPayloadsForgotten {
506 area: Area<MCL, MCC, MPL, S>,
507 protected: Option<Area<MCL, MCC, MPL, S>>,
508 },
509}
510
511// The store maintains a queue of `QueuedOp`s. Subscribers must be able to track their offset into the queue. But since items may be popped off, absolute offsets into the queue would be cumbersome. Instead, ops are addressed by a successively incremented counter (i.e., they get numbered sequentially). The store only needs to store the total number of ops that ever got popped off the queue in order to convert these absolute, unique, sequential op ids into local offsets in its queue.
512
513// The interesting part is how we organise our subscribers. Considerations:
514//
515// - we need to cheaply add and remove subscribers
516// - we need to cheaply query for the (a) subscriber which has the (a) lowest op id (so that we know whether we can pop off old events or not)
517// - we need to notify subscribers which reached the end of the queue when another op has been pushed
518//
519// We can elegantly satisfy these requirements by organising subscribers in a doubly-linked list which we keep sorted by the op id up to which the subscribers have processed the operations. But. Doubly-linked lists in rust (and in general, for that matter) are an absolute pain, because ownership and stuff. Read https://rust-unofficial.github.io/too-many-lists/ if you do not know what I am talking about.
520
521// So instead of the theoretically really nice solution, we'll hack something together. We store the subscribers in a [slab](https://docs.rs/slab/latest/slab/). When needing to pop an op, we do not remove any subscribers, their next attempt to produce an event will simply yield the final item of the producer. Dropping the user-facing part of the subscription also removes the internal part. For notifying up-to-date subscribers of a new push, simply iterate through the full slab. For realistic numbers of subscribers, this is probably not only "efficient enough", but actually significantly more performant than a doubly-linked list.
522
523#[derive(Debug)]
524pub struct EventSystem<const MCL: usize, const MCC: usize, const MPL: usize, N, S, PD, AT, Err> {
525 op_queue: VecDeque<QueuedOp<MCL, MCC, MPL, N, S, PD, AT>>,
526 // A statically set limit on how many ops to buffer at most at the same time.
527 max_queue_capacity: usize,
528 /// Total number of ops that have been popped of the op_queue so far
529 popped_count: u64,
530 subscribers: Slab<InternalSubscriber<Err>>,
531}
532
533impl<const MCL: usize, const MCC: usize, const MPL: usize, N, S, PD, AT, Err>
534 EventSystem<MCL, MCC, MPL, N, S, PD, AT, Err>
535{
536 /// Creates a new Eventsystem.
537 pub fn new(max_queue_capacity: usize) -> Self {
538 Self {
539 op_queue: VecDeque::new(),
540 max_queue_capacity,
541 popped_count: 0,
542 subscribers: Slab::new(),
543 }
544 }
545
546 /// Create a new subscription: setting up the internals, and returning the external part.
547 pub fn add_subscription(
548 this: Rc<RefCell<Self>>,
549 area: Area<MCL, MCC, MPL, S>,
550 ignore: QueryIgnoreParams,
551 ) -> Subscriber<MCL, MCC, MPL, N, S, PD, AT, Err> {
552 let cell = Rc::new(TakeCell::new());
553
554 let internal = InternalSubscriber {
555 next_op_id: cell.clone(),
556 };
557 let key = this.borrow_mut().subscribers.insert(internal);
558
559 Subscriber {
560 events: this,
561 next_op_id: cell,
562 slab_key: key,
563 area,
564 ignore,
565 buffered_event: None,
566 }
567 }
568
569 /// Call this inside your store impl after it has ingested an entry.
570 pub fn ingested_entry(
571 &mut self,
572 entry: AuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
573 origin: EntryOrigin,
574 ) {
575 self.enqueue_op(QueuedOp::Insertion { entry, origin })
576 }
577
578 /// Call this inside your store impl after it has appended to a payload.
579 pub fn appended_payload(
580 &mut self,
581 lengthy_entry: LengthyAuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>,
582 ) {
583 self.enqueue_op(QueuedOp::Appended { lengthy_entry })
584 }
585
586 /// Call this inside your store impl after it has forgotten an entry.
587 pub fn forgot_entry(&mut self, entry: LengthyAuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>) {
588 self.enqueue_op(QueuedOp::EntryForgotten { entry })
589 }
590
591 /// Call this inside your store impl after it has forgotten an area.
592 pub fn forgot_area(
593 &mut self,
594 area: Area<MCL, MCC, MPL, S>,
595 protected: Option<Area<MCL, MCC, MPL, S>>,
596 ) {
597 self.enqueue_op(QueuedOp::AreaForgotten { area, protected })
598 }
599
600 /// Call this inside your store impl after it has forgotten a payload.
601 pub fn forgot_payload(&mut self, entry: LengthyAuthorisedEntry<MCL, MCC, MPL, N, S, PD, AT>) {
602 self.enqueue_op(QueuedOp::PayloadForgotten { entry })
603 }
604
605 /// Call this inside your store impl after it has forgotten the payloads of an area.
606 pub fn forgot_area_payloads(
607 &mut self,
608 area: Area<MCL, MCC, MPL, S>,
609 protected: Option<Area<MCL, MCC, MPL, S>>,
610 ) {
611 self.enqueue_op(QueuedOp::AreaPayloadsForgotten { area, protected })
612 }
613
614 // We enqueue an operation. If the max capacity of the queue is reached through that, we pop the oldest op (which might cause straggling subscribers to be cancelled the next time they try to produce an event). If any subscribers have been awaiting a new op, we notify them.
615 fn enqueue_op(&mut self, op: QueuedOp<MCL, MCC, MPL, N, S, PD, AT>) {
616 self.op_queue.push_back(op);
617
618 if self.op_queue.len() > self.max_queue_capacity {
619 self.op_queue.pop_front();
620 }
621
622 for (_, sub) in self.subscribers.iter() {
623 if sub.next_op_id.is_empty() {
624 sub.next_op_id
625 .set(Ok(self.popped_count + (self.op_queue.len() as u64) - 1));
626 }
627 }
628 }
629
630 /// Given an op id, return the matching stored QueuedOp, or None if the id is too old (the corresponding op has already been popped).
631 fn resolve_op_id(&self, id: u64) -> Option<&QueuedOp<MCL, MCC, MPL, N, S, PD, AT>> {
632 match id.checked_sub(self.popped_count) {
633 None => None,
634 Some(index) => self.op_queue.get(index as usize),
635 }
636 }
637}
638
639/// The internal part of a subscriber.
640#[derive(Debug)]
641pub struct InternalSubscriber<Err> {
642 // This allows the public endpoint to await new entries. Stores Ok(op_id) of the next op_id to retrieve, stores Err(err) to emit an error err, is empty while the subscriber is fully up to date.
643 next_op_id: Rc<TakeCell<Result<u64, Err>>>,
644}
645
646/// The public-facing part of a subscriber (to be returned by Store::subscribe_area).
647#[derive(Debug)]
648pub struct Subscriber<const MCL: usize, const MCC: usize, const MPL: usize, N, S, PD, AT, Err> {
649 events: Rc<RefCell<EventSystem<MCL, MCC, MPL, N, S, PD, AT, Err>>>,
650 // Shared with InternalSubscriber.next_op_id.
651 next_op_id: Rc<TakeCell<Result<u64, Err>>>,
652 /// The key by which the internal subscriber part is stored in the EventSystem. Upon dropping, the Subscriber, the corresponding InternalSubscriber is removed from the slab.
653 slab_key: usize,
654 area: Area<MCL, MCC, MPL, S>,
655 ignore: QueryIgnoreParams,
656 /// Some store ops trigger *two* events. In those cases, the second event is stored here. Each call to `produce` checks for a buffered event first before continuing to process the op queue.
657 buffered_event: Option<StoreEvent<MCL, MCC, MPL, N, S, PD, AT>>,
658}
659
660impl<const MCL: usize, const MCC: usize, const MPL: usize, N, S, PD, AT, Err> Drop
661 for Subscriber<MCL, MCC, MPL, N, S, PD, AT, Err>
662{
663 fn drop(&mut self) {
664 self.events.borrow_mut().subscribers.remove(self.slab_key);
665 }
666}
667
668impl<const MCL: usize, const MCC: usize, const MPL: usize, N, S, PD, AT, Err> Producer
669 for Subscriber<MCL, MCC, MPL, N, S, PD, AT, Err>
670where
671 N: Clone,
672 S: PartialEq + Clone,
673 PD: Clone,
674 AT: Clone,
675{
676 type Item = StoreEvent<MCL, MCC, MPL, N, S, PD, AT>;
677
678 type Final = ();
679
680 type Error = Err;
681
682 async fn produce(&mut self) -> Result<Either<Self::Item, Self::Final>, Self::Error> {
683 if let Some(buffered) = self.buffered_event.take() {
684 return Ok(Left(buffered));
685 }
686
687 // We loop and skip over events that we ignore.
688 // We exit the loop when no more events are available or upon hitting an event we do not ignore.
689 loop {
690 match self.next_op_id.take().await {
691 Err(err) => return Err(err),
692 Ok(op_id) => {
693 match self.events.borrow().resolve_op_id(op_id) {
694 None => {
695 // We lag too far behind.
696 return Ok(Right(()));
697 }
698 Some(op) => {
699 // Advance the op_id.
700 if op_id + 1
701 == self.events.borrow().popped_count
702 + (self.events.borrow().op_queue.len() as u64)
703 {
704 // reached the end of the op queue. Do nothing, the next event insertion will fill self.next_op_id
705 } else {
706 self.next_op_id.set(Ok(op_id + 1));
707 }
708
709 match op {
710 QueuedOp::Appended { lengthy_entry } => {
711 if !self.area.includes_entry(lengthy_entry.entry().entry())
712 || self
713 .ignore
714 .ignores_lengthy_authorised_entry(lengthy_entry)
715 {
716 continue;
717 }
718
719 if self.ignore.ignore_incomplete_payloads {
720 // If the entry was ignored due to an incomplete payload, we buffer the append event and emit an insertion event first.
721 self.buffered_event =
722 Some(StoreEvent::Appended(lengthy_entry.clone()));
723
724 return Ok(Left(StoreEvent::Ingested {
725 entry: lengthy_entry.entry().clone(),
726 origin: EntryOrigin::Local,
727 }));
728 } else {
729 // Otherwise, emit the Appended event directly.
730 return Ok(Left(StoreEvent::Appended(
731 lengthy_entry.clone(),
732 )));
733 }
734 }
735 QueuedOp::AreaForgotten { area, protected } => {
736 if area.intersection(&self.area).is_some() {
737 if let Some(prot) = protected {
738 if prot.includes_area(&self.area) {
739 // continue with area, since the subscribed area is fully protected
740 continue;
741 }
742 }
743
744 return Ok(Left(StoreEvent::AreaForgotten {
745 area: area.clone(),
746 protected: protected.clone(),
747 }));
748 } else {
749 // no-op, continue with next event
750 }
751 }
752 QueuedOp::AreaPayloadsForgotten { area, protected } => {
753 if area.intersection(&self.area).is_some() {
754 if let Some(prot) = protected {
755 if prot.includes_area(&self.area) {
756 // continue with area, since the subscribed area is fully protected
757 continue;
758 }
759 }
760
761 return Ok(Left(StoreEvent::AreaPayloadsForgotten {
762 area: area.clone(),
763 protected: protected.clone(),
764 }));
765 } else {
766 // no-op, continue with next event
767 }
768 }
769 QueuedOp::EntryForgotten { entry } => {
770 if self.area.includes_entry(entry.entry().entry())
771 && !self.ignore.ignores_lengthy_authorised_entry(entry)
772 {
773 return Ok(Left(StoreEvent::EntryForgotten {
774 entry: entry.entry().clone(),
775 }));
776 } else {
777 // no-op, continue with next event
778 }
779 }
780
781 QueuedOp::Insertion { entry, origin } => {
782 // Is the entry in the subscribed-to area?
783 if self.area.includes_entry(entry.entry()) {
784 if self.ignore.ignores_fresh_entry(entry.entry()) {
785 return Ok(Left(StoreEvent::PruneAlert {
786 cause: entry.clone(),
787 }));
788 } else {
789 // Insertion is not ignored.
790 // Buffer the actual insertion event, then emit the prune alert.
791 self.buffered_event = Some(StoreEvent::Ingested {
792 entry: entry.clone(),
793 origin: *origin,
794 });
795
796 return Ok(Left(StoreEvent::PruneAlert {
797 cause: entry.clone(),
798 }));
799 }
800 } else if self.area.could_be_pruned_by(entry.entry()) {
801 // Insertion outside area but might still prune something inside the area.
802 return Ok(Left(StoreEvent::PruneAlert {
803 cause: entry.clone(),
804 }));
805 } else {
806 // no-op, insertion does not affect the area, continue with next event
807 }
808 }
809
810 QueuedOp::PayloadForgotten { entry } => {
811 if self.area.includes_entry(entry.entry().entry())
812 && !self.ignore.ignores_lengthy_authorised_entry(entry)
813 {
814 return Ok(Left(StoreEvent::PayloadForgotten(
815 entry.entry().clone(),
816 )));
817 } else {
818 // no-op, continue with next event
819 }
820 }
821 }
822 }
823 }
824 }
825 }
826 }
827 }
828}