Skip to main content

harmos_runtime/streams/
publications.rs

1/* ***
2 *
3 * Harmos
4 *
5 * SPDX-License-Identifier: Apache-2.0
6 *
7 * ***
8 */
9
10/* === Modules === */
11
12/* === Re-Exports === */
13
14/* === Imports === */
15
16// Standard Library Imports
17use std::collections::BTreeSet;
18use std::marker::PhantomData;
19use std::ops::RangeBounds;
20use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
21use std::sync::{Arc, Mutex, Weak};
22// Third Party Imports
23use tokio::sync::watch;
24// Local Imports
25use super::definition::Windows;
26use super::{Demand, Holding, Item, Query, StreamManager, Tier, Wired, window};
27use crate::{Application, state::Position, streams::Definition};
28use harmos_domain::inventory::publications;
29use harmos_domain::{StreamDeclaration, artifact_subset};
30
31/* === Constants === */
32
33const MAX_PUBLICATION_BYTES: usize = 1024 * 1024;
34const MAX_WINDOW_ROWS: usize = 65_536;
35
36/* === Macros === */
37
38/* === Type Aliases === */
39
40/* === Structs === */
41
42#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
43pub struct PublicationRow {
44    pub payload: Vec<u8>,
45    pub published_unix_micros: Option<i64>,
46
47    #[serde(default)]
48    pub context: Option<String>,
49}
50
51#[derive(Clone)]
52pub struct PluginStream {
53    pub(super) wired: Arc<Wired>,
54    pub(super) tier: Tier,
55    pub(super) closed: Arc<AtomicBool>,
56    pub(super) pool: Arc<super::publication::Pool>,
57    pub(super) ingest: Arc<super::ingest::Ingest>,
58}
59
60pub struct PublicationLease {
61    pub(super) stream: PluginStream,
62    pub(super) active: Arc<Mutex<bool>>,
63    pub(super) applied: watch::Receiver<Position>,
64    pub(super) sequence: AtomicU64,
65}
66
67/* === Enums === */
68
69#[derive(Debug, thiserror::Error)]
70pub enum StreamAdmissionError {
71    #[error("`{0}` is not a portable plugin owner identity")]
72    Owner(String),
73
74    #[error(transparent)]
75    Declaration(#[from] publications::Error),
76
77    #[error("stream `{0}` is already owned by a live publication lease")]
78    Occupied(String),
79
80    #[error("stream `{0}` changes an admitted schema or retention contract without a new version")]
81    Changed(String),
82
83    #[error("stream `{0}` collides with a native catalog definition")]
84    Native(String),
85
86    #[error("a plugin stream window exceeds the 65536 row limit")]
87    WindowLimit,
88
89    #[error("stream bindings differ from the admitted plugin inventory")]
90    Inventory,
91
92    #[error("the stream facility has stopped")]
93    Closed,
94
95    #[error(transparent)]
96    Schema(#[from] crate::schema::Error),
97}
98
99#[derive(Debug, PartialEq, Eq, thiserror::Error)]
100pub enum StreamPublicationError {
101    #[error("the publication lease is no longer active")]
102    Revoked,
103
104    #[error("a plugin publication exceeds the 1 MiB row limit")]
105    Oversized,
106
107    #[error("the shared plugin publication byte or slot capacity is full")]
108    Capacity,
109
110    #[error("the publication sequence is invalid or exhausted")]
111    Sequence,
112
113    #[error("a batched row is stamped earlier than the row before it")]
114    Stamp,
115
116    #[error("the stream window is full of rows its ingest has not taken in")]
117    Backlog,
118
119    #[error("the plugin activation has not committed")]
120    NotReady,
121
122    #[error(transparent)]
123    Payload(#[from] crate::schema::PayloadError),
124}
125
126/* === Traits === */
127
128/* === Implementations === */
129
130impl<A: Application> StreamManager<A> {
131    pub async fn admit(&self, owner: &str, declaration: StreamDeclaration) -> Result<PublicationLease, StreamAdmissionError> {
132        Ok(self.admit_all(owner, [declaration]).await?.remove(0))
133    }
134
135    pub fn validate_publications(
136        &self, owner: &str, declarations: impl IntoIterator<Item = StreamDeclaration>,
137    ) -> Result<(), StreamAdmissionError> {
138        let definitions = publication_definitions(owner, declarations)?;
139        if self.closed.load(Ordering::Acquire) {
140            return Err(StreamAdmissionError::Closed);
141        }
142        let _visibility = self.windows.visibility.read().expect("stream visibility poisoned");
143        let windows = self.windows.windows.read().expect("stream registry poisoned");
144        validate_existing(&windows, &definitions, false)
145    }
146
147    pub async fn admit_all(
148        &self, owner: &str, declarations: impl IntoIterator<Item = StreamDeclaration>,
149    ) -> Result<Vec<PublicationLease>, StreamAdmissionError> {
150        let definitions = publication_definitions(owner, declarations)?;
151        let validations = definitions
152            .iter()
153            .map(|definition| {
154                crate::schema::Validator::from_schema(
155                    definition.schema().expect("plugin definitions own a row schema"),
156                    &harmos_domain::config::SchemaSettings::default(),
157                )
158                .map_err(crate::schema::Error::from)
159                .map(Arc::new)
160            })
161            .collect::<Result<Vec<_>, _>>()?;
162        {
163            let _visibility = self.windows.visibility.read().expect("stream visibility poisoned");
164            let windows = self.windows.windows.read().expect("stream registry poisoned");
165            validate_existing(&windows, &definitions, true)?;
166        }
167        if self.closed.load(Ordering::Acquire) {
168            return Err(StreamAdmissionError::Closed);
169        }
170        let mut floors = Vec::with_capacity(definitions.len());
171        for definition in &definitions {
172            floors.push(self.tier.seal(definition.clone(), Vec::new()).await);
173        }
174
175        let _visibility = self.windows.visibility.read().expect("stream visibility poisoned");
176        let mut windows = self.windows.windows.write().expect("stream registry poisoned");
177        if self.closed.load(Ordering::Acquire) {
178            return Err(StreamAdmissionError::Closed);
179        }
180        validate_existing(&windows, &definitions, true)?;
181        let mut leases = Vec::with_capacity(definitions.len());
182        for ((definition, floor), validation) in definitions.into_iter().zip(floors).zip(validations) {
183            let revisions = windows.entry(Arc::<str>::from(definition.id())).or_default();
184            // A presentation-only edit keeps the wire revision and retained rows.
185            // Existing readers keep their immutable descriptor; new discovery sees
186            // the replacement's authored metadata.
187            if let Some(previous) = revisions.get(&definition.version())
188                && (previous.is_staged()
189                    || previous.definition.metadata() != definition.metadata()
190                    || previous.definition.row_metadata() != definition.row_metadata())
191            {
192                revisions.insert(
193                    definition.version(),
194                    Arc::new(Wired {
195                        validation: Some(Arc::clone(&validation)),
196                        window: Arc::clone(&previous.window),
197                        definition: definition.clone(),
198                        publisher: Mutex::new(Weak::new()),
199                        #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
200                        staged: None,
201                    }),
202                );
203            }
204            let wired = revisions
205                .entry(definition.version())
206                .or_insert_with(|| {
207                    let wired = Arc::new(Wired {
208                        validation: Some(Arc::clone(&validation)),
209                        window: Arc::new(
210                            window::Window::wired(
211                                definition.window(),
212                                floor,
213                                Arc::clone(&self.windows.sequence),
214                                self.windows.pump.clone(),
215                            )
216                            .visible_with(self.windows.visibility.clone()),
217                        ),
218                        definition,
219                        publisher: Mutex::new(Weak::new()),
220                        #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
221                        staged: None,
222                    });
223                    if floor > Position::ORIGIN && floor <= *self.applied.borrow() {
224                        wired.window.stands_on(floor);
225                    }
226                    wired
227                })
228                .clone();
229            let active = Arc::new(Mutex::new(true));
230            *wired.publisher.lock().expect("stream publisher poisoned") = Arc::downgrade(&active);
231            leases.push(PublicationLease {
232                stream: PluginStream {
233                    wired,
234                    tier: self.tier.clone(),
235                    closed: Arc::clone(&self.closed),
236                    pool: self.windows.publications.clone(),
237                    ingest: self.windows.ingest.clone(),
238                },
239                active,
240                applied: self.applied.clone(),
241                sequence: AtomicU64::new(0),
242            });
243        }
244        Ok(leases)
245    }
246
247    pub fn publication(&self, id: &str, version: u32) -> Option<PluginStream> {
248        let wired = self.windows.wired(id, version)?;
249        wired.definition.owner()?;
250
251        Some(PluginStream {
252            wired,
253            tier: self.tier.clone(),
254            closed: Arc::clone(&self.closed),
255            pool: self.windows.publications.clone(),
256            ingest: self.windows.ingest.clone(),
257        })
258    }
259
260    pub fn definitions(&self) -> Vec<Definition> {
261        let mut definitions: Vec<_> = self.windows.declared().into_iter().map(|wired| wired.definition.clone()).collect();
262        definitions.sort_by(|a, b| (a.id(), a.version()).cmp(&(b.id(), b.version())));
263        definitions
264    }
265}
266
267impl PublicationLease {
268    pub fn stream(&self) -> &PluginStream {
269        &self.stream
270    }
271
272    pub fn publish(&self, payload: Vec<u8>, missed: u64) -> Result<(), StreamPublicationError> {
273        self.publish_row(PublicationRow { payload, published_unix_micros: None, context: None }, missed)
274    }
275
276    pub fn publish_row(&self, row: PublicationRow, missed: u64) -> Result<(), StreamPublicationError> {
277        self.admit(row, None, missed).map(|_| ())
278    }
279
280    pub fn admit_row(
281        &self, row: PublicationRow, wire_sequence: u64, missed: u64,
282    ) -> Result<super::ManagedPublication, StreamPublicationError> {
283        self.admit(row, Some(wire_sequence), missed)
284    }
285
286    #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
287    pub fn stage_row(
288        &self, row: PublicationRow, wire_sequence: u64, missed: u64,
289    ) -> Result<super::StagedPublication, StreamPublicationError> {
290        self.stage_sequence(vec![row], Some(wire_sequence), missed, None).single()
291    }
292
293    // An ordered batch takes contiguous sequences from `first_wire_sequence`;
294    // `missed` applies before the first row only.
295    #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
296    pub fn stage_rows(&self, rows: Vec<PublicationRow>, first_wire_sequence: u64, missed: u64) -> super::StagedRows {
297        self.stage_sequence(rows, Some(first_wire_sequence), missed, None)
298    }
299
300    #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
301    pub fn stage_derived_row(&self, row: PublicationRow, missed: u64) -> Result<super::StagedPublication, StreamPublicationError> {
302        self.stage_sequence(vec![row], None, missed, None).single()
303    }
304
305    #[cfg(feature = "plugin-sidecar")]
306    pub(crate) fn stage_rows_in(
307        &self, context: &crate::operations::Context<'_>, rows: Vec<PublicationRow>, first_wire_sequence: u64, missed: u64,
308    ) -> super::StagedRows {
309        self.stage_sequence(rows, Some(first_wire_sequence), missed, Some(context))
310    }
311
312    // One visibility fence and one scope phase cover the whole batch, so no
313    // reader or commit observes it half staged.
314    #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
315    fn stage_sequence(
316        &self, rows: Vec<PublicationRow>, wire_sequence: Option<u64>, missed: u64, context: Option<&crate::operations::Context<'_>>,
317    ) -> super::StagedRows {
318        let Some(staged) = self.stream.wired.staged.as_ref() else {
319            return super::StagedRows::refused(Vec::new(), StreamPublicationError::NotReady);
320        };
321        let _visibility = self.stream.wired.window.visibility.write().expect("stream visibility poisoned");
322        let mut evicted = Vec::new();
323        let prepared = staged.scope.prepare_publication_in(context, |ready| {
324            let activation = staged.activation.upgrade().ok_or(StreamPublicationError::Revoked)?;
325            let mut receipts = Vec::with_capacity(rows.len());
326            let mut stamped = None;
327            // Staged Rows Join The Window At Commit; They Must Evict No Unread Row.
328            let mut room = if ready {
329                usize::MAX
330            } else {
331                self.stream.wired.window.room(self.stream.ingest.taken()).saturating_sub(activation.pending(&self.stream.wired))
332            };
333            for (offset, row) in (0_u64..).zip(rows) {
334                if room == 0 {
335                    return Ok((receipts, ready, Some(StreamPublicationError::Backlog)));
336                }
337                room -= 1;
338                let sequence = match wire_sequence.map(|first| first.checked_add(offset)) {
339                    Some(None) => {
340                        return Ok((receipts, ready, Some(StreamPublicationError::Sequence)));
341                    }
342                    sequence => sequence.flatten(),
343                };
344                // Stamps Within One Batch Never Go Back
345                if let Some(stamp) = row.published_unix_micros {
346                    if stamped.is_some_and(|previous| stamp < previous) {
347                        return Ok((receipts, ready, Some(StreamPublicationError::Stamp)));
348                    }
349                    stamped = Some(stamp);
350                }
351                let missed = if offset == 0 { missed } else { 0 };
352                let (publication, dropped) = match self.admit_locked(row, sequence, missed, ready) {
353                    Ok(admitted) => admitted,
354                    Err(error) => return Ok((receipts, ready, Some(error))),
355                };
356                evicted.extend(dropped);
357                receipts.push(if ready {
358                    let (_, committed) = watch::channel(true);
359                    super::StagedPublication { publication, committed }
360                } else {
361                    activation.push(&self.stream.wired, *self.applied.borrow(), publication)
362                });
363            }
364            Ok((receipts, ready, None))
365        });
366        drop(_visibility);
367        drop(evicted);
368        let (receipts, ready, refused) = match prepared {
369            Ok(prepared) => prepared,
370            Err(error) => return super::StagedRows::refused(Vec::new(), error),
371        };
372        if ready && !receipts.is_empty() {
373            self.stream.wired.window.knock();
374            self.stream.wired.window.wake_delivery();
375        }
376        match refused {
377            Some(error) => super::StagedRows::refused(receipts, error),
378            None => super::StagedRows::staged(receipts),
379        }
380    }
381
382    fn admit(&self, row: PublicationRow, sequence: Option<u64>, missed: u64) -> Result<super::ManagedPublication, StreamPublicationError> {
383        let _visibility = self.stream.wired.window.visibility.write().expect("stream visibility poisoned");
384        let result = {
385            #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
386            if let Some(staged) = &self.stream.wired.staged {
387                staged.scope.prepare_publication(|ready| {
388                    if !ready {
389                        return Err(StreamPublicationError::NotReady);
390                    }
391                    self.admit_locked(row, sequence, missed, true)
392                })
393            } else {
394                self.admit_locked(row, sequence, missed, true)
395            }
396            #[cfg(not(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar")))]
397            {
398                self.admit_locked(row, sequence, missed, true)
399            }
400        };
401        drop(_visibility);
402        let (publication, evicted) = result?;
403        drop(evicted);
404        self.stream.wired.window.knock();
405        self.stream.wired.window.wake_delivery();
406        Ok(publication)
407    }
408
409    // Visibility and original scope phase precede writer and row-state locks.
410    fn admit_locked(
411        &self, row: PublicationRow, sequence: Option<u64>, missed: u64, live: bool,
412    ) -> Result<(super::ManagedPublication, Option<super::window::Row>), StreamPublicationError> {
413        let active = self.active.lock().expect("stream lease poisoned");
414        if !*active || self.stream.closed.load(Ordering::Acquire) {
415            return Err(StreamPublicationError::Revoked);
416        }
417        if row.payload.len() > MAX_PUBLICATION_BYTES {
418            return Err(StreamPublicationError::Oversized);
419        }
420        let next = self
421            .sequence
422            .load(Ordering::Relaxed)
423            .checked_add(missed)
424            .and_then(|sequence| sequence.checked_add(1))
425            .ok_or(StreamPublicationError::Sequence)?;
426        if sequence.is_some_and(|sequence| sequence != next) {
427            return Err(StreamPublicationError::Sequence);
428        }
429        self.stream.wired.validation.as_ref().expect("compiled plugin schema").validate(&row.payload)?;
430        let generation = {
431            #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
432            {
433                self.stream.wired.staged.as_ref().map(|staged| staged.scope.generation().get())
434            }
435            #[cfg(not(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar")))]
436            {
437                None
438            }
439        };
440        if live && !self.stream.wired.window.can_advance(missed.checked_add(1).ok_or(StreamPublicationError::Sequence)?) {
441            return Err(StreamPublicationError::Sequence);
442        }
443        let taken = self.stream.ingest.taken();
444        if live && self.stream.wired.window.evicts_unread(taken) {
445            return Err(StreamPublicationError::Backlog);
446        }
447        let publication = self.stream.pool.retain(
448            &self.stream.wired.definition,
449            generation,
450            row,
451            next,
452            missed,
453            live.then_some((self.stream.wired.window.as_ref(), taken)),
454        )?;
455        let evicted = if live {
456            self.stream.wired.window.append_locked(*self.applied.borrow(), Arc::new(publication.clone()), missed)?
457        } else {
458            None
459        };
460        self.sequence.store(next, Ordering::Relaxed);
461        Ok((publication, evicted))
462    }
463
464    pub fn revoke(&self) {
465        #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
466        if let Some(staged) = &self.stream.wired.staged {
467            staged.scope.unbind_streams();
468        }
469        *self.active.lock().expect("stream lease poisoned") = false;
470        self.stream.ingest.wake();
471    }
472
473    // Wait until every live feed has taken in every row published on this
474    // stream so far. A caught-up stream answers without yielding.
475    pub async fn ingested(&self) -> Result<(), StreamPublicationError> {
476        let mut taken = self.stream.ingest.watch();
477        loop {
478            if !*self.active.lock().expect("stream lease poisoned") || self.stream.closed.load(Ordering::Acquire) {
479                return Err(StreamPublicationError::Revoked);
480            }
481            let published = self.stream.wired.window.latest_locked();
482            if taken.borrow_and_update().values().all(|cursor| *cursor >= published) {
483                return Ok(());
484            }
485            taken.changed().await.map_err(|_| StreamPublicationError::Revoked)?;
486        }
487    }
488}
489
490impl Drop for PublicationLease {
491    fn drop(&mut self) {
492        self.revoke();
493    }
494}
495
496impl PluginStream {
497    pub fn definition(&self) -> &Definition {
498        &self.wired.definition
499    }
500
501    pub fn query(&self, range: impl RangeBounds<Position>) -> Query<PublicationRow> {
502        Query {
503            window: Arc::clone(&self.wired.window),
504            definition: self.wired.definition.clone(),
505            tier: self.tier.clone(),
506            start: range.start_bound().cloned(),
507            end: range.end_bound().cloned(),
508            as_of: None,
509            partition: None,
510            retained_only: false,
511            marker: PhantomData,
512        }
513    }
514
515    pub fn watch(&self) -> impl futures_core::Stream<Item = Item<PublicationRow>> + Send + Unpin + use<> {
516        let window = Arc::clone(&self.wired.window);
517        let holding = Holding::engage(Arc::clone(&window));
518        window::following(window, Arc::clone(&self.closed), holding)
519    }
520
521    pub fn watch_managed(&self) -> impl futures_core::Stream<Item = Item<super::ManagedPublication>> + Send + Unpin + use<> {
522        let window = Arc::clone(&self.wired.window);
523        let holding = Holding::engage(Arc::clone(&window));
524        window::following(window, Arc::clone(&self.closed), holding)
525    }
526
527    pub fn demand(&self) -> Demand {
528        Demand::of(&self.wired.window)
529    }
530}
531
532impl Wired {
533    fn is_staged(&self) -> bool {
534        #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
535        {
536            self.staged.is_some()
537        }
538        #[cfg(not(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar")))]
539        {
540            false
541        }
542    }
543}
544
545/* === Public Functions === */
546
547pub(super) fn publication_definitions(
548    owner: &str, declarations: impl IntoIterator<Item = StreamDeclaration>,
549) -> Result<Vec<Definition>, StreamAdmissionError> {
550    if !artifact_subset(owner) {
551        return Err(StreamAdmissionError::Owner(owner.into()));
552    }
553    let mut ids = BTreeSet::new();
554    declarations
555        .into_iter()
556        .map(|declaration| {
557            publications::validate(&declaration)?;
558            if declaration.window > MAX_WINDOW_ROWS {
559                return Err(StreamAdmissionError::WindowLimit);
560            }
561            if !ids.insert(declaration.id.clone()) {
562                return Err(publications::Error::DuplicateStream { id: declaration.id }.into());
563            }
564            let definition = Definition::declared::<PublicationRow>(declaration.id, declaration.schema, declaration.window);
565            let definition = definition.presentation(declaration.metadata, declaration.row_metadata).publication(owner, declaration.row);
566            Ok(definition)
567        })
568        .collect()
569}
570
571pub(super) fn validate_existing(
572    windows: &Windows, definitions: &[Definition], require_unoccupied: bool,
573) -> Result<(), StreamAdmissionError> {
574    for definition in definitions {
575        let Some(revisions) = windows.get(definition.id()) else {
576            continue;
577        };
578        if revisions.values().any(|wired| wired.definition.owner().is_none()) {
579            return Err(StreamAdmissionError::Native(definition.id().into()));
580        }
581        if revisions.values().any(|wired| wired.definition.owner() != definition.owner()) {
582            return Err(StreamAdmissionError::Changed(definition.id().into()));
583        }
584        let Some(wired) = revisions.get(&definition.version()) else {
585            continue;
586        };
587        if wired.definition.owner() != definition.owner()
588            || wired.definition.schema() != definition.schema()
589            || wired.definition.window() != definition.window()
590        {
591            return Err(StreamAdmissionError::Changed(definition.id().into()));
592        }
593        #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
594        if require_unoccupied && wired.staged.as_ref().is_some_and(|staged| !staged.scope.has_committed()) {
595            return Err(StreamAdmissionError::Occupied(definition.id().into()));
596        }
597        if require_unoccupied
598            && wired
599                .publisher
600                .lock()
601                .expect("stream publisher poisoned")
602                .upgrade()
603                .is_some_and(|lease| *lease.lock().expect("stream lease poisoned"))
604        {
605            return Err(StreamAdmissionError::Occupied(definition.id().into()));
606        }
607    }
608    Ok(())
609}
610
611/* === Private Functions === */
612
613/* === Tests === */