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 &self.schema_settings,
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(
274 PublicationRow {
275 payload,
276 published_unix_micros: None,
277 context: None,
278 },
279 missed,
280 )
281 }
282
283 pub fn publish_row(&self, row: PublicationRow, missed: u64) -> Result<(), StreamPublicationError> {
284 self.admit(row, None, missed).map(|_| ())
285 }
286
287 pub fn admit_row(
288 &self, row: PublicationRow, wire_sequence: u64, missed: u64,
289 ) -> Result<super::ManagedPublication, StreamPublicationError> {
290 self.admit(row, Some(wire_sequence), missed)
291 }
292
293 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
294 pub fn stage_row(
295 &self, row: PublicationRow, wire_sequence: u64, missed: u64,
296 ) -> Result<super::StagedPublication, StreamPublicationError> {
297 self.stage_sequence(vec![row], Some(wire_sequence), missed, None).single()
298 }
299
300 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
303 pub fn stage_rows(&self, rows: Vec<PublicationRow>, first_wire_sequence: u64, missed: u64) -> super::StagedRows {
304 self.stage_sequence(rows, Some(first_wire_sequence), missed, None)
305 }
306
307 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
308 pub fn stage_derived_row(&self, row: PublicationRow, missed: u64) -> Result<super::StagedPublication, StreamPublicationError> {
309 self.stage_sequence(vec![row], None, missed, None).single()
310 }
311
312 #[cfg(feature = "plugin-sidecar")]
313 pub(crate) fn stage_rows_in(
314 &self, context: &crate::operations::Context<'_>, rows: Vec<PublicationRow>, first_wire_sequence: u64, missed: u64,
315 ) -> super::StagedRows {
316 self.stage_sequence(rows, Some(first_wire_sequence), missed, Some(context))
317 }
318
319 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
322 fn stage_sequence(
323 &self, rows: Vec<PublicationRow>, wire_sequence: Option<u64>, missed: u64, context: Option<&crate::operations::Context<'_>>,
324 ) -> super::StagedRows {
325 let Some(staged) = self.stream.wired.staged.as_ref() else {
326 return super::StagedRows::refused(Vec::new(), StreamPublicationError::NotReady);
327 };
328 let _visibility = self.stream.wired.window.visibility.write().expect("stream visibility poisoned");
329 let mut evicted = Vec::new();
330 let prepared = staged.scope.prepare_publication_in(context, |ready| {
331 let activation = staged.activation.upgrade().ok_or(StreamPublicationError::Revoked)?;
332 let mut receipts = Vec::with_capacity(rows.len());
333 let mut stamped = None;
334 let mut room = if ready {
336 usize::MAX
337 } else {
338 self.stream.wired.window.room(self.stream.ingest.taken()).saturating_sub(activation.pending(&self.stream.wired))
339 };
340 for (offset, row) in (0_u64..).zip(rows) {
341 if room == 0 {
342 return Ok((receipts, ready, Some(StreamPublicationError::Backlog)));
343 }
344 room -= 1;
345 let sequence = match wire_sequence.map(|first| first.checked_add(offset)) {
346 Some(None) => {
347 return Ok((receipts, ready, Some(StreamPublicationError::Sequence)));
348 }
349 sequence => sequence.flatten(),
350 };
351 if let Some(stamp) = row.published_unix_micros {
353 if stamped.is_some_and(|previous| stamp < previous) {
354 return Ok((receipts, ready, Some(StreamPublicationError::Stamp)));
355 }
356 stamped = Some(stamp);
357 }
358 let missed = if offset == 0 { missed } else { 0 };
359 let (publication, dropped) = match self.admit_locked(row, sequence, missed, ready) {
360 Ok(admitted) => admitted,
361 Err(error) => return Ok((receipts, ready, Some(error))),
362 };
363 evicted.extend(dropped);
364 receipts.push(if ready {
365 let (_, committed) = watch::channel(true);
366 super::StagedPublication { publication, committed }
367 } else {
368 activation.push(&self.stream.wired, *self.applied.borrow(), publication)
369 });
370 }
371 Ok((receipts, ready, None))
372 });
373 drop(_visibility);
374 drop(evicted);
375 let (receipts, ready, refused) = match prepared {
376 Ok(prepared) => prepared,
377 Err(error) => return super::StagedRows::refused(Vec::new(), error),
378 };
379 if ready && !receipts.is_empty() {
380 self.stream.wired.window.knock();
381 self.stream.wired.window.wake_delivery();
382 }
383 match refused {
384 Some(error) => super::StagedRows::refused(receipts, error),
385 None => super::StagedRows::staged(receipts),
386 }
387 }
388
389 fn admit(&self, row: PublicationRow, sequence: Option<u64>, missed: u64) -> Result<super::ManagedPublication, StreamPublicationError> {
390 let _visibility = self.stream.wired.window.visibility.write().expect("stream visibility poisoned");
391 let result = {
392 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
393 if let Some(staged) = &self.stream.wired.staged {
394 staged.scope.prepare_publication(|ready| {
395 if !ready {
396 return Err(StreamPublicationError::NotReady);
397 }
398 self.admit_locked(row, sequence, missed, true)
399 })
400 } else {
401 self.admit_locked(row, sequence, missed, true)
402 }
403 #[cfg(not(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar")))]
404 {
405 self.admit_locked(row, sequence, missed, true)
406 }
407 };
408 drop(_visibility);
409 let (publication, evicted) = result?;
410 drop(evicted);
411 self.stream.wired.window.knock();
412 self.stream.wired.window.wake_delivery();
413 Ok(publication)
414 }
415
416 fn admit_locked(
418 &self, row: PublicationRow, sequence: Option<u64>, missed: u64, live: bool,
419 ) -> Result<(super::ManagedPublication, Option<super::window::Row>), StreamPublicationError> {
420 let active = self.active.lock().expect("stream lease poisoned");
421 if !*active || self.stream.closed.load(Ordering::Acquire) {
422 return Err(StreamPublicationError::Revoked);
423 }
424 if row.payload.len() > MAX_PUBLICATION_BYTES {
425 return Err(StreamPublicationError::Oversized);
426 }
427 let next = self
428 .sequence
429 .load(Ordering::Relaxed)
430 .checked_add(missed)
431 .and_then(|sequence| sequence.checked_add(1))
432 .ok_or(StreamPublicationError::Sequence)?;
433 if sequence.is_some_and(|sequence| sequence != next) {
434 return Err(StreamPublicationError::Sequence);
435 }
436 self.stream.wired.validation.as_ref().expect("compiled plugin schema").validate(&row.payload)?;
437 let generation = {
438 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
439 {
440 self.stream.wired.staged.as_ref().map(|staged| staged.scope.generation().get())
441 }
442 #[cfg(not(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar")))]
443 {
444 None
445 }
446 };
447 if live && !self.stream.wired.window.can_advance(missed.checked_add(1).ok_or(StreamPublicationError::Sequence)?) {
448 return Err(StreamPublicationError::Sequence);
449 }
450 let taken = self.stream.ingest.taken();
451 if live && self.stream.wired.window.evicts_unread(taken) {
452 return Err(StreamPublicationError::Backlog);
453 }
454 let publication = self.stream.pool.retain(
455 &self.stream.wired.definition,
456 generation,
457 row,
458 next,
459 missed,
460 live.then_some((self.stream.wired.window.as_ref(), taken)),
461 )?;
462 let evicted = if live {
463 self.stream.wired.window.append_locked(*self.applied.borrow(), Arc::new(publication.clone()), missed)?
464 } else {
465 None
466 };
467 self.sequence.store(next, Ordering::Relaxed);
468 Ok((publication, evicted))
469 }
470
471 pub fn revoke(&self) {
472 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
473 if let Some(staged) = &self.stream.wired.staged {
474 staged.scope.unbind_streams();
475 }
476 *self.active.lock().expect("stream lease poisoned") = false;
477 self.stream.ingest.wake();
478 }
479
480 pub async fn ingested(&self) -> Result<(), StreamPublicationError> {
483 let mut taken = self.stream.ingest.watch();
484 loop {
485 if !*self.active.lock().expect("stream lease poisoned") || self.stream.closed.load(Ordering::Acquire) {
486 return Err(StreamPublicationError::Revoked);
487 }
488 let published = self.stream.wired.window.latest_locked();
489 if taken.borrow_and_update().values().all(|cursor| *cursor >= published) {
490 return Ok(());
491 }
492 taken.changed().await.map_err(|_| StreamPublicationError::Revoked)?;
493 }
494 }
495}
496
497impl Drop for PublicationLease {
498 fn drop(&mut self) {
499 self.revoke();
500 }
501}
502
503impl PluginStream {
504 pub fn definition(&self) -> &Definition {
505 &self.wired.definition
506 }
507
508 pub fn query(&self, range: impl RangeBounds<Position>) -> Query<PublicationRow> {
509 Query {
510 window: Arc::clone(&self.wired.window),
511 definition: self.wired.definition.clone(),
512 tier: self.tier.clone(),
513 start: range.start_bound().cloned(),
514 end: range.end_bound().cloned(),
515 as_of: None,
516 partition: None,
517 retained_only: false,
518 marker: PhantomData,
519 }
520 }
521
522 pub fn watch(&self) -> impl futures_core::Stream<Item = Item<PublicationRow>> + Send + Unpin + use<> {
523 let window = Arc::clone(&self.wired.window);
524 let holding = Holding::engage(Arc::clone(&window));
525 window::following(window, Arc::clone(&self.closed), holding)
526 }
527
528 pub fn watch_managed(&self) -> impl futures_core::Stream<Item = Item<super::ManagedPublication>> + Send + Unpin + use<> {
529 let window = Arc::clone(&self.wired.window);
530 let holding = Holding::engage(Arc::clone(&window));
531 window::following(window, Arc::clone(&self.closed), holding)
532 }
533
534 pub fn demand(&self) -> Demand {
535 Demand::of(&self.wired.window)
536 }
537}
538
539impl Wired {
540 fn is_staged(&self) -> bool {
541 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
542 {
543 self.staged.is_some()
544 }
545 #[cfg(not(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar")))]
546 {
547 false
548 }
549 }
550}
551
552pub(super) fn publication_definitions(
555 owner: &str, declarations: impl IntoIterator<Item = StreamDeclaration>,
556) -> Result<Vec<Definition>, StreamAdmissionError> {
557 if !artifact_subset(owner) {
558 return Err(StreamAdmissionError::Owner(owner.into()));
559 }
560 let mut ids = BTreeSet::new();
561 declarations
562 .into_iter()
563 .map(|declaration| {
564 publications::validate(&declaration)?;
565 if declaration.window > MAX_WINDOW_ROWS {
566 return Err(StreamAdmissionError::WindowLimit);
567 }
568 if !ids.insert(declaration.id.clone()) {
569 return Err(publications::Error::DuplicateStream { id: declaration.id }.into());
570 }
571 let definition = Definition::declared::<PublicationRow>(declaration.id, declaration.schema, declaration.window);
572 let definition = definition.presentation(declaration.metadata, declaration.row_metadata).publication(owner, declaration.row);
573 Ok(definition)
574 })
575 .collect()
576}
577
578pub(super) fn validate_existing(
579 windows: &Windows, definitions: &[Definition], require_unoccupied: bool,
580) -> Result<(), StreamAdmissionError> {
581 for definition in definitions {
582 let Some(revisions) = windows.get(definition.id()) else {
583 continue;
584 };
585 if revisions.values().any(|wired| wired.definition.owner().is_none()) {
586 return Err(StreamAdmissionError::Native(definition.id().into()));
587 }
588 if revisions.values().any(|wired| wired.definition.owner() != definition.owner()) {
589 return Err(StreamAdmissionError::Changed(definition.id().into()));
590 }
591 let Some(wired) = revisions.get(&definition.version()) else {
592 continue;
593 };
594 if wired.definition.owner() != definition.owner()
595 || wired.definition.schema() != definition.schema()
596 || wired.definition.window() != definition.window()
597 {
598 return Err(StreamAdmissionError::Changed(definition.id().into()));
599 }
600 #[cfg(any(feature = "plugin", feature = "plugin-guest", feature = "plugin-sidecar"))]
601 if require_unoccupied && wired.staged.as_ref().is_some_and(|staged| !staged.scope.has_committed()) {
602 return Err(StreamAdmissionError::Occupied(definition.id().into()));
603 }
604 if require_unoccupied
605 && wired
606 .publisher
607 .lock()
608 .expect("stream publisher poisoned")
609 .upgrade()
610 .is_some_and(|lease| *lease.lock().expect("stream lease poisoned"))
611 {
612 return Err(StreamAdmissionError::Occupied(definition.id().into()));
613 }
614 }
615 Ok(())
616}
617
618