1use std::collections::BTreeSet;
18use std::marker::PhantomData;
19use std::ops::RangeBounds;
20use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
21use std::sync::{Arc, Mutex, Weak};
22use tokio::sync::watch;
24use 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
31const MAX_PUBLICATION_BYTES: usize = 1024 * 1024;
34const MAX_WINDOW_ROWS: usize = 65_536;
35
36#[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#[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
126impl<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 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 #[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 #[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 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 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 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 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
545pub(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