1use std::collections::VecDeque;
4use std::sync::Arc;
5
6use arrow_array::RecordBatch;
7use arrow_schema::SchemaRef;
8use datafusion::physical_expr::PhysicalExpr;
9use futures::FutureExt;
10use laminar_core::checkpoint::{OutputPartitionId, PartitionSequence, StreamGeneration};
11
12#[cfg(feature = "cluster")]
13use super::cluster::{ClusterReaderFrame, ClusterReaderRead, ClusterSubscriptionReader};
14use super::registry::{ChargedUpdate, MvUpdate, SubscriptionRead, SubscriptionReader};
15
16#[derive(Debug)]
17enum PortalReader {
18 Local(SubscriptionReader),
19 #[cfg(feature = "cluster")]
20 Cluster(ClusterSubscriptionReader),
21}
22
23impl PortalReader {
24 async fn next(&mut self) -> PortalRead {
25 match self {
26 Self::Local(reader) => PortalRead::Local(reader.next().await),
27 #[cfg(feature = "cluster")]
28 Self::Cluster(reader) => PortalRead::Cluster(reader.next().await),
29 }
30 }
31
32 fn try_next(&mut self) -> Option<PortalRead> {
33 match self {
34 Self::Local(reader) => reader.next().now_or_never().map(PortalRead::Local),
35 #[cfg(feature = "cluster")]
36 Self::Cluster(reader) => reader.try_next().map(PortalRead::Cluster),
37 }
38 }
39}
40
41enum PortalRead {
42 Local(SubscriptionRead),
43 #[cfg(feature = "cluster")]
44 Cluster(ClusterReaderRead),
45}
46
47#[doc(hidden)]
49#[derive(Clone)]
50pub struct SubscriptionFrameLease {
51 _local_owner: Option<ChargedUpdate>,
52 #[cfg(feature = "cluster")]
53 _cluster_owner: Option<Arc<tokio::sync::OwnedSemaphorePermit>>,
54}
55
56impl std::fmt::Debug for SubscriptionFrameLease {
57 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 formatter.write_str("SubscriptionFrameLease")
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Eq)]
64pub enum ClusterSubscriptionFrameMetadata {
65 Data {
67 stream_generation: StreamGeneration,
69 partition: OutputPartitionId,
71 partition_sequence: PartitionSequence,
73 committed_epoch: u64,
75 },
76 Progress {
78 stream_generation: StreamGeneration,
80 epoch: u64,
82 checkpoint_id: u64,
84 },
85 ViewRows {
87 epoch: u64,
89 snapshot: bool,
92 },
93 ViewProgress {
95 epoch: u64,
97 checkpoint_id: u64,
99 },
100}
101
102#[derive(Debug, Clone)]
104pub struct SubscriptionEnvelope {
105 pub frame: PortalFrame,
107 pub cluster: Option<ClusterSubscriptionFrameMetadata>,
109 pub error_code: Option<&'static str>,
111}
112
113impl SubscriptionEnvelope {
114 fn local(frame: PortalFrame) -> Self {
115 Self {
116 frame,
117 cluster: None,
118 error_code: None,
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
125pub enum PortalFrame {
126 Batch {
129 batch: RecordBatch,
131 sequence: u64,
135 #[doc(hidden)]
137 lease: SubscriptionFrameLease,
138 },
139 Barrier {
141 sequence: u64,
143 epoch: u64,
145 checkpoint_id: u64,
147 through_sequence: u64,
150 },
151 Lagged(u64),
154 Error {
156 message: String,
158 },
159}
160
161#[derive(Debug)]
163pub struct SubscriptionPortal {
164 name: String,
165 schema: SchemaRef,
166 snapshot: VecDeque<RecordBatch>,
168 reader: Option<PortalReader>,
169 closed: bool,
170 filter: Option<Arc<dyn PhysicalExpr>>,
171}
172
173impl SubscriptionPortal {
174 pub(crate) fn open(
175 name: impl Into<String>,
176 schema: SchemaRef,
177 reader: SubscriptionReader,
178 ) -> Self {
179 Self::new_inner(name, schema, reader, None)
180 }
181
182 pub(crate) fn open_with_filter(
183 name: impl Into<String>,
184 schema: SchemaRef,
185 reader: SubscriptionReader,
186 filter: Arc<dyn PhysicalExpr>,
187 ) -> Self {
188 Self::new_inner(name, schema, reader, Some(filter))
189 }
190
191 pub(crate) fn open_view(
194 name: impl Into<String>,
195 schema: SchemaRef,
196 snapshot: RecordBatch,
197 reader: SubscriptionReader,
198 filter: Option<Arc<dyn PhysicalExpr>>,
199 ) -> Self {
200 let mut portal = Self::new_inner(name, schema, reader, filter);
201 if snapshot.num_rows() > 0 {
202 portal.snapshot.push_back(snapshot);
203 }
204 portal
205 }
206
207 fn new_inner(
208 name: impl Into<String>,
209 schema: SchemaRef,
210 reader: SubscriptionReader,
211 filter: Option<Arc<dyn PhysicalExpr>>,
212 ) -> Self {
213 Self {
214 name: name.into(),
215 schema,
216 snapshot: VecDeque::new(),
217 reader: Some(PortalReader::Local(reader)),
218 closed: false,
219 filter,
220 }
221 }
222
223 #[cfg(feature = "cluster")]
224 pub(crate) fn open_cluster(
225 name: impl Into<String>,
226 schema: SchemaRef,
227 reader: ClusterSubscriptionReader,
228 filter: Option<Arc<dyn PhysicalExpr>>,
229 ) -> Self {
230 Self {
231 name: name.into(),
232 schema,
233 snapshot: VecDeque::new(),
234 reader: Some(PortalReader::Cluster(reader)),
235 closed: false,
236 filter,
237 }
238 }
239
240 #[must_use]
242 pub fn schema(&self) -> SchemaRef {
243 Arc::clone(&self.schema)
244 }
245
246 pub async fn next_frame(&mut self) -> Option<PortalFrame> {
248 self.next_envelope().await.map(|envelope| envelope.frame)
249 }
250
251 pub async fn next_envelope(&mut self) -> Option<SubscriptionEnvelope> {
253 if self.closed {
254 return None;
255 }
256 if let Some(envelope) = self.next_snapshot_envelope() {
257 return Some(envelope);
258 }
259
260 loop {
261 let read = self.reader.as_mut()?.next().await;
262 if let Some(frame) = self.process_read(read) {
263 return Some(frame);
264 }
265 }
266 }
267
268 pub fn try_next_frame(&mut self) -> Option<PortalFrame> {
270 self.try_next_envelope().map(|envelope| envelope.frame)
271 }
272
273 pub fn try_next_envelope(&mut self) -> Option<SubscriptionEnvelope> {
275 if self.closed {
276 return None;
277 }
278 if let Some(envelope) = self.next_snapshot_envelope() {
279 return Some(envelope);
280 }
281
282 loop {
283 let read = self.reader.as_mut()?.try_next()?;
284 if let Some(frame) = self.process_read(read) {
285 return Some(frame);
286 }
287 }
288 }
289
290 fn next_snapshot_envelope(&mut self) -> Option<SubscriptionEnvelope> {
291 while let Some(batch) = self.snapshot.pop_front() {
292 let Some(PortalReader::Local(reader)) = &self.reader else {
293 return None;
294 };
295 let frame = PortalFrame::Batch {
296 batch,
297 sequence: reader.cursor(),
298 lease: SubscriptionFrameLease {
299 _local_owner: None,
300 #[cfg(feature = "cluster")]
301 _cluster_owner: None,
302 },
303 };
304 if let Some(envelope) = self.process_envelope(SubscriptionEnvelope::local(frame)) {
305 return Some(envelope);
306 }
307 }
308 None
309 }
310
311 fn process_read(&mut self, read: PortalRead) -> Option<SubscriptionEnvelope> {
312 match read {
313 PortalRead::Local(read) => self.process_local_read(read),
314 #[cfg(feature = "cluster")]
315 PortalRead::Cluster(read) => self.process_cluster_read(read),
316 }
317 }
318
319 fn process_local_read(&mut self, read: SubscriptionRead) -> Option<SubscriptionEnvelope> {
320 let frame = match read {
321 SubscriptionRead::Update { sequence, update } => translate(sequence, update),
322 SubscriptionRead::Lagged(skipped) => {
323 tracing::warn!(
324 subscription = %self.name,
325 skipped,
326 "subscription cursor was evicted; closing"
327 );
328 self.close();
329 return Some(SubscriptionEnvelope::local(PortalFrame::Lagged(skipped)));
330 }
331 SubscriptionRead::Terminal(message) => {
332 tracing::warn!(
333 subscription = %self.name,
334 %message,
335 "subscription log terminated; closing"
336 );
337 self.close();
338 return Some(SubscriptionEnvelope::local(PortalFrame::Error { message }));
339 }
340 };
341
342 self.process_envelope(SubscriptionEnvelope::local(frame))
343 }
344
345 #[cfg(feature = "cluster")]
346 fn process_cluster_read(&mut self, read: ClusterReaderRead) -> Option<SubscriptionEnvelope> {
347 let envelope = match read {
348 ClusterReaderRead::Frame(ClusterReaderFrame::Batch {
349 batch,
350 delivery_sequence,
351 stream_generation,
352 partition,
353 partition_sequence,
354 committed_epoch,
355 permit,
356 }) => SubscriptionEnvelope {
357 frame: PortalFrame::Batch {
358 batch,
359 sequence: delivery_sequence,
360 lease: SubscriptionFrameLease {
361 _local_owner: None,
362 _cluster_owner: Some(permit),
363 },
364 },
365 cluster: Some(ClusterSubscriptionFrameMetadata::Data {
366 stream_generation,
367 partition,
368 partition_sequence,
369 committed_epoch,
370 }),
371 error_code: None,
372 },
373 ClusterReaderRead::Frame(ClusterReaderFrame::Progress {
374 delivery_sequence,
375 through_sequence,
376 stream_generation,
377 epoch,
378 checkpoint_id,
379 }) => SubscriptionEnvelope {
380 frame: PortalFrame::Barrier {
381 sequence: delivery_sequence,
382 epoch,
383 checkpoint_id,
384 through_sequence,
385 },
386 cluster: Some(ClusterSubscriptionFrameMetadata::Progress {
387 stream_generation,
388 epoch,
389 checkpoint_id,
390 }),
391 error_code: None,
392 },
393 ClusterReaderRead::Frame(ClusterReaderFrame::ViewRows {
394 batch,
395 delivery_sequence,
396 epoch,
397 snapshot,
398 permit,
399 }) => SubscriptionEnvelope {
400 frame: PortalFrame::Batch {
401 batch,
402 sequence: delivery_sequence,
403 lease: SubscriptionFrameLease {
404 _local_owner: None,
405 _cluster_owner: Some(permit),
406 },
407 },
408 cluster: Some(ClusterSubscriptionFrameMetadata::ViewRows { epoch, snapshot }),
409 error_code: None,
410 },
411 ClusterReaderRead::Frame(ClusterReaderFrame::ViewProgress {
412 delivery_sequence,
413 epoch,
414 checkpoint_id,
415 }) => SubscriptionEnvelope {
416 frame: PortalFrame::Barrier {
417 sequence: delivery_sequence,
418 epoch,
419 checkpoint_id,
420 through_sequence: delivery_sequence,
421 },
422 cluster: Some(ClusterSubscriptionFrameMetadata::ViewProgress {
423 epoch,
424 checkpoint_id,
425 }),
426 error_code: None,
427 },
428 ClusterReaderRead::Terminal(error) => {
429 let code = error.code();
430 tracing::warn!(
431 subscription = %self.name,
432 code,
433 error = %error,
434 "committed cluster subscription terminated"
435 );
436 self.close();
437 return Some(SubscriptionEnvelope {
438 frame: PortalFrame::Error {
439 message: format!("[{code}] {error}"),
440 },
441 cluster: None,
442 error_code: Some(code),
443 });
444 }
445 };
446 self.process_envelope(envelope)
447 }
448
449 fn process_envelope(&mut self, envelope: SubscriptionEnvelope) -> Option<SubscriptionEnvelope> {
450 let SubscriptionEnvelope {
451 frame,
452 cluster,
453 error_code,
454 } = envelope;
455
456 let PortalFrame::Batch {
457 batch,
458 sequence,
459 lease,
460 } = frame
461 else {
462 if matches!(&frame, PortalFrame::Error { .. }) {
463 self.close();
464 }
465 return Some(SubscriptionEnvelope {
466 frame,
467 cluster,
468 error_code,
469 });
470 };
471 let Some(filter) = self.filter.as_ref() else {
472 return Some(SubscriptionEnvelope {
473 frame: PortalFrame::Batch {
474 batch,
475 sequence,
476 lease,
477 },
478 cluster,
479 error_code,
480 });
481 };
482 match crate::filter_compile::apply(&batch, filter.as_ref()) {
483 Ok(Some(filtered)) => Some(SubscriptionEnvelope {
484 frame: PortalFrame::Batch {
485 batch: filtered,
486 sequence,
487 lease,
488 },
489 cluster,
490 error_code,
491 }),
492 Ok(None) => None,
493 Err(error) => {
494 tracing::warn!(
495 subscription = %self.name,
496 %error,
497 "subscription filter failed; closing"
498 );
499 let message = error.to_string();
500 self.close();
501 Some(SubscriptionEnvelope {
502 frame: PortalFrame::Error { message },
503 cluster: None,
504 error_code: None,
505 })
506 }
507 }
508 }
509
510 pub fn close(&mut self) {
512 self.closed = true;
513 self.snapshot.clear();
514 self.reader = None;
515 }
516
517 #[must_use]
519 pub fn is_closed(&self) -> bool {
520 self.closed
521 }
522}
523
524impl Drop for SubscriptionPortal {
525 fn drop(&mut self) {
526 self.close();
527 }
528}
529
530fn translate(sequence: u64, update: ChargedUpdate) -> PortalFrame {
531 match update.as_ref() {
532 MvUpdate::Batch(batch) => {
533 let batch = batch.clone();
534 PortalFrame::Batch {
535 batch,
536 sequence,
537 lease: SubscriptionFrameLease {
538 _local_owner: Some(update),
539 #[cfg(feature = "cluster")]
540 _cluster_owner: None,
541 },
542 }
543 }
544 MvUpdate::Barrier {
545 epoch,
546 checkpoint_id,
547 through_sequence,
548 } => PortalFrame::Barrier {
549 sequence,
550 epoch: *epoch,
551 checkpoint_id: *checkpoint_id,
552 through_sequence: *through_sequence,
553 },
554 MvUpdate::Error(message) => PortalFrame::Error {
555 message: message.clone(),
556 },
557 }
558}
559
560#[cfg(test)]
561mod tests;