1#![allow(refining_impl_trait)]
5
6use std::collections::HashMap;
7use std::pin::Pin;
8use std::sync::atomic::{AtomicBool, Ordering};
9use std::sync::Arc;
10
11use buffa::Message;
12use bytes::Bytes;
13use connectrpc::{
14 Chain, ConnectError, ConnectRpcService, Limits, PreEncoded, RequestContext as Context,
15 ServiceRequest,
16};
17use datafusion::arrow::record_batch::RecordBatch;
18use datafusion::execution::context::TaskContext;
19use datafusion::execution::runtime_env::RuntimeEnv;
20use exoware_proto::common::Entry;
21use exoware_proto::google::rpc::{ErrorInfo, RetryInfo};
22use exoware_proto::ingest::{
23 PutResponse as ProtoPutResponse, Service as IngestApi, ServiceServer as IngestServiceServer,
24};
25#[cfg(test)]
26use exoware_proto::log::retention::v1::SetRetentionRequestView;
27use exoware_proto::log::retention::v1::{
28 Service as RetentionApi, ServiceServer as RetentionServiceServer, SetRetentionResponse,
29};
30#[cfg(test)]
31use exoware_proto::log::stream::v1::GetRequestView;
32use exoware_proto::log::stream::v1::{
33 GetResponse as StreamGetResponse, Service as StreamApi, ServiceServer as StreamServiceServer,
34 SubscribeRequestView, SubscribeResponse,
35};
36use exoware_proto::prune::{
37 PruneResponse, Service as PruneApi, ServiceServer as PruneServiceServer,
38};
39use exoware_proto::query::{
40 Detail, GetManyEntry, GetManyFrame, GetResponse, RangeFrame, ReduceResponse,
41 Service as QueryApi, ServiceServer as QueryServiceServer,
42};
43use exoware_proto::stream_filter::{Filter, StreamFilter};
44use exoware_proto::{
45 connect_compression_registry, parse_range_traversal_direction,
46 to_domain_reduce_request_from_view, with_error_info_detail, with_query_detail,
47 with_retry_info_detail, RangeTraversalDirection,
48};
49use exoware_sdk as exoware_proto;
50use exoware_sdk::common::kv::v1::filter::KindView as ProtoFilterKindView;
51use exoware_sdk::keys::Key;
52use exoware_sdk::selector::Selector;
53use futures::{stream as stream_util, Stream, StreamExt};
54use tokio::sync::Notify;
55
56use crate::reduce::{decode_group, execute_reduce, RangeError, ReduceExecution, REDUCE_BATCH_ROWS};
57use crate::stream::{StreamHub, StreamNotifier};
58use crate::validate::{self, IngestLimits};
59use crate::{
60 FilteredBatch, Ingest, IngestError, Log, Prune, Query, QueryExtra, RangeScan, Retention,
61 StoreEngine,
62};
63
64const MAX_CONNECTRPC_BODY_BYTES: usize = 256 * 1024 * 1024;
66const RANGE_STREAM_MAX_FRAME_ROWS: usize = 4096;
67const REDUCE_FRAME_TARGET_BYTES: usize = 16 * 1024 * 1024;
68const SUBSCRIBE_GET_BATCH_LOOKAHEAD: usize = 8;
74
75fn query_detail(sequence_number: u64, extra: QueryExtra) -> Detail {
76 Detail {
77 sequence_number,
78 extra: extra.into_iter().collect(),
79 ..Default::default()
80 }
81}
82
83struct RangeStreamRequest {
84 start_key: Key,
85 end_key: Key,
86 limit: usize,
87 batch_size: usize,
88 forward: bool,
89 sequence_number: u64,
90}
91
92async fn range_stream<Q>(
93 query: Arc<Q>,
94 request: RangeStreamRequest,
95) -> Result<Pin<Box<dyn Stream<Item = Result<RangeFrame, ConnectError>> + Send>>, ConnectError>
96where
97 Q: Query,
98{
99 let RangeStreamRequest {
100 start_key,
101 end_key,
102 limit,
103 batch_size,
104 forward,
105 sequence_number,
106 } = request;
107 let entries = query
108 .range_scan(start_key, end_key, limit, forward)
109 .await
110 .map_err(ConnectError::internal)?;
111
112 Ok(Box::pin(stream_util::unfold(
113 Some((entries, false)),
114 move |state| async move {
115 let (mut entries, emitted_frame) = state?;
116 let batch = match entries.next_batch(batch_size).await {
117 Ok(batch) => batch,
118 Err(e) => return Some((Err(ConnectError::internal(e)), None)),
119 };
120 let detail = query_detail(sequence_number, batch.extra);
121 if batch.rows.is_empty() {
122 if emitted_frame && detail.extra.is_empty() {
123 return None;
124 }
125 return Some((
126 Ok(RangeFrame {
127 detail: Some(detail).into(),
128 ..Default::default()
129 }),
130 None,
131 ));
132 }
133
134 let mut chunk = Vec::with_capacity(batch.rows.len());
135 for (key, value) in batch.rows {
136 chunk.push(Entry {
137 key: key.into(),
138 value,
139 ..Default::default()
140 });
141 }
142 Some((
143 Ok(RangeFrame {
144 results: chunk,
145 detail: Some(detail).into(),
146 ..Default::default()
147 }),
148 Some((entries, true)),
149 ))
150 },
151 )))
152}
153
154pub struct AppState<E> {
157 pub engine: Arc<E>,
159 pub ingest_limits: IngestLimits,
161 pub ready: Arc<AtomicBool>,
164 pub stream: Arc<StreamHub>,
166}
167
168impl<E> Clone for AppState<E> {
169 fn clone(&self) -> Self {
170 Self {
171 engine: self.engine.clone(),
172 ingest_limits: self.ingest_limits,
173 ready: self.ready.clone(),
174 stream: self.stream.clone(),
175 }
176 }
177}
178
179impl<E> AppState<E>
180where
181 E: StoreEngine,
182{
183 pub fn new(engine: Arc<E>) -> Self {
184 let current_sequence = engine.current_sequence();
185 Self {
186 engine,
187 ingest_limits: IngestLimits::default(),
188 ready: Arc::new(AtomicBool::new(true)),
189 stream: Arc::new(StreamHub::new(current_sequence)),
190 }
191 }
192
193 pub fn with_ingest_limits(mut self, limits: IngestLimits) -> Self {
194 self.ingest_limits = limits;
195 self
196 }
197}
198
199pub struct IngestState<I> {
201 pub ingest: Arc<I>,
203 pub limits: IngestLimits,
205 pub ready: Arc<AtomicBool>,
207 pub notifier: Option<Arc<dyn StreamNotifier>>,
209}
210
211impl<I> Clone for IngestState<I> {
212 fn clone(&self) -> Self {
213 Self {
214 ingest: self.ingest.clone(),
215 limits: self.limits,
216 ready: self.ready.clone(),
217 notifier: self.notifier.clone(),
218 }
219 }
220}
221
222impl<I> IngestState<I>
223where
224 I: Ingest,
225{
226 pub fn new(ingest: Arc<I>) -> Self {
227 Self {
228 ingest,
229 limits: IngestLimits::default(),
230 ready: Arc::new(AtomicBool::new(true)),
231 notifier: None,
232 }
233 }
234
235 pub fn with_notifier(ingest: Arc<I>, notifier: Arc<dyn StreamNotifier>) -> Self {
236 Self {
237 ingest,
238 limits: IngestLimits::default(),
239 ready: Arc::new(AtomicBool::new(true)),
240 notifier: Some(notifier),
241 }
242 }
243
244 pub fn with_limits(mut self, limits: IngestLimits) -> Self {
245 self.limits = limits;
246 self
247 }
248}
249
250impl<E> From<AppState<E>> for IngestState<E> {
251 fn from(state: AppState<E>) -> Self {
252 Self {
253 ingest: state.engine,
254 limits: state.ingest_limits,
255 ready: state.ready,
256 notifier: Some(state.stream),
257 }
258 }
259}
260
261pub struct QueryState<Q> {
263 pub query: Arc<Q>,
265 context: Arc<TaskContext>,
266}
267
268impl<Q> Clone for QueryState<Q> {
269 fn clone(&self) -> Self {
270 Self {
271 query: self.query.clone(),
272 context: self.context.clone(),
273 }
274 }
275}
276
277impl<Q> QueryState<Q>
278where
279 Q: Query,
280{
281 pub fn new(query: Arc<Q>) -> Self {
287 Self {
288 query,
289 context: Arc::default(),
290 }
291 }
292
293 pub fn with_runtime(mut self, runtime: Arc<RuntimeEnv>) -> Self {
295 self.context = Arc::new(TaskContext::default().with_runtime(runtime));
296 self
297 }
298}
299
300impl<E> From<AppState<E>> for QueryState<E> {
301 fn from(state: AppState<E>) -> Self {
302 Self {
303 query: state.engine,
304 context: Arc::default(),
305 }
306 }
307}
308
309pub struct PruneState<P> {
311 pub prune: Arc<P>,
313}
314
315impl<P> Clone for PruneState<P> {
316 fn clone(&self) -> Self {
317 Self {
318 prune: self.prune.clone(),
319 }
320 }
321}
322
323impl<P> PruneState<P>
324where
325 P: Prune,
326{
327 pub fn new(prune: Arc<P>) -> Self {
328 Self { prune }
329 }
330}
331
332impl<E> From<AppState<E>> for PruneState<E> {
333 fn from(state: AppState<E>) -> Self {
334 Self {
335 prune: state.engine,
336 }
337 }
338}
339
340pub struct RetentionState<R> {
342 pub retention: Arc<R>,
344}
345
346impl<R> Clone for RetentionState<R> {
347 fn clone(&self) -> Self {
348 Self {
349 retention: self.retention.clone(),
350 }
351 }
352}
353
354impl<R> RetentionState<R>
355where
356 R: Retention,
357{
358 pub fn new(retention: Arc<R>) -> Self {
359 Self { retention }
360 }
361}
362
363impl<E> From<AppState<E>> for RetentionState<E> {
364 fn from(state: AppState<E>) -> Self {
365 Self {
366 retention: state.engine,
367 }
368 }
369}
370
371pub struct StreamState<L> {
373 pub log: Arc<L>,
375 pub notifier: Arc<dyn StreamNotifier>,
377}
378
379impl<L> Clone for StreamState<L> {
380 fn clone(&self) -> Self {
381 Self {
382 log: self.log.clone(),
383 notifier: self.notifier.clone(),
384 }
385 }
386}
387
388impl<L> StreamState<L>
389where
390 L: Log,
391{
392 pub fn new(log: Arc<L>, notifier: Arc<dyn StreamNotifier>) -> Self {
393 Self { log, notifier }
394 }
395}
396
397impl<E> From<AppState<E>> for StreamState<E> {
398 fn from(state: AppState<E>) -> Self {
399 Self {
400 log: state.engine,
401 notifier: state.stream,
402 }
403 }
404}
405
406pub struct IngestConnect<I> {
407 state: IngestState<I>,
408}
409
410impl<I> Clone for IngestConnect<I> {
411 fn clone(&self) -> Self {
412 Self {
413 state: self.state.clone(),
414 }
415 }
416}
417
418impl<I> IngestConnect<I>
419where
420 I: Ingest,
421{
422 pub fn new(state: impl Into<IngestState<I>>) -> Self {
423 Self {
424 state: state.into(),
425 }
426 }
427}
428
429const RETRY_HINT_DELAY: std::time::Duration = std::time::Duration::from_secs(1);
431const REASON_WORKER_NOT_READY: &str = "WORKER_NOT_READY";
433const REASON_INGEST_UNAVAILABLE: &str = "INGEST_UNAVAILABLE";
435
436fn with_retry_hint(err: ConnectError, retry_delay: std::time::Duration) -> ConnectError {
438 with_retry_info_detail(
439 err,
440 RetryInfo {
441 retry_delay: Some(buffa_types::google::protobuf::Duration::from(retry_delay)).into(),
442 ..Default::default()
443 },
444 )
445}
446
447fn ingest_error_to_connect(err: IngestError) -> ConnectError {
448 match err {
449 IngestError::Unavailable { message } => with_retry_hint(
450 with_error_info_detail(
451 ConnectError::unavailable(message),
452 ErrorInfo {
453 reason: REASON_INGEST_UNAVAILABLE.to_string(),
454 domain: crate::validate::INGEST_ERROR_DOMAIN.to_string(),
455 ..Default::default()
456 },
457 ),
458 RETRY_HINT_DELAY,
459 ),
460 IngestError::Internal { message } => ConnectError::internal(message),
461 }
462}
463
464impl<I> IngestApi for IngestConnect<I>
465where
466 I: Ingest,
467{
468 async fn put(
469 &self,
470 _ctx: Context,
471 request: ServiceRequest<'_, exoware_proto::log::ingest::v1::PutRequest>,
472 ) -> connectrpc::ServiceResult<ProtoPutResponse> {
473 if !self.state.ready.load(Ordering::SeqCst) {
474 return Err(with_retry_hint(
475 with_error_info_detail(
476 ConnectError::unavailable("ingest is not ready"),
477 ErrorInfo {
478 reason: REASON_WORKER_NOT_READY.to_string(),
479 domain: crate::validate::INGEST_ERROR_DOMAIN.to_string(),
480 ..Default::default()
481 },
482 ),
483 RETRY_HINT_DELAY,
484 ));
485 }
486
487 validate::validate_put_request(request.view(), self.state.limits)?;
488
489 let wire = request.bytes();
490 let mut batch = Vec::with_capacity(request.kvs.len());
491 for kv in request.kvs.iter() {
492 let key: Key = wire.slice_ref(kv.key);
493 let value = wire.slice_ref(kv.value);
494 batch.push((key, value));
495 }
496
497 let seq = self
498 .state
499 .ingest
500 .put_batch(batch)
501 .await
502 .map_err(ingest_error_to_connect)?;
503
504 if let Some(notifier) = &self.state.notifier {
506 notifier.advance(seq);
507 }
508
509 connectrpc::Response::ok(ProtoPutResponse {
510 sequence_number: seq,
511 ..Default::default()
512 })
513 }
514}
515
516pub struct QueryConnect<Q> {
517 state: QueryState<Q>,
518}
519
520impl<Q> Clone for QueryConnect<Q> {
521 fn clone(&self) -> Self {
522 Self {
523 state: self.state.clone(),
524 }
525 }
526}
527
528impl<Q> QueryConnect<Q>
529where
530 Q: Query,
531{
532 pub fn new(state: impl Into<QueryState<Q>>) -> Self {
533 Self {
534 state: state.into(),
535 }
536 }
537
538 fn current_sequence_number(&self) -> u64 {
539 self.state.query.current_sequence()
540 }
541
542 fn error_detail(&self) -> Detail {
543 Detail {
544 sequence_number: self.current_sequence_number(),
545 ..Default::default()
546 }
547 }
548
549 fn consistency_not_ready_error(&self, required: u64, current: u64) -> ConnectError {
550 let err = with_retry_hint(
551 ConnectError::aborted("minimum consistency token is not yet visible"),
552 RETRY_HINT_DELAY,
553 );
554 with_query_detail(
555 with_error_info_detail(
556 err,
557 ErrorInfo {
558 reason: "CONSISTENCY_NOT_READY".to_string(),
559 domain: "store.query".to_string(),
560 metadata: [
561 ("required_sequence_number".to_string(), required.to_string()),
562 ("current_sequence_number".to_string(), current.to_string()),
563 ]
564 .into_iter()
565 .collect(),
566 ..Default::default()
567 },
568 ),
569 self.error_detail(),
570 )
571 }
572
573 fn ensure_min_sequence_number(&self, required: Option<u64>) -> Result<u64, ConnectError> {
574 let current = self.current_sequence_number();
575 if let Some(required) = required {
576 if current < required {
577 return Err(self.consistency_not_ready_error(required, current));
578 }
579 }
580 Ok(current)
581 }
582}
583
584impl<Q> QueryApi for QueryConnect<Q>
585where
586 Q: Query,
587{
588 async fn get(
589 &self,
590 _ctx: Context,
591 request: ServiceRequest<'_, exoware_proto::store::query::v1::GetRequest>,
592 ) -> connectrpc::ServiceResult<GetResponse> {
593 validate::validate_get_request(request.view())?;
594 let token = self.ensure_min_sequence_number(request.min_sequence_number)?;
595 let wire = request.bytes();
596 let key: Key = wire.slice_ref(request.key);
597 let (value, extra) = self
598 .state
599 .query
600 .get(key)
601 .await
602 .map_err(ConnectError::internal)?;
603 let detail = query_detail(token, extra);
604 connectrpc::Response::ok(GetResponse {
605 value,
606 detail: Some(detail).into(),
607 ..Default::default()
608 })
609 }
610
611 async fn get_many(
612 &self,
613 _ctx: Context,
614 request: ServiceRequest<'_, exoware_proto::store::query::v1::GetManyRequest>,
615 ) -> connectrpc::ServiceResult<connectrpc::ServiceStream<GetManyFrame>> {
616 validate::validate_get_many_request(request.view())?;
617 let sequence_number = self.ensure_min_sequence_number(request.min_sequence_number)?;
618
619 let wire = request.bytes();
620 let keys: Vec<Key> = request.keys.iter().map(|key| wire.slice_ref(key)).collect();
621 let (entries, extra) = self
622 .state
623 .query
624 .get_many(keys)
625 .await
626 .map_err(ConnectError::internal)?;
627 let detail = query_detail(sequence_number, extra);
628 let batch_size = (request.batch_size as usize).min(RANGE_STREAM_MAX_FRAME_ROWS);
629 let mut frames = Vec::new();
630 let mut chunk = Vec::new();
631 for (key, value) in entries {
632 chunk.push(GetManyEntry {
633 key: key.to_vec(),
634 value,
635 ..Default::default()
636 });
637 if chunk.len() >= batch_size {
638 frames.push(Ok(GetManyFrame {
639 results: std::mem::take(&mut chunk),
640 detail: Some(detail.clone()).into(),
641 ..Default::default()
642 }));
643 }
644 }
645 if !chunk.is_empty() {
646 frames.push(Ok(GetManyFrame {
647 results: chunk,
648 detail: Some(detail).into(),
649 ..Default::default()
650 }));
651 } else if frames.is_empty() {
652 frames.push(Ok(GetManyFrame {
653 detail: Some(detail).into(),
654 ..Default::default()
655 }));
656 }
657
658 Ok(connectrpc::Response::stream(stream_util::iter(frames)))
659 }
660
661 async fn range(
662 &self,
663 _ctx: Context,
664 request: ServiceRequest<'_, exoware_proto::store::query::v1::RangeRequest>,
665 ) -> connectrpc::ServiceResult<connectrpc::ServiceStream<RangeFrame>> {
666 validate::validate_range_request(request.view())?;
667 let sequence_number = self.ensure_min_sequence_number(request.min_sequence_number)?;
668 let wire = request.bytes();
669 let start_key: Key = wire.slice_ref(request.start);
670 let end_key: Key = wire.slice_ref(request.end);
671 let limit = request.limit.map(|v| v as usize).unwrap_or(usize::MAX);
672 let batch_size = (request.batch_size as usize).min(RANGE_STREAM_MAX_FRAME_ROWS);
673 let forward = match parse_range_traversal_direction(request.mode) {
674 Ok(RangeTraversalDirection::Forward) => true,
675 Ok(RangeTraversalDirection::Reverse) => false,
676 Err(e) => return Err(ConnectError::internal(format!("traversal mode: {e:?}"))),
677 };
678 Ok(connectrpc::Response::stream(
679 range_stream(
680 self.state.query.clone(),
681 RangeStreamRequest {
682 start_key,
683 end_key,
684 limit,
685 batch_size,
686 forward,
687 sequence_number,
688 },
689 )
690 .await?,
691 ))
692 }
693
694 async fn reduce(
695 &self,
696 _ctx: Context,
697 request: ServiceRequest<'_, exoware_proto::store::query::v1::ReduceRequest>,
698 ) -> connectrpc::ServiceResult<connectrpc::ServiceStream<ReduceResponse>> {
699 validate::validate_reduce_request(request.view())?;
700 let sequence_number = self.ensure_min_sequence_number(request.min_sequence_number)?;
701 let wire = request.bytes();
702 let start_key: Key = wire.slice_ref(request.start);
703 let end_key: Key = wire.slice_ref(request.end);
704 let domain = to_domain_reduce_request_from_view(&request.params)
705 .map_err(validate::reduce_params_error)?;
706 let execution = execute_reduce(
707 self.state.query.clone(),
708 start_key,
709 end_key,
710 domain,
711 self.state.context.clone(),
712 )
713 .map_err(|error| match error {
714 RangeError::Reduce(message) => validate::reduce_params_error(message),
715 error => reduce_error(error),
716 })?;
717 Ok(connectrpc::Response::stream(reduce_frames(
718 execution,
719 sequence_number,
720 )))
721 }
722}
723
724fn reduce_error(error: RangeError) -> ConnectError {
725 match error {
726 RangeError::Resources(message) => ConnectError::resource_exhausted(message),
727 RangeError::Backend(message) => ConnectError::internal(message),
728 RangeError::Reduce(message) => ConnectError::failed_precondition(message),
729 }
730}
731
732struct ReduceFrameState {
733 execution: ReduceExecution,
734 batch: Option<RecordBatch>,
735 row: usize,
736 emitted: bool,
737}
738
739fn reduce_frames(
740 execution: ReduceExecution,
741 sequence_number: u64,
742) -> impl Stream<Item = Result<ReduceResponse, ConnectError>> + Send {
743 stream_util::unfold(
744 Some(ReduceFrameState {
745 execution,
746 batch: None,
747 row: 0,
748 emitted: false,
749 }),
750 move |state| async move {
751 let mut state = state?;
752 let mut response = ReduceResponse::default();
753 let mut frame_bytes = 0usize;
754 let mut done = false;
755 loop {
756 if state
757 .batch
758 .as_ref()
759 .is_none_or(|batch| state.row == batch.num_rows())
760 {
761 match state.execution.batches.next().await {
762 Some(Ok(batch)) => {
763 state.batch = Some(batch);
764 state.row = 0;
765 }
766 Some(Err(error)) => return Some((Err(reduce_error(error.into())), None)),
767 None => {
768 done = true;
769 break;
770 }
771 }
772 }
773 let batch = state.batch.as_ref().unwrap();
774 if batch.num_rows() == 0 {
775 continue;
776 }
777 let group = match decode_group(
778 batch,
779 state.row,
780 state.execution.group_count,
781 &state.execution.result_kinds,
782 ) {
783 Ok(group) => group,
784 Err(error) => return Some((Err(reduce_error(error.into())), None)),
785 };
786 if state.execution.group_count == 0 {
787 response.results = group.results;
788 state.row += 1;
789 break;
790 }
791 let group_bytes = group.encoded_len() as usize + 16;
792 if !response.groups.is_empty()
793 && frame_bytes.saturating_add(group_bytes) > REDUCE_FRAME_TARGET_BYTES
794 {
795 break;
796 }
797 frame_bytes = frame_bytes.saturating_add(group_bytes);
798 response.groups.push(group);
799 state.row += 1;
800 if response.groups.len() == REDUCE_BATCH_ROWS {
801 break;
802 }
803 }
804 if response.results.is_empty() && response.groups.is_empty() && state.emitted && done {
805 return None;
806 }
807 response.detail = Some(query_detail(
808 sequence_number,
809 state.execution.extra.lock().unwrap().clone(),
810 ))
811 .into();
812 state.emitted = true;
813 Some((Ok(response), if done { None } else { Some(state) }))
814 },
815 )
816}
817
818pub struct PruneConnect<P> {
819 state: PruneState<P>,
820}
821
822impl<P> Clone for PruneConnect<P> {
823 fn clone(&self) -> Self {
824 Self {
825 state: self.state.clone(),
826 }
827 }
828}
829
830impl<P> PruneConnect<P>
831where
832 P: Prune,
833{
834 pub fn new(state: impl Into<PruneState<P>>) -> Self {
835 Self {
836 state: state.into(),
837 }
838 }
839}
840
841impl<P> PruneApi for PruneConnect<P>
842where
843 P: Prune,
844{
845 async fn prune(
846 &self,
847 _ctx: Context,
848 request: ServiceRequest<'_, exoware_proto::store::prune::v1::PruneRequest>,
849 ) -> connectrpc::ServiceResult<PruneResponse> {
850 validate::validate_prune_request(request.view())?;
851 let document = exoware_proto::parse_and_validate_policy_document(request.view())
852 .map_err(|e| ConnectError::invalid_argument(e.to_string()))?;
853
854 self.state
855 .prune
856 .apply_prune_policies(document)
857 .await
858 .map_err(ConnectError::internal)?;
859 connectrpc::Response::ok(PruneResponse::default())
860 }
861}
862
863pub struct StreamConnect<B> {
864 state: StreamState<B>,
865}
866
867impl<B> Clone for StreamConnect<B> {
868 fn clone(&self) -> Self {
869 Self {
870 state: self.state.clone(),
871 }
872 }
873}
874
875impl<B> StreamConnect<B>
876where
877 B: Log,
878{
879 pub fn new(state: impl Into<StreamState<B>>) -> Self {
880 Self {
881 state: state.into(),
882 }
883 }
884
885 fn batch_evicted_connect_error(oldest_retained: Option<u64>) -> ConnectError {
886 let mut metadata = HashMap::new();
887 if let Some(v) = oldest_retained {
888 metadata.insert(
889 crate::stream::METADATA_OLDEST_RETAINED.to_string(),
890 v.to_string(),
891 );
892 }
893 with_error_info_detail(
894 ConnectError::out_of_range("batch has been evicted from the log"),
895 ErrorInfo {
896 reason: crate::stream::REASON_BATCH_EVICTED.to_string(),
897 domain: crate::stream::STREAM_ERROR_DOMAIN.to_string(),
898 metadata: metadata.into_iter().collect(),
899 ..Default::default()
900 },
901 )
902 }
903
904 fn batch_evicted_error(&self, oldest_retained: Option<u64>) -> ConnectError {
905 Self::batch_evicted_connect_error(oldest_retained)
906 }
907
908 fn batch_not_found_error(&self) -> ConnectError {
909 with_error_info_detail(
910 ConnectError::not_found("batch not found"),
911 ErrorInfo {
912 reason: crate::stream::REASON_BATCH_NOT_FOUND.to_string(),
913 domain: crate::stream::STREAM_ERROR_DOMAIN.to_string(),
914 ..Default::default()
915 },
916 )
917 }
918}
919
920type OrderedBatchStream =
924 Pin<Box<dyn Stream<Item = (u64, Result<Option<FilteredBatch>, String>)> + Send>>;
925
926fn ordered_batch_stream<B, S>(
927 log: Arc<B>,
928 sequences: S,
929 mut first_batch: Option<FilteredBatch>,
930 matchers: Arc<crate::stream::CompiledMatchers>,
931) -> OrderedBatchStream
932where
933 B: Log,
934 S: Stream<Item = u64> + Send + 'static,
935{
936 Box::pin(
937 sequences
938 .map(move |sequence_number| {
939 let log = log.clone();
940 let first_batch = first_batch.take();
944 let matchers = matchers.clone();
945 async move {
946 let batch = match first_batch {
947 Some(batch) => Ok(Some(batch)),
948 None => {
949 log.get_batch_filtered(sequence_number, matchers.as_ref())
950 .await
951 }
952 };
953 (sequence_number, batch)
954 }
955 })
956 .buffered(SUBSCRIBE_GET_BATCH_LOOKAHEAD),
957 )
958}
959
960fn live_sequence_stream(
966 notifier: Arc<dyn StreamNotifier>,
967 notify: Arc<Notify>,
968 start: Option<u64>,
969) -> impl Stream<Item = u64> + Send {
970 stream_util::unfold(start, move |next| {
971 let notifier = notifier.clone();
972 let notify = notify.clone();
973 async move {
974 let sequence_number = next?;
975 while sequence_number > notifier.current_sequence() {
978 let notified = notify.clone().notified_owned();
979 if sequence_number <= notifier.current_sequence() {
980 break;
981 }
982 notified.await;
983 }
984 Some((sequence_number, sequence_number.checked_add(1)))
985 }
986 })
987}
988
989struct ReplayState {
990 batches: OrderedBatchStream,
991}
992
993enum ReplayProgress {
994 Frame(SubscribeResponse),
995 Advanced,
996 Done,
997}
998
999struct SubscriptionState<B> {
1000 state: StreamState<B>,
1001 replay: Option<ReplayState>,
1004 live: Option<OrderedBatchStream>,
1005 terminated: bool,
1006}
1007
1008impl<B> SubscriptionState<B>
1009where
1010 B: Log,
1011{
1012 fn new(state: StreamState<B>, replay: Option<ReplayState>, live: OrderedBatchStream) -> Self {
1013 Self {
1014 state,
1015 replay,
1016 live: Some(live),
1017 terminated: false,
1018 }
1019 }
1020
1021 fn into_stream(
1022 self,
1023 ) -> Pin<Box<dyn Stream<Item = Result<SubscribeResponse, ConnectError>> + Send>> {
1024 Box::pin(stream_util::unfold(self, |mut state| async move {
1025 loop {
1026 if state.terminated {
1027 return None;
1028 }
1029
1030 if state.replay.is_some() {
1031 match state.next_replay_frame().await {
1032 Ok(ReplayProgress::Frame(frame)) => return Some((Ok(frame), state)),
1033 Ok(ReplayProgress::Advanced) => continue,
1034 Ok(ReplayProgress::Done) => {}
1035 Err(err) => return Some((state.terminate(err), state)),
1036 }
1037 }
1038
1039 let (sequence_number, batch) = state.live.as_mut()?.next().await?;
1042 match state.resolve_batch(sequence_number, batch).await {
1043 Ok(Some(frame)) => return Some((Ok(frame), state)),
1044 Ok(None) => continue,
1045 Err(err) => return Some((state.terminate(err), state)),
1046 }
1047 }
1048 }))
1049 }
1050
1051 fn terminate(&mut self, err: ConnectError) -> Result<SubscribeResponse, ConnectError> {
1052 self.terminated = true;
1053 self.replay = None;
1054 self.live = None;
1055 Err(err)
1056 }
1057
1058 async fn next_replay_frame(&mut self) -> Result<ReplayProgress, ConnectError> {
1059 let Some(replay) = &mut self.replay else {
1060 return Ok(ReplayProgress::Done);
1061 };
1062 let Some((sequence_number, batch)) = replay.batches.next().await else {
1063 self.replay = None;
1064 return Ok(ReplayProgress::Done);
1065 };
1066 Ok(match self.resolve_batch(sequence_number, batch).await? {
1067 Some(frame) => ReplayProgress::Frame(frame),
1068 None => ReplayProgress::Advanced,
1069 })
1070 }
1071
1072 async fn resolve_batch(
1073 &mut self,
1074 sequence_number: u64,
1075 batch: Result<Option<FilteredBatch>, String>,
1076 ) -> Result<Option<SubscribeResponse>, ConnectError> {
1077 let batch = batch.map_err(ConnectError::internal)?;
1078 let Some(batch) = batch else {
1079 let oldest = self
1080 .state
1081 .log
1082 .oldest_retained_batch()
1083 .await
1084 .map_err(ConnectError::internal)?;
1085 return Err(StreamConnect::<B>::batch_evicted_connect_error(oldest));
1086 };
1087 let entries = batch.into_entries();
1088 if entries.is_empty() {
1089 return Ok(None);
1090 }
1091 Ok(Some(SubscribeResponse {
1092 sequence_number,
1093 entries,
1094 ..Default::default()
1095 }))
1096 }
1097}
1098
1099fn domain_filter_from_subscribe_view(
1100 req: &SubscribeRequestView<'_>,
1101) -> Result<StreamFilter, ConnectError> {
1102 let mut selectors = Vec::with_capacity(req.selectors.len());
1103 for mk in req.selectors.iter() {
1104 selectors.push(Selector {
1107 prefix: Bytes::copy_from_slice(mk.prefix),
1108 payload_regex: exoware_sdk::kv_codec::Utf8::from(mk.payload_regex),
1109 });
1110 }
1111 let mut value_filters = Vec::with_capacity(req.value_filters.len());
1112 for vf in req.value_filters.iter() {
1113 value_filters.push(match vf.kind {
1114 Some(ProtoFilterKindView::Exact(bytes)) => Filter::Exact(Bytes::copy_from_slice(bytes)),
1115 Some(ProtoFilterKindView::Prefix(bytes)) => {
1116 Filter::Prefix(Bytes::copy_from_slice(bytes))
1117 }
1118 Some(ProtoFilterKindView::Regex(pattern)) => Filter::Regex(pattern.to_string()),
1119 None => {
1120 return Err(ConnectError::invalid_argument(
1121 "each value_filter must set exactly one of exact, prefix, or regex",
1122 ))
1123 }
1124 });
1125 }
1126 Ok(StreamFilter {
1127 selectors,
1128 value_filters,
1129 })
1130}
1131
1132impl<B> StreamApi for StreamConnect<B>
1133where
1134 B: Log,
1135{
1136 async fn subscribe(
1137 &self,
1138 _ctx: Context,
1139 request: ServiceRequest<'_, exoware_proto::log::stream::v1::SubscribeRequest>,
1140 ) -> connectrpc::ServiceResult<connectrpc::ServiceStream<SubscribeResponse>> {
1141 let filter = domain_filter_from_subscribe_view(request.view())?;
1142 let since = request.since_sequence_number;
1143
1144 let matchers = Arc::new(crate::stream::compile_matchers(&filter)?);
1148 let subscription = self.state.notifier.subscribe();
1149 let replay_bound = subscription.current_sequence;
1150 let live_notify = subscription.notify;
1151
1152 let replay = match since {
1158 Some(s) if s <= replay_bound && s > 0 => {
1159 let first_batch = self
1160 .state
1161 .log
1162 .get_batch_filtered(s, matchers.as_ref())
1163 .await
1164 .map_err(ConnectError::internal)?;
1165 let Some(first_batch) = first_batch else {
1166 let oldest = self
1167 .state
1168 .log
1169 .oldest_retained_batch()
1170 .await
1171 .map_err(ConnectError::internal)?;
1172 return Err(self.batch_evicted_error(oldest));
1173 };
1174 Some(ReplayState {
1175 batches: ordered_batch_stream(
1176 self.state.log.clone(),
1177 stream_util::iter(s..=replay_bound),
1178 Some(first_batch),
1179 matchers.clone(),
1180 ),
1181 })
1182 }
1183 _ => None,
1184 };
1185 let live = ordered_batch_stream(
1186 self.state.log.clone(),
1187 live_sequence_stream(
1188 self.state.notifier.clone(),
1189 live_notify,
1190 replay_bound.checked_add(1),
1191 ),
1192 None,
1193 matchers,
1194 );
1195
1196 Ok(connectrpc::Response::stream(
1197 SubscriptionState::new(self.state.clone(), replay, live).into_stream(),
1198 ))
1199 }
1200
1201 async fn get(
1202 &self,
1203 _ctx: Context,
1204 request: ServiceRequest<'_, exoware_proto::log::stream::v1::GetRequest>,
1205 ) -> connectrpc::ServiceResult<PreEncoded<StreamGetResponse>> {
1206 let seq = request.sequence_number;
1207 match self
1208 .state
1209 .log
1210 .get_batch(seq)
1211 .await
1212 .map_err(ConnectError::internal)?
1213 {
1214 Some(batch) => connectrpc::Response::ok(PreEncoded::from_bytes_unchecked(
1215 batch.into_response_bytes(),
1216 )),
1217 None => {
1218 let current = self.state.log.current_sequence();
1219 if seq > current {
1221 Err(self.batch_not_found_error())
1222 } else {
1223 let oldest = self
1224 .state
1225 .log
1226 .oldest_retained_batch()
1227 .await
1228 .map_err(ConnectError::internal)?;
1229 Err(self.batch_evicted_error(oldest))
1230 }
1231 }
1232 }
1233 }
1234}
1235
1236pub struct RetentionConnect<R> {
1237 state: RetentionState<R>,
1238}
1239
1240impl<R> Clone for RetentionConnect<R> {
1241 fn clone(&self) -> Self {
1242 Self {
1243 state: self.state.clone(),
1244 }
1245 }
1246}
1247
1248impl<R> RetentionConnect<R>
1249where
1250 R: Retention,
1251{
1252 pub fn new(state: impl Into<RetentionState<R>>) -> Self {
1253 Self {
1254 state: state.into(),
1255 }
1256 }
1257}
1258
1259impl<R> RetentionApi for RetentionConnect<R>
1260where
1261 R: Retention,
1262{
1263 async fn set_retention(
1264 &self,
1265 _ctx: Context,
1266 request: ServiceRequest<'_, exoware_proto::log::retention::v1::SetRetentionRequest>,
1267 ) -> connectrpc::ServiceResult<SetRetentionResponse> {
1268 let policy = exoware_proto::parse_set_retention_request_view(request.view())
1272 .map_err(ConnectError::invalid_argument)?;
1273 if let Some(policy) = policy.as_ref() {
1274 exoware_proto::validate_retention_policy(policy)
1275 .map_err(ConnectError::invalid_argument)?;
1276 }
1277
1278 let oldest_retained_sequence = self
1279 .state
1280 .retention
1281 .set_retention(policy)
1282 .await
1283 .map_err(ConnectError::internal)?;
1284 connectrpc::Response::ok(SetRetentionResponse {
1285 oldest_retained_sequence,
1286 ..Default::default()
1287 })
1288 }
1289}
1290
1291fn connect_limits() -> Limits {
1292 Limits::default()
1293 .with_max_request_body_size(MAX_CONNECTRPC_BODY_BYTES)
1294 .with_max_message_size(MAX_CONNECTRPC_BODY_BYTES)
1295}
1296
1297pub(crate) type IngestService<I> = ConnectRpcService<IngestServiceServer<IngestConnect<I>>>;
1298pub(crate) type QueryService<Q> = ConnectRpcService<QueryServiceServer<QueryConnect<Q>>>;
1299pub(crate) type PruneService<P> = ConnectRpcService<PruneServiceServer<PruneConnect<P>>>;
1300pub(crate) type RetentionService<R> =
1301 ConnectRpcService<RetentionServiceServer<RetentionConnect<R>>>;
1302pub(crate) type StreamService<B> = ConnectRpcService<StreamServiceServer<StreamConnect<B>>>;
1303pub(crate) type QueryStack<Q, B> = ConnectRpcService<
1304 Chain<QueryServiceServer<QueryConnect<Q>>, StreamServiceServer<StreamConnect<B>>>,
1305>;
1306pub(crate) type ConnectStack<I, Q, P, R, B> = ConnectRpcService<
1307 Chain<
1308 IngestServiceServer<IngestConnect<I>>,
1309 Chain<
1310 QueryServiceServer<QueryConnect<Q>>,
1311 Chain<
1312 PruneServiceServer<PruneConnect<P>>,
1313 Chain<
1314 RetentionServiceServer<RetentionConnect<R>>,
1315 StreamServiceServer<StreamConnect<B>>,
1316 >,
1317 >,
1318 >,
1319 >,
1320>;
1321
1322fn ingest_server<I>(state: IngestState<I>) -> IngestServiceServer<IngestConnect<I>>
1323where
1324 I: Ingest,
1325{
1326 IngestServiceServer::new(IngestConnect::new(state))
1327}
1328
1329fn query_server<Q>(state: QueryState<Q>) -> QueryServiceServer<QueryConnect<Q>>
1330where
1331 Q: Query,
1332{
1333 QueryServiceServer::new(QueryConnect::new(state))
1334}
1335
1336fn prune_server<P>(state: PruneState<P>) -> PruneServiceServer<PruneConnect<P>>
1337where
1338 P: Prune,
1339{
1340 PruneServiceServer::new(PruneConnect::new(state))
1341}
1342
1343fn retention_server<R>(state: RetentionState<R>) -> RetentionServiceServer<RetentionConnect<R>>
1344where
1345 R: Retention,
1346{
1347 RetentionServiceServer::new(RetentionConnect::new(state))
1348}
1349
1350fn stream_server<B>(state: StreamState<B>) -> StreamServiceServer<StreamConnect<B>>
1351where
1352 B: Log,
1353{
1354 StreamServiceServer::new(StreamConnect::new(state))
1355}
1356
1357pub fn ingest_service<I>(state: IngestState<I>) -> IngestService<I>
1358where
1359 I: Ingest,
1360{
1361 ConnectRpcService::new(ingest_server(state))
1362 .with_limits(connect_limits())
1363 .with_compression(connect_compression_registry())
1364}
1365
1366pub fn query_service<Q>(state: QueryState<Q>) -> QueryService<Q>
1367where
1368 Q: Query,
1369{
1370 ConnectRpcService::new(query_server(state))
1371 .with_limits(connect_limits())
1372 .with_compression(connect_compression_registry())
1373}
1374
1375pub fn prune_service<P>(state: PruneState<P>) -> PruneService<P>
1376where
1377 P: Prune,
1378{
1379 ConnectRpcService::new(prune_server(state))
1380 .with_limits(connect_limits())
1381 .with_compression(connect_compression_registry())
1382}
1383
1384pub fn retention_service<R>(state: RetentionState<R>) -> RetentionService<R>
1385where
1386 R: Retention,
1387{
1388 ConnectRpcService::new(retention_server(state))
1389 .with_limits(connect_limits())
1390 .with_compression(connect_compression_registry())
1391}
1392
1393pub fn stream_service<B>(state: StreamState<B>) -> StreamService<B>
1394where
1395 B: Log,
1396{
1397 ConnectRpcService::new(stream_server(state))
1398 .with_limits(connect_limits())
1399 .with_compression(connect_compression_registry())
1400}
1401
1402pub fn query_stack<Q, B>(
1403 query_state: QueryState<Q>,
1404 stream_state: StreamState<B>,
1405) -> QueryStack<Q, B>
1406where
1407 Q: Query,
1408 B: Log,
1409{
1410 ConnectRpcService::new(Chain(
1411 query_server(query_state),
1412 stream_server(stream_state),
1413 ))
1414 .with_limits(connect_limits())
1415 .with_compression(connect_compression_registry())
1416}
1417
1418pub fn connect_stack<E>(state: AppState<E>) -> ConnectStack<E, E, E, E, E>
1419where
1420 E: StoreEngine,
1421{
1422 ConnectRpcService::new(Chain(
1423 ingest_server(state.clone().into()),
1424 Chain(
1425 query_server(state.clone().into()),
1426 Chain(
1427 prune_server(state.clone().into()),
1428 Chain(
1429 retention_server(state.clone().into()),
1430 stream_server(state.into()),
1431 ),
1432 ),
1433 ),
1434 ))
1435 .with_limits(connect_limits())
1436 .with_compression(connect_compression_registry())
1437}
1438
1439#[cfg(test)]
1440mod tests {
1441 use super::*;
1442 use std::collections::{BTreeMap, HashMap};
1443 use std::sync::atomic::{AtomicU64, AtomicUsize};
1444 use std::sync::Mutex;
1445 use std::time::Duration;
1446
1447 use buffa::Message;
1448 use exoware_proto::common::kv::v1::Selector as ProtoSelector;
1449 use exoware_proto::log::retention::v1::SetRetentionRequest;
1450 use exoware_proto::log::stream::v1::{
1451 GetRequest as StreamGetRequest, SubscribeRequest, SubscribeRequestView,
1452 };
1453 use exoware_proto::store::prune::v1::{
1454 policy_retain, KeysScope, Policy as ProtoPolicy, PolicyRetain, PruneRequest,
1455 PruneRequestView, RetainKeepLatest,
1456 };
1457 use exoware_sdk::keys::Prefix;
1458 use exoware_sdk::kv_codec::KvReducedValue;
1459 use exoware_sdk::prune_policy::{PrunePolicyDocument, PRUNE_POLICY_DOCUMENT_VERSION};
1460 use exoware_sdk::retention::RetentionPolicy;
1461 use exoware_sdk::{decode_connect_error, to_domain_reduce_response};
1462 use futures::StreamExt;
1463
1464 use crate::{
1465 Ingest, IngestError, Log, LogBatch, Prune, Query, QueryExtra, RangeScan, RangeScanBatch,
1466 Retention, Sequence, StreamNotification, StreamNotifier,
1467 };
1468
1469 const TEST_PREFIX: u8 = 1;
1470
1471 #[derive(Clone)]
1472 struct PublishDuringReplay {
1473 hub: Arc<StreamHub>,
1474 sequence_offset: u64,
1475 kvs: Vec<(Bytes, Bytes)>,
1476 }
1477
1478 #[derive(Default)]
1479 struct FakeEngineState {
1480 current_sequence: u64,
1481 batches: BTreeMap<u64, Option<Vec<(Bytes, Bytes)>>>,
1482 oldest_retained: Option<u64>,
1483 publish_on_get_batch: Option<PublishDuringReplay>,
1484 range_rows: Vec<(Bytes, Bytes)>,
1485 range_eof_extra: QueryExtra,
1486 range_next_count: usize,
1487 range_batch_limit: Option<usize>,
1488 query_extra: QueryExtra,
1489 prune_policy_counts: Vec<usize>,
1490 put_error: Option<IngestError>,
1491 retention_calls: Vec<Option<RetentionPolicy>>,
1492 retention_floor: Option<u64>,
1493 }
1494
1495 #[derive(Default)]
1496 struct FakeEngine {
1497 state: Arc<Mutex<FakeEngineState>>,
1498 }
1499
1500 struct IteratorRangeScan {
1501 iter: Box<dyn Iterator<Item = Result<(Bytes, Bytes), String>> + Send + 'static>,
1502 eof_extra: Option<QueryExtra>,
1503 remaining_batches: Option<usize>,
1504 }
1505
1506 impl RangeScan for IteratorRangeScan {
1507 async fn next_batch(&mut self, max_items: usize) -> Result<RangeScanBatch, String> {
1508 if let Some(remaining) = &mut self.remaining_batches {
1509 if *remaining == 0 {
1510 futures::future::pending::<()>().await;
1511 }
1512 *remaining -= 1;
1513 }
1514 let mut rows = Vec::new();
1515 for row in self.iter.by_ref().take(max_items) {
1516 rows.push(row?);
1517 }
1518 let extra = if rows.is_empty() {
1519 self.eof_extra.take().unwrap_or_default()
1520 } else {
1521 QueryExtra::default()
1522 };
1523 Ok(RangeScanBatch { rows, extra })
1524 }
1525 }
1526
1527 fn range_scan_from_iter<I>(iter: I) -> IteratorRangeScan
1528 where
1529 I: Iterator<Item = Result<(Bytes, Bytes), String>> + Send + 'static,
1530 {
1531 range_scan_from_iter_with_eof_extra(iter, QueryExtra::default())
1532 }
1533
1534 fn range_scan_from_iter_with_eof_extra<I>(iter: I, eof_extra: QueryExtra) -> IteratorRangeScan
1535 where
1536 I: Iterator<Item = Result<(Bytes, Bytes), String>> + Send + 'static,
1537 {
1538 IteratorRangeScan {
1539 iter: Box::new(iter),
1540 eof_extra: Some(eof_extra),
1541 remaining_batches: None,
1542 }
1543 }
1544
1545 impl FakeEngine {
1546 fn set_current_sequence(&self, sequence_number: u64) {
1547 self.state.lock().expect("lock").current_sequence = sequence_number;
1548 }
1549
1550 fn set_put_error(&self, err: IngestError) {
1551 self.state.lock().expect("lock").put_error = Some(err);
1552 }
1553
1554 fn set_batch(&self, sequence_number: u64, kvs: Option<Vec<(Bytes, Bytes)>>) {
1555 self.state
1556 .lock()
1557 .expect("lock")
1558 .batches
1559 .insert(sequence_number, kvs);
1560 }
1561
1562 fn set_oldest_retained(&self, oldest_retained: Option<u64>) {
1563 self.state.lock().expect("lock").oldest_retained = oldest_retained;
1564 }
1565
1566 fn publish_live(
1567 &self,
1568 hub: Arc<StreamHub>,
1569 sequence_number: u64,
1570 kvs: Vec<(Bytes, Bytes)>,
1571 ) {
1572 let mut state = self.state.lock().expect("lock");
1573 state.current_sequence = state.current_sequence.max(sequence_number);
1574 state.batches.insert(sequence_number, Some(kvs.clone()));
1575 drop(state);
1576 hub.publish(sequence_number);
1577 }
1578
1579 fn publish_on_every_get_batch(
1580 &self,
1581 hub: Arc<StreamHub>,
1582 sequence_offset: u64,
1583 kvs: Vec<(Bytes, Bytes)>,
1584 ) {
1585 self.state.lock().expect("lock").publish_on_get_batch = Some(PublishDuringReplay {
1586 hub,
1587 sequence_offset,
1588 kvs,
1589 });
1590 }
1591
1592 fn set_range_rows(&self, rows: Vec<(Bytes, Bytes)>) {
1593 self.state.lock().expect("lock").range_rows = rows;
1594 }
1595
1596 fn set_range_eof_extra(&self, extra: QueryExtra) {
1597 self.state.lock().expect("lock").range_eof_extra = extra;
1598 }
1599
1600 fn range_next_count(&self) -> usize {
1601 self.state.lock().expect("lock").range_next_count
1602 }
1603
1604 fn set_query_extra(&self, extra: QueryExtra) {
1605 self.state.lock().expect("lock").query_extra = extra;
1606 }
1607
1608 fn set_retention_floor(&self, floor: Option<u64>) {
1609 self.state.lock().expect("lock").retention_floor = floor;
1610 }
1611
1612 fn retention_calls(&self) -> Vec<Option<RetentionPolicy>> {
1613 self.state.lock().expect("lock").retention_calls.clone()
1614 }
1615 }
1616
1617 impl Sequence for FakeEngine {
1618 fn current_sequence(&self) -> u64 {
1619 self.state.lock().expect("lock").current_sequence
1620 }
1621 }
1622
1623 impl Ingest for FakeEngine {
1624 async fn put_batch(&self, kvs: Vec<(Bytes, Bytes)>) -> Result<u64, IngestError> {
1625 let mut state = self.state.lock().map_err(|e| IngestError::Internal {
1626 message: e.to_string(),
1627 })?;
1628 if let Some(err) = state.put_error.take() {
1629 return Err(err);
1630 }
1631 state.current_sequence += 1;
1632 let seq = state.current_sequence;
1633 state.batches.insert(seq, Some(kvs));
1634 Ok(seq)
1635 }
1636 }
1637
1638 impl Query for FakeEngine {
1639 type RangeScan = IteratorRangeScan;
1640
1641 async fn get(&self, _key: Bytes) -> Result<(Option<Bytes>, QueryExtra), String> {
1642 self.state
1643 .lock()
1644 .map(|state| (None, state.query_extra.clone()))
1645 .map_err(|e| e.to_string())
1646 }
1647
1648 async fn get_many(
1649 &self,
1650 keys: Vec<Bytes>,
1651 ) -> Result<(Vec<(Bytes, Option<Bytes>)>, QueryExtra), String> {
1652 self.state
1653 .lock()
1654 .map(|state| {
1655 let entries = keys.into_iter().map(|key| (key, None)).collect();
1656 (entries, state.query_extra.clone())
1657 })
1658 .map_err(|e| e.to_string())
1659 }
1660
1661 async fn range_scan(
1662 &self,
1663 _start: Bytes,
1664 _end: Bytes,
1665 _limit: usize,
1666 _forward: bool,
1667 ) -> Result<Self::RangeScan, String> {
1668 let result = self
1669 .state
1670 .lock()
1671 .map(|state| {
1672 (
1673 state.range_rows.clone(),
1674 state.range_eof_extra.clone(),
1675 state.range_batch_limit,
1676 )
1677 })
1678 .map_err(|e| e.to_string());
1679 let state = self.state.clone();
1680 let cursor = result.map(|(rows, eof_extra, remaining_batches)| {
1681 let mut cursor = range_scan_from_iter_with_eof_extra(
1682 rows.into_iter().map(move |row| {
1683 state.lock().expect("lock").range_next_count += 1;
1684 Ok(row)
1685 }),
1686 eof_extra,
1687 );
1688 cursor.remaining_batches = remaining_batches;
1689 cursor
1690 });
1691 cursor
1692 }
1693 }
1694
1695 impl Prune for FakeEngine {
1696 async fn apply_prune_policies(&self, document: PrunePolicyDocument) -> Result<(), String> {
1697 self.state
1698 .lock()
1699 .map(|mut state| {
1700 state.prune_policy_counts.push(document.policies.len());
1701 })
1702 .map_err(|e| e.to_string())
1703 }
1704 }
1705
1706 impl Log for FakeEngine {
1707 async fn get_batch(&self, sequence_number: u64) -> Result<Option<LogBatch>, String> {
1708 let result: Result<_, String> = (|| {
1709 let mut state = self.state.lock().map_err(|e| e.to_string())?;
1710 let publish = state.publish_on_get_batch.clone();
1711 if let Some(publish) = publish.as_ref() {
1712 let live_sequence = publish.sequence_offset + sequence_number;
1713 state.current_sequence = state.current_sequence.max(live_sequence);
1714 state
1715 .batches
1716 .entry(live_sequence)
1717 .or_insert_with(|| Some(publish.kvs.clone()));
1718 }
1719 Ok((
1720 publish,
1721 state.batches.get(&sequence_number).cloned().unwrap_or(None),
1722 ))
1723 })();
1724 let (publish, batch) = result?;
1725 if let Some(publish) = publish {
1726 publish
1727 .hub
1728 .publish(publish.sequence_offset + sequence_number);
1729 }
1730 Ok(batch.map(|kvs| LogBatch::from_entries(sequence_number, kvs)))
1731 }
1732
1733 async fn oldest_retained_batch(&self) -> Result<Option<u64>, String> {
1734 self.state
1735 .lock()
1736 .map(|state| state.oldest_retained)
1737 .map_err(|e| e.to_string())
1738 }
1739 }
1740
1741 impl Retention for FakeEngine {
1742 async fn set_retention(
1743 &self,
1744 policy: Option<RetentionPolicy>,
1745 ) -> Result<Option<u64>, String> {
1746 self.state
1747 .lock()
1748 .map(|mut state| {
1749 state.retention_calls.push(policy);
1750 state.retention_floor
1751 })
1752 .map_err(|e| e.to_string())
1753 }
1754 }
1755
1756 #[derive(Default)]
1757 struct BatchGate {
1758 released: AtomicBool,
1759 waits: AtomicUsize,
1760 notify: Notify,
1761 }
1762
1763 impl BatchGate {
1764 async fn wait(&self) {
1765 loop {
1766 if self.released.load(Ordering::Acquire) {
1767 return;
1768 }
1769 let notified = self.notify.notified();
1770 self.waits.fetch_add(1, Ordering::Release);
1771 if self.released.load(Ordering::Acquire) {
1772 return;
1773 }
1774 notified.await;
1775 }
1776 }
1777
1778 fn release(&self) {
1779 self.released.store(true, Ordering::Release);
1780 self.notify.notify_waiters();
1781 }
1782
1783 fn wake(&self) {
1784 self.notify.notify_waiters();
1785 }
1786
1787 fn wait_count(&self) -> usize {
1788 self.waits.load(Ordering::Acquire)
1789 }
1790 }
1791
1792 #[derive(Default)]
1793 struct GatedLogState {
1794 batches: BTreeMap<u64, Vec<(Bytes, Bytes)>>,
1795 errors: HashMap<u64, String>,
1796 gates: HashMap<u64, Arc<BatchGate>>,
1797 started_sequences: Vec<u64>,
1798 get_counts: HashMap<u64, usize>,
1799 in_flight: usize,
1800 max_in_flight: usize,
1801 }
1802
1803 #[derive(Default)]
1804 struct GatedLog {
1805 current_sequence: AtomicU64,
1806 state: Arc<Mutex<GatedLogState>>,
1807 started: Arc<Notify>,
1808 }
1809
1810 impl GatedLog {
1811 fn set_batch(&self, sequence_number: u64, kvs: Vec<(Bytes, Bytes)>) {
1812 self.current_sequence
1813 .fetch_max(sequence_number, Ordering::Release);
1814 self.state
1815 .lock()
1816 .expect("lock")
1817 .batches
1818 .insert(sequence_number, kvs);
1819 }
1820
1821 fn gate(&self, sequence_number: u64) {
1822 self.state
1823 .lock()
1824 .expect("lock")
1825 .gates
1826 .insert(sequence_number, Arc::new(BatchGate::default()));
1827 }
1828
1829 fn set_error(&self, sequence_number: u64, error: impl Into<String>) {
1830 self.state
1831 .lock()
1832 .expect("lock")
1833 .errors
1834 .insert(sequence_number, error.into());
1835 }
1836
1837 fn release(&self, sequence_number: u64) {
1838 self.state
1839 .lock()
1840 .expect("lock")
1841 .gates
1842 .get(&sequence_number)
1843 .expect("gate")
1844 .release();
1845 }
1846
1847 fn wake(&self, sequence_number: u64) {
1848 self.state
1849 .lock()
1850 .expect("lock")
1851 .gates
1852 .get(&sequence_number)
1853 .expect("gate")
1854 .wake();
1855 }
1856
1857 fn gate_wait_count(&self, sequence_number: u64) -> usize {
1858 self.state
1859 .lock()
1860 .expect("lock")
1861 .gates
1862 .get(&sequence_number)
1863 .expect("gate")
1864 .wait_count()
1865 }
1866
1867 fn started_sequences(&self) -> Vec<u64> {
1868 self.state.lock().expect("lock").started_sequences.clone()
1869 }
1870
1871 fn get_count(&self, sequence_number: u64) -> usize {
1872 self.state
1873 .lock()
1874 .expect("lock")
1875 .get_counts
1876 .get(&sequence_number)
1877 .copied()
1878 .unwrap_or_default()
1879 }
1880
1881 fn in_flight(&self) -> usize {
1882 self.state.lock().expect("lock").in_flight
1883 }
1884
1885 fn max_in_flight(&self) -> usize {
1886 self.state.lock().expect("lock").max_in_flight
1887 }
1888
1889 async fn wait_for_started(&self, count: usize) {
1890 tokio::time::timeout(Duration::from_secs(1), async {
1891 loop {
1892 if self.started_sequences().len() >= count {
1893 return;
1894 }
1895 let notified = self.started.notified();
1896 if self.started_sequences().len() >= count {
1897 return;
1898 }
1899 notified.await;
1900 }
1901 })
1902 .await
1903 .expect("batch reads should start");
1904 }
1905
1906 async fn wait_for_gate_waits(&self, sequence_number: u64, count: usize) {
1907 tokio::time::timeout(Duration::from_secs(1), async {
1908 while self.gate_wait_count(sequence_number) < count {
1909 tokio::task::yield_now().await;
1910 }
1911 })
1912 .await
1913 .expect("batch gate should be polled");
1914 }
1915 }
1916
1917 struct InFlightGuard {
1918 state: Arc<Mutex<GatedLogState>>,
1919 }
1920
1921 impl Drop for InFlightGuard {
1922 fn drop(&mut self) {
1923 self.state.lock().expect("lock").in_flight -= 1;
1924 }
1925 }
1926
1927 impl Sequence for GatedLog {
1928 fn current_sequence(&self) -> u64 {
1929 self.current_sequence.load(Ordering::Acquire)
1930 }
1931 }
1932
1933 impl Log for GatedLog {
1934 async fn get_batch(&self, sequence_number: u64) -> Result<Option<LogBatch>, String> {
1935 let (gate, batch, error) = {
1936 let mut state = self.state.lock().map_err(|err| err.to_string())?;
1937 state.started_sequences.push(sequence_number);
1938 *state.get_counts.entry(sequence_number).or_default() += 1;
1939 state.in_flight += 1;
1940 state.max_in_flight = state.max_in_flight.max(state.in_flight);
1941 (
1942 state.gates.get(&sequence_number).cloned(),
1943 state.batches.get(&sequence_number).cloned(),
1944 state.errors.get(&sequence_number).cloned(),
1945 )
1946 };
1947 self.started.notify_waiters();
1948 let _guard = InFlightGuard {
1949 state: self.state.clone(),
1950 };
1951 if let Some(gate) = gate {
1952 gate.wait().await;
1953 }
1954 if let Some(error) = error {
1955 return Err(error);
1956 }
1957 Ok(batch.map(|kvs| LogBatch::from_entries(sequence_number, kvs)))
1958 }
1959
1960 async fn oldest_retained_batch(&self) -> Result<Option<u64>, String> {
1961 Ok(self
1962 .state
1963 .lock()
1964 .map_err(|err| err.to_string())?
1965 .batches
1966 .first_key_value()
1967 .map(|(sequence_number, _)| *sequence_number))
1968 }
1969 }
1970
1971 impl Retention for GatedLog {
1972 async fn set_retention(
1973 &self,
1974 _policy: Option<RetentionPolicy>,
1975 ) -> Result<Option<u64>, String> {
1976 self.oldest_retained_batch().await
1977 }
1978 }
1979
1980 struct DispatchLog {
1981 current_sequence: u64,
1982 get_batch_calls: AtomicUsize,
1983 get_batch_filtered_calls: AtomicUsize,
1984 }
1985
1986 impl DispatchLog {
1987 fn new(current_sequence: u64) -> Self {
1988 Self {
1989 current_sequence,
1990 get_batch_calls: AtomicUsize::new(0),
1991 get_batch_filtered_calls: AtomicUsize::new(0),
1992 }
1993 }
1994 }
1995
1996 impl Sequence for DispatchLog {
1997 fn current_sequence(&self) -> u64 {
1998 self.current_sequence
1999 }
2000 }
2001
2002 impl Log for DispatchLog {
2003 async fn get_batch(&self, sequence_number: u64) -> Result<Option<LogBatch>, String> {
2004 self.get_batch_calls.fetch_add(1, Ordering::Relaxed);
2005 Ok(Some(LogBatch::from_entries(
2006 sequence_number,
2007 vec![matching_kv(b"unfiltered", b"whole")],
2008 )))
2009 }
2010
2011 async fn get_batch_filtered(
2012 &self,
2013 sequence_number: u64,
2014 _matchers: &crate::stream::CompiledMatchers,
2015 ) -> Result<Option<FilteredBatch>, String> {
2016 self.get_batch_filtered_calls
2017 .fetch_add(1, Ordering::Relaxed);
2018 let entries = if sequence_number == 1 {
2019 Vec::new()
2020 } else {
2021 let (key, value) = matching_kv(b"filtered", b"selected");
2022 vec![Entry {
2023 key: key.to_vec(),
2024 value,
2025 ..Default::default()
2026 }]
2027 };
2028 Ok(Some(FilteredBatch::from_entries(entries)))
2029 }
2030
2031 async fn oldest_retained_batch(&self) -> Result<Option<u64>, String> {
2032 Ok(Some(1))
2033 }
2034 }
2035
2036 impl Retention for DispatchLog {
2037 async fn set_retention(
2038 &self,
2039 _policy: Option<RetentionPolicy>,
2040 ) -> Result<Option<u64>, String> {
2041 Ok(Some(1))
2042 }
2043 }
2044
2045 struct MalformedLog {
2046 current_sequence: u64,
2047 malformed: u64,
2048 }
2049
2050 impl Sequence for MalformedLog {
2051 fn current_sequence(&self) -> u64 {
2052 self.current_sequence
2053 }
2054 }
2055
2056 impl Log for MalformedLog {
2057 async fn get_batch(&self, sequence_number: u64) -> Result<Option<LogBatch>, String> {
2058 if sequence_number > self.current_sequence {
2059 return Ok(None);
2060 }
2061 if sequence_number == self.malformed {
2062 return Ok(Some(LogBatch::from_response_bytes(
2063 sequence_number,
2064 Bytes::from_static(&[0xff, 0xff, 0xff, 0xff]),
2065 )));
2066 }
2067 Ok(Some(LogBatch::from_entries(
2068 sequence_number,
2069 vec![matching_kv(b"ok", &[sequence_number as u8])],
2070 )))
2071 }
2072
2073 async fn oldest_retained_batch(&self) -> Result<Option<u64>, String> {
2074 Ok(Some(1))
2075 }
2076 }
2077
2078 struct QueryOnlyEngine {
2079 sequence_number: u64,
2080 value: Option<Bytes>,
2081 }
2082
2083 impl Sequence for QueryOnlyEngine {
2084 fn current_sequence(&self) -> u64 {
2085 self.sequence_number
2086 }
2087 }
2088
2089 impl Query for QueryOnlyEngine {
2090 type RangeScan = IteratorRangeScan;
2091
2092 async fn get(&self, _key: Bytes) -> Result<(Option<Bytes>, QueryExtra), String> {
2093 Ok((self.value.clone(), QueryExtra::default()))
2094 }
2095
2096 async fn range_scan(
2097 &self,
2098 _start: Bytes,
2099 _end: Bytes,
2100 _limit: usize,
2101 _forward: bool,
2102 ) -> Result<Self::RangeScan, String> {
2103 Ok(range_scan_from_iter(std::iter::empty()))
2104 }
2105
2106 async fn get_many(
2107 &self,
2108 keys: Vec<Bytes>,
2109 ) -> Result<(Vec<(Bytes, Option<Bytes>)>, QueryExtra), String> {
2110 Ok((
2111 keys.into_iter().map(|key| (key, None)).collect(),
2112 QueryExtra::default(),
2113 ))
2114 }
2115 }
2116
2117 #[derive(Default)]
2118 struct PruneOnlyEngine {
2119 documents: Mutex<Vec<(u32, usize)>>,
2120 }
2121
2122 impl PruneOnlyEngine {
2123 fn applied_count(&self) -> usize {
2124 self.documents.lock().expect("lock").len()
2125 }
2126
2127 fn last_document(&self) -> Option<(u32, usize)> {
2128 self.documents.lock().expect("lock").last().copied()
2129 }
2130 }
2131
2132 impl Prune for PruneOnlyEngine {
2133 async fn apply_prune_policies(&self, document: PrunePolicyDocument) -> Result<(), String> {
2134 self.documents
2135 .lock()
2136 .map(|mut documents| {
2137 documents.push((document.version, document.policies.len()));
2138 })
2139 .map_err(|e| e.to_string())
2140 }
2141 }
2142
2143 impl Retention for PruneOnlyEngine {
2144 async fn set_retention(
2145 &self,
2146 _policy: Option<RetentionPolicy>,
2147 ) -> Result<Option<u64>, String> {
2148 Ok(None)
2149 }
2150 }
2151
2152 struct ManualNotifier {
2153 current_sequence: AtomicU64,
2154 notify: Arc<Notify>,
2155 }
2156
2157 impl ManualNotifier {
2158 fn new(current_sequence: u64) -> Self {
2159 Self {
2160 current_sequence: AtomicU64::new(current_sequence),
2161 notify: Arc::new(Notify::new()),
2162 }
2163 }
2164 }
2165
2166 impl StreamNotifier for ManualNotifier {
2167 fn subscribe(&self) -> StreamNotification {
2168 StreamNotification {
2169 current_sequence: self.current_sequence.load(Ordering::Acquire),
2170 notify: self.notify.clone(),
2171 }
2172 }
2173
2174 fn current_sequence(&self) -> u64 {
2175 self.current_sequence.load(Ordering::Acquire)
2176 }
2177
2178 fn advance(&self, seq: u64) {
2179 self.current_sequence.fetch_max(seq, Ordering::SeqCst);
2180 self.notify.notify_waiters();
2181 }
2182 }
2183
2184 fn matching_kv(payload: &[u8], value: &[u8]) -> (Bytes, Bytes) {
2185 let key = Prefix::from_byte(TEST_PREFIX)
2186 .encode(payload)
2187 .expect("encode key");
2188 (key, Bytes::copy_from_slice(value))
2189 }
2190
2191 fn nonmatching_kv(payload: &[u8], value: &[u8]) -> (Bytes, Bytes) {
2192 let key = Prefix::from_byte(TEST_PREFIX + 1)
2193 .encode(payload)
2194 .expect("encode key");
2195 (key, Bytes::copy_from_slice(value))
2196 }
2197
2198 fn numeric_query_extra(name: &str, value: f64) -> QueryExtra {
2199 HashMap::from([(
2200 name.to_string(),
2201 buffa_types::google::protobuf::Value::from(value),
2202 )])
2203 }
2204
2205 fn subscribe_request_bytes(since_sequence_number: Option<u64>) -> Vec<u8> {
2206 SubscribeRequest {
2207 selectors: vec![ProtoSelector {
2208 prefix: Bytes::from(vec![TEST_PREFIX]),
2209 payload_regex: "(?s).*".to_string(),
2210 ..Default::default()
2211 }],
2212 since_sequence_number,
2213 ..Default::default()
2214 }
2215 .encode_to_vec()
2216 }
2217
2218 fn put_request(
2219 value_len: usize,
2220 ) -> buffa::view::OwnedView<exoware_proto::log::ingest::v1::PutRequestView<'static>> {
2221 let bytes = exoware_proto::ingest::PutRequest {
2222 kvs: vec![exoware_proto::common::Entry {
2223 key: b"k".to_vec(),
2224 value: Bytes::from(vec![1u8; value_len]),
2225 ..Default::default()
2226 }],
2227 ..Default::default()
2228 }
2229 .encode_to_vec();
2230 buffa::view::OwnedView::<exoware_proto::log::ingest::v1::PutRequestView<'static>>::decode(
2231 bytes.into(),
2232 )
2233 .expect("decode put request")
2234 }
2235
2236 fn keys_scope() -> KeysScope {
2237 KeysScope {
2238 selector: Some(ProtoSelector {
2239 prefix: Bytes::from(vec![TEST_PREFIX]),
2240 payload_regex: "(?s).*".to_string(),
2241 ..Default::default()
2242 })
2243 .into(),
2244 ..Default::default()
2245 }
2246 }
2247
2248 fn keys_drop_all_policy() -> ProtoPolicy {
2249 ProtoPolicy {
2250 keys: Some(keys_scope()).into(),
2251 retain: Some(PolicyRetain {
2252 kind: Some(policy_retain::Kind::DropAll(Box::default())),
2253 ..Default::default()
2254 })
2255 .into(),
2256 ..Default::default()
2257 }
2258 }
2259
2260 fn keys_keep_latest_policy(count: u64) -> ProtoPolicy {
2261 ProtoPolicy {
2262 keys: Some(keys_scope()).into(),
2263 retain: Some(PolicyRetain {
2264 kind: Some(policy_retain::Kind::KeepLatest(Box::new(
2265 RetainKeepLatest {
2266 count,
2267 ..Default::default()
2268 },
2269 ))),
2270 ..Default::default()
2271 })
2272 .into(),
2273 ..Default::default()
2274 }
2275 }
2276
2277 fn prune_request(
2278 policies: Vec<ProtoPolicy>,
2279 ) -> buffa::view::OwnedView<PruneRequestView<'static>> {
2280 let bytes = PruneRequest {
2281 policies,
2282 ..Default::default()
2283 }
2284 .encode_to_vec();
2285 buffa::view::OwnedView::<PruneRequestView<'static>>::decode(bytes.into())
2286 .expect("decode prune request")
2287 }
2288
2289 async fn subscribe_stream<B>(
2290 connect: &StreamConnect<B>,
2291 since_sequence_number: Option<u64>,
2292 ) -> Result<
2293 Pin<Box<dyn Stream<Item = Result<SubscribeResponse, ConnectError>> + Send>>,
2294 ConnectError,
2295 >
2296 where
2297 B: Log,
2298 {
2299 let bytes = subscribe_request_bytes(since_sequence_number);
2300 let request = buffa::view::OwnedView::<SubscribeRequestView<'static>>::decode(bytes.into())
2301 .expect("decode subscribe request");
2302 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2303 Ok(StreamApi::subscribe(connect, Context::default(), request)
2304 .await?
2305 .body)
2306 }
2307
2308 async fn next_subscribe_frame(
2309 frames: &mut tokio::sync::mpsc::UnboundedReceiver<Result<SubscribeResponse, ConnectError>>,
2310 ) -> SubscribeResponse {
2311 tokio::time::timeout(Duration::from_secs(1), frames.recv())
2312 .await
2313 .expect("stream should yield")
2314 .expect("frame should exist")
2315 .expect("frame should be ok")
2316 }
2317
2318 async fn set_retention<R>(
2319 connect: &RetentionConnect<R>,
2320 policy: Option<RetentionPolicy>,
2321 ) -> Result<SetRetentionResponse, ConnectError>
2322 where
2323 R: Retention,
2324 {
2325 let bytes = SetRetentionRequest {
2326 policy: policy
2327 .as_ref()
2328 .map(exoware_proto::retention_policy_to_proto)
2329 .into(),
2330 ..Default::default()
2331 }
2332 .encode_to_vec();
2333 let request =
2334 buffa::view::OwnedView::<SetRetentionRequestView<'static>>::decode(bytes.into())
2335 .expect("decode set_retention request");
2336 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2337 Ok(
2338 RetentionApi::set_retention(connect, Context::default(), request)
2339 .await?
2340 .body,
2341 )
2342 }
2343
2344 #[tokio::test]
2345 async fn prune_connect_accepts_prune_only_engine() {
2346 let prune = Arc::new(PruneOnlyEngine::default());
2347 let connect = PruneConnect::new(PruneState::new(prune.clone()));
2348 let request = prune_request(vec![keys_drop_all_policy()]);
2349
2350 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2351 PruneApi::prune(&connect, Context::default(), request)
2352 .await
2353 .expect("prune");
2354
2355 assert_eq!(prune.applied_count(), 1);
2356 assert_eq!(
2357 prune.last_document(),
2358 Some((PRUNE_POLICY_DOCUMENT_VERSION, 1))
2359 );
2360 }
2361
2362 #[tokio::test]
2363 async fn prune_rejects_unparseable_policy_before_engine_prune() {
2364 let prune = Arc::new(PruneOnlyEngine::default());
2365 let connect = PruneConnect::new(PruneState::new(prune.clone()));
2366 let invalid_policy = ProtoPolicy {
2369 keys: Some(KeysScope::default()).into(),
2370 ..Default::default()
2371 };
2372 let request = prune_request(vec![invalid_policy]);
2373
2374 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2375 let err = PruneApi::prune(&connect, Context::default(), request)
2376 .await
2377 .expect_err("invalid prune");
2378
2379 assert_eq!(err.code, connectrpc::ErrorCode::InvalidArgument);
2380 assert_eq!(prune.applied_count(), 0);
2381 }
2382
2383 #[tokio::test]
2384 async fn prune_rejects_invalid_policy_before_engine_prune() {
2385 let prune = Arc::new(PruneOnlyEngine::default());
2386 let connect = PruneConnect::new(PruneState::new(prune.clone()));
2387 let request = prune_request(vec![keys_keep_latest_policy(0)]);
2388
2389 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2390 let err = PruneApi::prune(&connect, Context::default(), request)
2391 .await
2392 .expect_err("invalid prune");
2393
2394 assert_eq!(err.code, connectrpc::ErrorCode::InvalidArgument);
2395 assert_eq!(prune.applied_count(), 0);
2396 }
2397
2398 #[tokio::test]
2399 async fn set_retention_applies_policy_and_returns_floor() {
2400 let engine = Arc::new(FakeEngine::default());
2401 engine.set_current_sequence(5);
2402 engine.set_retention_floor(Some(4));
2403 let connect = RetentionConnect::new(RetentionState::new(engine.clone()));
2404
2405 let response = set_retention(&connect, Some(RetentionPolicy::KeepLatest { count: 2 }))
2406 .await
2407 .expect("set_retention");
2408
2409 assert_eq!(response.oldest_retained_sequence, Some(4));
2411 assert_eq!(
2412 engine.retention_calls(),
2413 vec![Some(RetentionPolicy::KeepLatest { count: 2 })]
2414 );
2415 }
2416
2417 #[tokio::test]
2418 async fn set_retention_rejects_zero_keep_latest_before_engine() {
2419 let engine = Arc::new(FakeEngine::default());
2420 let connect = RetentionConnect::new(RetentionState::new(engine.clone()));
2421
2422 let err = set_retention(&connect, Some(RetentionPolicy::KeepLatest { count: 0 }))
2423 .await
2424 .expect_err("count 0 rejected");
2425
2426 assert_eq!(err.code, connectrpc::ErrorCode::InvalidArgument);
2427 assert!(engine.retention_calls().is_empty());
2429 }
2430
2431 #[tokio::test]
2432 async fn set_retention_absent_policy_clears_rule() {
2433 let engine = Arc::new(FakeEngine::default());
2434 engine.set_retention_floor(None);
2435 let connect = RetentionConnect::new(RetentionState::new(engine.clone()));
2436
2437 let response = set_retention(&connect, None)
2438 .await
2439 .expect("clear retention");
2440
2441 assert_eq!(response.oldest_retained_sequence, None);
2443 assert_eq!(engine.retention_calls(), vec![None]);
2444 }
2445
2446 #[tokio::test]
2447 async fn query_connect_accepts_query_only_engine() {
2448 let query = Arc::new(QueryOnlyEngine {
2449 sequence_number: 9,
2450 value: Some(Bytes::from_static(b"value")),
2451 });
2452 let connect = QueryConnect::new(QueryState::new(query));
2453 let bytes = exoware_proto::query::GetRequest {
2454 key: b"k".to_vec(),
2455 ..Default::default()
2456 }
2457 .encode_to_vec();
2458 let request = buffa::view::OwnedView::<
2459 exoware_proto::store::query::v1::GetRequestView<'static>,
2460 >::decode(bytes.into())
2461 .expect("decode get request");
2462
2463 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2464 let response = QueryApi::get(&connect, Context::default(), request)
2465 .await
2466 .expect("get")
2467 .body;
2468 let detail = response.detail.as_option().expect("query detail");
2469
2470 assert_eq!(response.value.as_deref(), Some(b"value".as_slice()));
2471 assert_eq!(detail.sequence_number, 9);
2472 }
2473
2474 #[tokio::test]
2475 async fn get_includes_engine_query_extra() {
2476 let engine = Arc::new(FakeEngine::default());
2477 engine.set_current_sequence(5);
2478 engine.set_query_extra(HashMap::from([(
2479 "scanned_bytes".to_string(),
2480 buffa_types::google::protobuf::Value::from(123.0),
2481 )]));
2482 let connect = QueryConnect::new(AppState::new(engine));
2483 let bytes = exoware_proto::query::GetRequest {
2484 key: b"k".to_vec(),
2485 ..Default::default()
2486 }
2487 .encode_to_vec();
2488 let request = buffa::view::OwnedView::<
2489 exoware_proto::store::query::v1::GetRequestView<'static>,
2490 >::decode(bytes.into())
2491 .expect("decode get request");
2492
2493 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2494 let response = QueryApi::get(&connect, Context::default(), request)
2495 .await
2496 .expect("get")
2497 .body;
2498 let detail = response.detail.as_option().expect("query detail");
2499
2500 assert_eq!(detail.sequence_number, 5);
2501 assert_eq!(
2502 detail
2503 .extra
2504 .get("scanned_bytes")
2505 .and_then(|v| v.as_number()),
2506 Some(123.0)
2507 );
2508 }
2509
2510 #[test]
2511 fn split_service_constructors_build_independent_process_surfaces() {
2512 let engine = Arc::new(FakeEngine::default());
2513 let state = AppState::new(engine);
2514
2515 let _ingest = ingest_service(state.clone().into());
2516 let _query = query_service(state.clone().into());
2517 let _prune = prune_service(state.clone().into());
2518 let _retention = retention_service(state.clone().into());
2519 let _stream = stream_service(state.clone().into());
2520 let _query_stack = query_stack(state.clone().into(), state.into());
2521 }
2522
2523 #[tokio::test]
2524 async fn ingest_uses_configured_value_limit() {
2525 let engine = Arc::new(FakeEngine::default());
2526 let state = IngestState::new(engine).with_limits(IngestLimits { max_value_len: 4 });
2527 let connect = IngestConnect::new(state);
2528
2529 let request = put_request(5);
2530 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2531 let err = IngestApi::put(&connect, Context::default(), request)
2532 .await
2533 .expect_err("put should reject oversized value");
2534
2535 assert_eq!(err.code, connectrpc::ErrorCode::InvalidArgument);
2536 }
2537
2538 #[tokio::test]
2539 async fn ingest_unavailable_surfaces_as_unavailable_with_retry_info() {
2540 let engine = Arc::new(FakeEngine::default());
2541 engine.set_put_error(IngestError::Unavailable {
2542 message: "backend bouncing".to_string(),
2543 });
2544 let connect = IngestConnect::new(IngestState::new(engine));
2545
2546 let request = put_request(1);
2547 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2548 let err = IngestApi::put(&connect, Context::default(), request)
2549 .await
2550 .expect_err("transient put failure should surface");
2551
2552 assert_eq!(err.code, connectrpc::ErrorCode::Unavailable);
2553 let decoded = decode_connect_error(&err).expect("decode details");
2554 assert_eq!(
2555 decoded.error_info.expect("error info").reason,
2556 REASON_INGEST_UNAVAILABLE
2557 );
2558 let retry_delay = decoded.retry_info.expect("retry info").retry_delay;
2559 let retry_delay = retry_delay.as_option().expect("retry delay");
2560 assert_eq!((retry_delay.seconds, retry_delay.nanos), (1, 0));
2561 }
2562
2563 #[tokio::test]
2564 async fn ingest_internal_surfaces_as_internal_without_error_info() {
2565 let engine = Arc::new(FakeEngine::default());
2566 engine.set_put_error(IngestError::Internal {
2567 message: "invariant violated".to_string(),
2568 });
2569 let connect = IngestConnect::new(IngestState::new(engine));
2570
2571 let request = put_request(1);
2572 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2573 let err = IngestApi::put(&connect, Context::default(), request)
2574 .await
2575 .expect_err("fatal put failure should surface");
2576
2577 assert_eq!(err.code, connectrpc::ErrorCode::Internal);
2578 let decoded = decode_connect_error(&err).expect("decode details");
2579 assert!(decoded.error_info.is_none());
2580 }
2581
2582 #[tokio::test]
2583 async fn put_when_not_ready_keeps_worker_not_ready_reason() {
2584 let engine = Arc::new(FakeEngine::default());
2585 let state = IngestState::new(engine);
2586 state.ready.store(false, Ordering::SeqCst);
2587 let connect = IngestConnect::new(state);
2588
2589 let request = put_request(1);
2590 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2591 let err = IngestApi::put(&connect, Context::default(), request)
2592 .await
2593 .expect_err("not-ready gate should reject");
2594
2595 assert_eq!(err.code, connectrpc::ErrorCode::Unavailable);
2598 let decoded = decode_connect_error(&err).expect("decode details");
2599 assert_eq!(
2600 decoded.error_info.expect("error info").reason,
2601 REASON_WORKER_NOT_READY
2602 );
2603 }
2604
2605 #[tokio::test]
2606 async fn stream_can_be_advanced_by_external_notifier() {
2607 let engine = Arc::new(FakeEngine::default());
2608 let notifier = Arc::new(ManualNotifier::new(0));
2609 let connect = StreamConnect::new(StreamState::new(engine.clone(), notifier.clone()));
2610 let mut stream = subscribe_stream(&connect, None).await.expect("subscribe");
2611
2612 engine.set_current_sequence(1);
2613 engine.set_batch(1, Some(vec![matching_kv(b"hit", b"v1")]));
2614 notifier.advance(1);
2615
2616 let frame = tokio::time::timeout(Duration::from_secs(1), stream.next())
2617 .await
2618 .expect("stream should yield")
2619 .expect("frame should exist")
2620 .expect("frame should be ok");
2621 assert_eq!(frame.sequence_number, 1);
2622 assert_eq!(frame.entries.len(), 1);
2623 assert_eq!(frame.entries[0].value.as_ref(), b"v1");
2624 }
2625
2626 #[tokio::test]
2627 async fn reduce_consumes_range_iterator_and_returns_detail() {
2628 let engine = Arc::new(FakeEngine::default());
2629 engine.set_current_sequence(7);
2630 engine.set_range_rows(vec![
2631 (Bytes::from_static(b"a"), Bytes::from_static(b"xx")),
2632 (Bytes::from_static(b"bb"), Bytes::from_static(b"yyy")),
2633 ]);
2634 let connect = QueryConnect::new(AppState::new(engine.clone()));
2635 let bytes = exoware_proto::query::ReduceRequest {
2636 start: b"a".to_vec(),
2637 end: b"z".to_vec(),
2638 params: Some(exoware_proto::query::ReduceParams {
2639 reducers: vec![exoware_proto::query::RangeReducerSpec {
2640 op: exoware_proto::query::RangeReduceOp::RANGE_REDUCE_OP_COUNT_ALL.into(),
2641 ..Default::default()
2642 }],
2643 ..Default::default()
2644 })
2645 .into(),
2646 ..Default::default()
2647 }
2648 .encode_to_vec();
2649 let request = buffa::view::OwnedView::<
2650 exoware_proto::store::query::v1::ReduceRequestView<'static>,
2651 >::decode(bytes.into())
2652 .expect("decode reduce request");
2653
2654 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2655 let mut frames = QueryApi::reduce(&connect, Context::default(), request)
2656 .await
2657 .expect("reduce")
2658 .body;
2659 let response = frames
2660 .next()
2661 .await
2662 .expect("scalar frame")
2663 .expect("reduce frame");
2664 assert!(frames.next().await.is_none());
2665 let detail = response.detail.as_option().expect("query detail").clone();
2666 let response =
2667 to_domain_reduce_response(connectrpc::StreamMessage::from_message(&response).view())
2668 .expect("decode reduce response");
2669
2670 assert_eq!(engine.range_next_count(), 2);
2671 assert_eq!(response.results.len(), 1);
2672 assert_eq!(response.results[0].value, Some(KvReducedValue::UInt64(2)));
2673 assert_eq!(detail.sequence_number, 7);
2674 assert!(detail.extra.is_empty());
2675 }
2676
2677 #[tokio::test]
2678 async fn reduce_uses_eof_query_extra() {
2679 let engine = Arc::new(FakeEngine::default());
2680 engine.set_current_sequence(8);
2681 engine.set_range_rows(vec![
2682 (Bytes::from_static(b"a"), Bytes::from_static(b"xx")),
2683 (Bytes::from_static(b"bb"), Bytes::from_static(b"yyy")),
2684 ]);
2685 engine.set_range_eof_extra(numeric_query_extra("final_rows", 2.0));
2686 let connect = QueryConnect::new(AppState::new(engine));
2687 let bytes = exoware_proto::query::ReduceRequest {
2688 start: b"a".to_vec(),
2689 end: b"z".to_vec(),
2690 params: Some(exoware_proto::query::ReduceParams {
2691 reducers: vec![exoware_proto::query::RangeReducerSpec {
2692 op: exoware_proto::query::RangeReduceOp::RANGE_REDUCE_OP_COUNT_ALL.into(),
2693 ..Default::default()
2694 }],
2695 ..Default::default()
2696 })
2697 .into(),
2698 ..Default::default()
2699 }
2700 .encode_to_vec();
2701 let request = buffa::view::OwnedView::<
2702 exoware_proto::store::query::v1::ReduceRequestView<'static>,
2703 >::decode(bytes.into())
2704 .expect("decode reduce request");
2705
2706 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2707 let mut frames = QueryApi::reduce(&connect, Context::default(), request)
2708 .await
2709 .expect("reduce")
2710 .body;
2711 let response = frames
2712 .next()
2713 .await
2714 .expect("scalar frame")
2715 .expect("reduce frame");
2716 assert!(frames.next().await.is_none());
2717 let detail = response.detail.as_option().expect("query detail");
2718
2719 assert_eq!(detail.sequence_number, 8);
2720 assert_eq!(
2721 detail.extra.get("final_rows").and_then(|v| v.as_number()),
2722 Some(2.0)
2723 );
2724 }
2725
2726 fn grouped_reduce_fixture(
2727 groups: usize,
2728 width: usize,
2729 ) -> (Arc<FakeEngine>, exoware_proto::RangeReduceRequest) {
2730 use commonware_codec::Encode as _;
2731 use exoware_sdk::kv_codec::{KvExpr, KvFieldKind, KvFieldRef, StoredRow, StoredValue};
2732 let engine = Arc::new(FakeEngine::default());
2733 engine.set_current_sequence(9);
2734 engine.set_range_rows(
2735 (0..groups)
2736 .map(|group| {
2737 (
2738 Bytes::copy_from_slice(&group.to_be_bytes()),
2739 StoredRow {
2740 values: vec![Some(StoredValue::Utf8(format!(
2741 "{group:08}-{}",
2742 "x".repeat(width)
2743 )))],
2744 }
2745 .encode(),
2746 )
2747 })
2748 .collect(),
2749 );
2750 let request = exoware_proto::RangeReduceRequest {
2751 reducers: vec![exoware_proto::RangeReducerSpec {
2752 op: exoware_proto::RangeReduceOp::CountAll,
2753 expr: None,
2754 filter: None,
2755 }],
2756 group_by: vec![KvExpr::Field(KvFieldRef::Value {
2757 index: 0,
2758 kind: KvFieldKind::Utf8,
2759 nullable: true,
2760 })],
2761 filter: None,
2762 };
2763 (engine, request)
2764 }
2765
2766 #[tokio::test]
2767 async fn reduce_frames_bound_rows_and_preserve_final_detail() {
2768 let (engine, request) = grouped_reduce_fixture(5000, 0);
2769 engine.set_range_eof_extra(numeric_query_extra("final_rows", 5000.0));
2770 let state = QueryState::new(engine.clone());
2771 let execution = execute_reduce(
2772 engine,
2773 Bytes::new(),
2774 Bytes::new(),
2775 request,
2776 state.context.clone(),
2777 )
2778 .unwrap();
2779 let mut frames = Box::pin(reduce_frames(execution, 9));
2780 let mut seen = std::collections::BTreeSet::new();
2781 let mut frame_count = 0;
2782 while let Some(frame) = frames.next().await {
2783 let frame = frame.unwrap();
2784 assert!(frame.groups.len() <= REDUCE_BATCH_ROWS);
2785 assert!(frame.encoded_len() as usize <= REDUCE_FRAME_TARGET_BYTES);
2786 assert_eq!(frame.detail.as_option().unwrap().sequence_number, 9);
2787 assert_eq!(
2788 frame.detail.as_option().unwrap().extra["final_rows"].as_number(),
2789 Some(5000.0)
2790 );
2791 for group in
2792 to_domain_reduce_response(connectrpc::StreamMessage::from_message(&frame).view())
2793 .unwrap()
2794 .groups
2795 {
2796 let Some(KvReducedValue::Utf8(key)) =
2797 group.group_values.into_iter().next().unwrap()
2798 else {
2799 panic!("group key");
2800 };
2801 assert!(seen.insert(key));
2802 assert_eq!(group.results[0].value, Some(KvReducedValue::UInt64(1)));
2803 }
2804 frame_count += 1;
2805 }
2806 assert_eq!(seen.len(), 5000);
2807 assert!(frame_count > 1);
2808 assert_eq!(state.context.memory_pool().reserved(), 0);
2809 }
2810
2811 #[tokio::test]
2812 async fn reduce_splits_wide_evaluated_batches_and_wire_frames() {
2813 let (engine, request) = grouped_reduce_fixture(40, 512 * 1024);
2814 let state = QueryState::new(engine.clone());
2815 let execution = execute_reduce(
2816 engine,
2817 Bytes::new(),
2818 Bytes::new(),
2819 request,
2820 state.context.clone(),
2821 )
2822 .unwrap();
2823 let mut frames = Box::pin(reduce_frames(execution, 9));
2824 let mut count = 0;
2825 let mut frame_count = 0;
2826 while let Some(frame) = frames.next().await {
2827 let frame = frame.unwrap();
2828 assert!(frame.encoded_len() as usize <= REDUCE_FRAME_TARGET_BYTES);
2829 count += frame.groups.len();
2830 frame_count += 1;
2831 }
2832 assert_eq!(count, 40);
2833 assert!(frame_count > 1);
2834 }
2835
2836 #[tokio::test]
2837 async fn reduce_allows_one_group_larger_than_the_frame_target() {
2838 let (engine, mut request) = grouped_reduce_fixture(1, 9 * 1024 * 1024);
2839 request.reducers.push(exoware_proto::RangeReducerSpec {
2840 op: exoware_proto::RangeReduceOp::MinField,
2841 expr: Some(request.group_by[0].clone()),
2842 filter: None,
2843 });
2844 let state = QueryState::new(engine.clone());
2845 let execution =
2846 execute_reduce(engine, Bytes::new(), Bytes::new(), request, state.context).unwrap();
2847 let mut frames = Box::pin(reduce_frames(execution, 9));
2848 let frame = frames.next().await.unwrap().unwrap();
2849 assert!(frame.encoded_len() as usize > REDUCE_FRAME_TARGET_BYTES);
2850 assert!((frame.encoded_len() as usize) < MAX_CONNECTRPC_BODY_BYTES);
2851 let response =
2852 to_domain_reduce_response(connectrpc::StreamMessage::from_message(&frame).view())
2853 .unwrap();
2854 assert_eq!(response.groups.len(), 1);
2855 assert_eq!(
2856 response.groups[0].results[0].value,
2857 Some(KvReducedValue::UInt64(1))
2858 );
2859 assert_eq!(
2860 response.groups[0].results[1].value,
2861 response.groups[0].group_values[0]
2862 );
2863 assert!(frames.next().await.is_none());
2864 }
2865
2866 #[tokio::test]
2867 async fn empty_grouped_reduce_emits_one_detail_frame() {
2868 let (engine, request) = grouped_reduce_fixture(0, 0);
2869 engine.set_range_eof_extra(numeric_query_extra("final_rows", 0.0));
2870 let state = QueryState::new(engine.clone());
2871 let execution =
2872 execute_reduce(engine, Bytes::new(), Bytes::new(), request, state.context).unwrap();
2873 let mut frames = Box::pin(reduce_frames(execution, 9));
2874 let frame = frames.next().await.unwrap().unwrap();
2875 assert!(frame.results.is_empty());
2876 assert!(frame.groups.is_empty());
2877 assert_eq!(frame.detail.as_option().unwrap().sequence_number, 9);
2878 assert_eq!(
2879 frame.detail.as_option().unwrap().extra["final_rows"].as_number(),
2880 Some(0.0)
2881 );
2882 assert!(frames.next().await.is_none());
2883 }
2884
2885 #[tokio::test]
2886 async fn concurrent_reduce_cancellation_releases_shared_pool() {
2887 use datafusion::execution::memory_pool::FairSpillPool;
2888 use datafusion::execution::runtime_env::RuntimeEnvBuilder;
2889 let (engine, request) = grouped_reduce_fixture(5000, 0);
2890 engine.state.lock().unwrap().range_batch_limit = Some(1);
2891 let runtime = RuntimeEnvBuilder::new()
2892 .with_memory_pool(Arc::new(FairSpillPool::new(256 * 1024 * 1024)))
2893 .build_arc()
2894 .unwrap();
2895 let state = QueryState::new(engine.clone()).with_runtime(runtime);
2896 let cloned = state.clone();
2897 assert!(Arc::ptr_eq(&state.context, &cloned.context));
2898 let left = execute_reduce(
2899 engine.clone(),
2900 Bytes::new(),
2901 Bytes::new(),
2902 request.clone(),
2903 state.context.clone(),
2904 )
2905 .unwrap();
2906 let right = execute_reduce(
2907 engine.clone(),
2908 Bytes::new(),
2909 Bytes::new(),
2910 request,
2911 cloned.context.clone(),
2912 )
2913 .unwrap();
2914 let mut left = Box::pin(reduce_frames(left, 9));
2915 let mut right = Box::pin(reduce_frames(right, 9));
2916 while engine.range_next_count() < REDUCE_BATCH_ROWS * 2 {
2917 assert!(futures::poll!(left.next()).is_pending());
2918 assert!(futures::poll!(right.next()).is_pending());
2919 tokio::task::yield_now().await;
2920 }
2921 assert!(state.context.memory_pool().reserved() > 0);
2922 drop(left);
2923 assert!(state.context.memory_pool().reserved() > 0);
2924 drop(right);
2925 assert_eq!(state.context.memory_pool().reserved(), 0);
2926 let consumed = engine.range_next_count();
2927 tokio::task::yield_now().await;
2928 assert_eq!(engine.range_next_count(), consumed);
2929 assert_eq!(consumed, REDUCE_BATCH_ROWS * 2);
2930 }
2931
2932 #[tokio::test]
2933 async fn get_many_populates_detail_on_each_frame() {
2934 let engine = Arc::new(FakeEngine::default());
2935 engine.set_current_sequence(11);
2936 let connect = QueryConnect::new(AppState::new(engine));
2937 let bytes = exoware_proto::query::GetManyRequest {
2938 keys: vec![b"a".to_vec(), b"bb".to_vec(), b"ccc".to_vec()],
2939 batch_size: 2,
2940 ..Default::default()
2941 }
2942 .encode_to_vec();
2943 let request = buffa::view::OwnedView::<
2944 exoware_proto::store::query::v1::GetManyRequestView<'static>,
2945 >::decode(bytes.into())
2946 .expect("decode get_many request");
2947
2948 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2949 let mut stream = QueryApi::get_many(&connect, Context::default(), request)
2950 .await
2951 .expect("get_many")
2952 .body;
2953 let mut frame_sizes = Vec::new();
2954 let mut detail_frames = 0usize;
2955 while let Some(frame) = stream.next().await {
2956 let frame = frame.expect("get_many frame");
2957 frame_sizes.push(frame.results.len());
2958 let detail = frame.detail.as_option().expect("query detail");
2959 assert_eq!(detail.sequence_number, 11);
2960 assert!(detail.extra.is_empty());
2961 detail_frames += 1;
2962 }
2963
2964 assert_eq!(frame_sizes, vec![2, 1]);
2965 assert_eq!(detail_frames, 2);
2966 }
2967
2968 #[tokio::test]
2969 async fn range_returns_without_materializing_full_iterator() {
2970 let engine = Arc::new(FakeEngine::default());
2971 engine.set_current_sequence(9);
2972 engine.set_range_rows(
2973 (0..1000)
2974 .map(|i| {
2975 (
2976 Bytes::from(format!("key-{i:04}")),
2977 Bytes::from_static(b"value"),
2978 )
2979 })
2980 .collect(),
2981 );
2982 let connect = QueryConnect::new(AppState::new(engine.clone()));
2983 let bytes = exoware_proto::query::RangeRequest {
2984 start: b"a".to_vec(),
2985 end: b"z".to_vec(),
2986 limit: Some(1000),
2987 batch_size: 1,
2988 ..Default::default()
2989 }
2990 .encode_to_vec();
2991 let request = buffa::view::OwnedView::<
2992 exoware_proto::store::query::v1::RangeRequestView<'static>,
2993 >::decode(bytes.into())
2994 .expect("decode range request");
2995
2996 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
2997 let mut stream = QueryApi::range(&connect, Context::default(), request)
2998 .await
2999 .expect("range")
3000 .body;
3001
3002 tokio::time::sleep(Duration::from_millis(50)).await;
3003 let consumed = engine.range_next_count();
3004 assert!(
3005 consumed < 1000,
3006 "range should not consume the full iterator before the response stream is read; consumed {consumed}",
3007 );
3008
3009 let mut rows = 0;
3010 let mut latest_detail = None;
3011 let mut detail_frames = 0usize;
3012 while let Some(frame) = stream.next().await {
3013 let frame = frame.expect("range frame");
3014 rows += frame.results.len();
3015 if let Some(detail) = frame.detail.as_option() {
3016 detail_frames += 1;
3017 latest_detail = Some(detail.clone());
3018 }
3019 }
3020
3021 assert_eq!(rows, 1000);
3022 assert_eq!(detail_frames, 1000);
3023 let detail = latest_detail.expect("query detail");
3024 assert_eq!(detail.sequence_number, 9);
3025 assert!(detail.extra.is_empty());
3026 }
3027
3028 #[tokio::test]
3029 async fn range_emits_eof_query_extra_after_rows() {
3030 let engine = Arc::new(FakeEngine::default());
3031 engine.set_current_sequence(10);
3032 engine.set_range_rows(vec![
3033 (Bytes::from_static(b"a"), Bytes::from_static(b"1")),
3034 (Bytes::from_static(b"b"), Bytes::from_static(b"2")),
3035 ]);
3036 engine.set_range_eof_extra(numeric_query_extra("final_rows", 2.0));
3037 let connect = QueryConnect::new(AppState::new(engine));
3038 let bytes = exoware_proto::query::RangeRequest {
3039 start: b"a".to_vec(),
3040 end: b"z".to_vec(),
3041 limit: Some(2),
3042 batch_size: 2,
3043 ..Default::default()
3044 }
3045 .encode_to_vec();
3046 let request = buffa::view::OwnedView::<
3047 exoware_proto::store::query::v1::RangeRequestView<'static>,
3048 >::decode(bytes.into())
3049 .expect("decode range request");
3050
3051 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
3052 let mut stream = QueryApi::range(&connect, Context::default(), request)
3053 .await
3054 .expect("range")
3055 .body;
3056 let mut frames = Vec::new();
3057 while let Some(frame) = stream.next().await {
3058 frames.push(frame.expect("range frame"));
3059 }
3060
3061 assert_eq!(frames.len(), 2);
3062 assert_eq!(frames[0].results.len(), 2);
3063 let row_detail = frames[0].detail.as_option().expect("row detail");
3064 assert!(row_detail.extra.is_empty());
3065
3066 assert!(frames[1].results.is_empty());
3067 let final_detail = frames[1].detail.as_option().expect("final detail");
3068 assert_eq!(final_detail.sequence_number, 10);
3069 assert_eq!(
3070 final_detail
3071 .extra
3072 .get("final_rows")
3073 .and_then(|v| v.as_number()),
3074 Some(2.0)
3075 );
3076 }
3077
3078 #[tokio::test]
3079 async fn subscribe_without_replay_reads_the_next_live_batch() {
3080 let engine = Arc::new(FakeEngine::default());
3081 let state = AppState::new(engine.clone());
3082 let connect = StreamConnect::new(state.clone());
3083 let mut stream = subscribe_stream(&connect, None).await.expect("subscribe");
3084 engine.publish_live(state.stream.clone(), 1, vec![matching_kv(b"hit", b"v1")]);
3085 let frame = tokio::time::timeout(Duration::from_secs(1), stream.next())
3086 .await
3087 .expect("stream should yield")
3088 .expect("frame should exist")
3089 .expect("frame should be ok");
3090 assert_eq!(frame.sequence_number, 1);
3091 assert_eq!(frame.entries.len(), 1);
3092 assert_eq!(frame.entries[0].value.as_ref(), b"v1");
3093 }
3094
3095 #[tokio::test]
3096 async fn dropping_subscription_cancels_in_flight_lookahead() {
3097 let last_sequence = SUBSCRIBE_GET_BATCH_LOOKAHEAD as u64 + 1;
3098 let engine = Arc::new(GatedLog::default());
3099 for sequence_number in 1..=last_sequence {
3100 engine.set_batch(
3101 sequence_number,
3102 vec![matching_kv(b"replay", &[sequence_number as u8])],
3103 );
3104 }
3105 for sequence_number in 2..=last_sequence {
3106 engine.gate(sequence_number);
3107 }
3108
3109 let notifier = Arc::new(ManualNotifier::new(last_sequence));
3110 let connect = StreamConnect::new(StreamState::new(engine.clone(), notifier));
3111 let mut stream = subscribe_stream(&connect, Some(1))
3112 .await
3113 .expect("subscribe");
3114
3115 let first = tokio::time::timeout(Duration::from_secs(1), stream.next())
3116 .await
3117 .expect("first frame")
3118 .expect("frame exists")
3119 .expect("frame ok");
3120 assert_eq!(first.sequence_number, 1);
3121
3122 assert!(
3123 tokio::time::timeout(Duration::from_millis(50), stream.next())
3124 .await
3125 .is_err(),
3126 "gated batches must hold the stream pending",
3127 );
3128 engine.wait_for_started(last_sequence as usize).await;
3129 assert_eq!(engine.in_flight(), SUBSCRIBE_GET_BATCH_LOOKAHEAD);
3130
3131 drop(stream);
3132 assert_eq!(engine.in_flight(), 0);
3133 }
3134
3135 #[tokio::test]
3136 async fn subscribe_uses_filtered_override_and_get_remains_unfiltered() {
3137 let engine = Arc::new(DispatchLog::new(3));
3138 let notifier = Arc::new(ManualNotifier::new(2));
3139 let connect = StreamConnect::new(StreamState::new(engine.clone(), notifier.clone()));
3140 let mut stream = subscribe_stream(&connect, Some(1))
3141 .await
3142 .expect("subscribe");
3143
3144 let replay = stream.next().await.unwrap().unwrap();
3145 assert_eq!(replay.sequence_number, 2);
3146 assert_eq!(replay.entries[0].value.as_ref(), b"selected");
3147 assert_eq!(engine.get_batch_filtered_calls.load(Ordering::Relaxed), 2);
3148 assert_eq!(engine.get_batch_calls.load(Ordering::Relaxed), 0);
3149
3150 notifier.advance(3);
3151 let live = stream.next().await.unwrap().unwrap();
3152 assert_eq!(live.sequence_number, 3);
3153 assert_eq!(engine.get_batch_filtered_calls.load(Ordering::Relaxed), 3);
3154
3155 let request = buffa::view::OwnedView::<GetRequestView<'static>>::decode(
3156 StreamGetRequest {
3157 sequence_number: 3,
3158 ..Default::default()
3159 }
3160 .encode_to_vec()
3161 .into(),
3162 )
3163 .expect("decode get request");
3164 let request = ServiceRequest::from_parts(request.reborrow(), request.bytes());
3165 StreamApi::get(&connect, Context::default(), request)
3166 .await
3167 .expect("get");
3168
3169 assert_eq!(engine.get_batch_calls.load(Ordering::Relaxed), 1);
3170 assert_eq!(engine.get_batch_filtered_calls.load(Ordering::Relaxed), 3);
3171 }
3172
3173 #[tokio::test]
3174 async fn subscribe_since_evicted_batch_fails_setup_with_batch_evicted_metadata() {
3175 let engine = Arc::new(FakeEngine::default());
3176 engine.set_current_sequence(3);
3177 engine.set_oldest_retained(Some(2));
3178 engine.set_batch(2, Some(vec![matching_kv(b"replay", b"v2")]));
3179 engine.set_batch(3, Some(vec![matching_kv(b"replay", b"v3")]));
3180
3181 let connect = StreamConnect::new(AppState::new(engine));
3182 let Err(err) = subscribe_stream(&connect, Some(1)).await else {
3183 panic!("evicted cursor must fail subscription setup");
3184 };
3185
3186 assert_eq!(err.code, connectrpc::ErrorCode::OutOfRange);
3187 let decoded = decode_connect_error(&err).expect("decode connect error");
3188 let info = decoded.error_info.expect("error info");
3189 assert_eq!(info.reason, crate::stream::REASON_BATCH_EVICTED);
3190 assert_eq!(
3191 info.metadata
3192 .get(crate::stream::METADATA_OLDEST_RETAINED)
3193 .map(String::as_str),
3194 Some("2"),
3195 );
3196 }
3197
3198 #[tokio::test]
3199 async fn malformed_first_replay_batch_fails_subscription_setup() {
3200 let engine = Arc::new(MalformedLog {
3201 current_sequence: 2,
3202 malformed: 1,
3203 });
3204 let notifier = Arc::new(ManualNotifier::new(2));
3205 let connect = StreamConnect::new(StreamState::new(engine, notifier));
3206
3207 let Err(err) = subscribe_stream(&connect, Some(1)).await else {
3208 panic!("malformed first replay batch must fail setup");
3209 };
3210 assert_eq!(err.code, connectrpc::ErrorCode::Internal);
3211 }
3212
3213 #[tokio::test]
3214 async fn malformed_later_replay_batch_terminates_established_stream() {
3215 let engine = Arc::new(MalformedLog {
3216 current_sequence: 2,
3217 malformed: 2,
3218 });
3219 let notifier = Arc::new(ManualNotifier::new(2));
3220 let connect = StreamConnect::new(StreamState::new(engine, notifier));
3221 let mut stream = subscribe_stream(&connect, Some(1))
3222 .await
3223 .expect("subscribe");
3224
3225 let first = stream.next().await.unwrap().unwrap();
3226 assert_eq!(first.sequence_number, 1);
3227
3228 let err = stream
3229 .next()
3230 .await
3231 .unwrap()
3232 .expect_err("malformed later batch must surface on the stream");
3233 assert_eq!(err.code, connectrpc::ErrorCode::Internal);
3234 assert!(
3235 stream.next().await.is_none(),
3236 "stream must terminate after the decode failure",
3237 );
3238 }
3239
3240 #[tokio::test]
3241 async fn all_replay_batches_filtered_advance_silently_to_live() {
3242 let engine = Arc::new(FakeEngine::default());
3243 engine.set_current_sequence(3);
3244 engine.set_oldest_retained(Some(1));
3245 for seq in 1..=3 {
3246 engine.set_batch(seq, Some(vec![nonmatching_kv(b"skip", &[seq as u8])]));
3247 }
3248
3249 let state = AppState::new(engine.clone());
3250 let connect = StreamConnect::new(state.clone());
3251 let mut stream = subscribe_stream(&connect, Some(1))
3252 .await
3253 .expect("subscribe");
3254
3255 engine.publish_live(state.stream.clone(), 4, vec![matching_kv(b"live", b"v4")]);
3256 let frame = tokio::time::timeout(Duration::from_secs(1), stream.next())
3257 .await
3258 .expect("stream should yield")
3259 .expect("frame should exist")
3260 .expect("frame should be ok");
3261 assert_eq!(frame.sequence_number, 4);
3262 assert_eq!(frame.entries[0].value.as_ref(), b"v4");
3263 }
3264
3265 #[tokio::test]
3266 async fn replay_lookahead_is_bounded_and_emits_in_order() {
3267 let last_sequence = SUBSCRIBE_GET_BATCH_LOOKAHEAD as u64 + 1;
3268 let engine = Arc::new(GatedLog::default());
3269 for sequence_number in 1..=last_sequence {
3270 engine.set_batch(
3271 sequence_number,
3272 vec![matching_kv(b"replay", &[sequence_number as u8])],
3273 );
3274 }
3275 for sequence_number in 2..=last_sequence {
3276 engine.gate(sequence_number);
3277 }
3278
3279 let notifier = Arc::new(ManualNotifier::new(last_sequence));
3280 let connect = StreamConnect::new(StreamState::new(engine.clone(), notifier));
3281 let mut stream = subscribe_stream(&connect, Some(1))
3282 .await
3283 .expect("subscribe");
3284
3285 assert_eq!(engine.started_sequences(), vec![1]);
3286
3287 let (sender, mut frames) = tokio::sync::mpsc::unbounded_channel();
3288 let reader = tokio::spawn(async move {
3289 while let Some(frame) = stream.next().await {
3290 if sender.send(frame).is_err() {
3291 return;
3292 }
3293 }
3294 });
3295
3296 assert_eq!(next_subscribe_frame(&mut frames).await.sequence_number, 1);
3297 engine.wait_for_started(last_sequence as usize).await;
3298
3299 assert_eq!(engine.in_flight(), SUBSCRIBE_GET_BATCH_LOOKAHEAD);
3300 assert_eq!(engine.max_in_flight(), SUBSCRIBE_GET_BATCH_LOOKAHEAD);
3301 assert_eq!(
3302 engine.started_sequences(),
3303 (1..=last_sequence).collect::<Vec<_>>()
3304 );
3305
3306 for sequence_number in (3..=last_sequence).rev() {
3307 engine.release(sequence_number);
3308 }
3309 assert!(
3310 tokio::time::timeout(Duration::from_millis(50), frames.recv())
3311 .await
3312 .is_err(),
3313 "later batches must wait for the first pending batch",
3314 );
3315
3316 engine.release(2);
3317 for expected in 2..=last_sequence {
3318 assert_eq!(
3319 next_subscribe_frame(&mut frames).await.sequence_number,
3320 expected
3321 );
3322 }
3323 assert_eq!(engine.get_count(1), 1);
3324
3325 reader.abort();
3326 let _ = reader.await;
3327 }
3328
3329 #[tokio::test]
3330 async fn replay_lookahead_refills_after_filtered_batches() {
3331 let last_sequence = SUBSCRIBE_GET_BATCH_LOOKAHEAD as u64 + 2;
3332 let engine = Arc::new(GatedLog::default());
3333 for sequence_number in 1..=last_sequence {
3334 let kv = if sequence_number == last_sequence {
3335 matching_kv(b"match", &[sequence_number as u8])
3336 } else {
3337 nonmatching_kv(b"skip", &[sequence_number as u8])
3338 };
3339 engine.set_batch(sequence_number, vec![kv]);
3340 }
3341
3342 let notifier = Arc::new(ManualNotifier::new(last_sequence));
3343 let connect = StreamConnect::new(StreamState::new(engine.clone(), notifier));
3344 let mut stream = subscribe_stream(&connect, Some(1))
3345 .await
3346 .expect("subscribe");
3347
3348 let last = stream.next().await.unwrap().unwrap();
3349 assert_eq!(last.sequence_number, last_sequence);
3350
3351 for sequence_number in 1..=last_sequence {
3352 assert_eq!(engine.get_count(sequence_number), 1);
3353 }
3354 }
3355
3356 #[derive(Default)]
3357 struct GatedFilteredLog {
3358 inner: GatedLog,
3359 }
3360
3361 impl Sequence for GatedFilteredLog {
3362 fn current_sequence(&self) -> u64 {
3363 self.inner.current_sequence()
3364 }
3365 }
3366
3367 impl Log for GatedFilteredLog {
3368 async fn get_batch(&self, _sequence_number: u64) -> Result<Option<LogBatch>, String> {
3369 Err("subscribe must not use the unfiltered read".into())
3370 }
3371
3372 async fn get_batch_filtered(
3373 &self,
3374 sequence_number: u64,
3375 matchers: &crate::stream::CompiledMatchers,
3376 ) -> Result<Option<FilteredBatch>, String> {
3377 let Some(batch) = self.inner.get_batch(sequence_number).await? else {
3378 return Ok(None);
3379 };
3380 let response = batch.decode_response()?;
3381 let entries = crate::stream::apply_filter(matchers, response.entries);
3382 Ok(Some(FilteredBatch::from_entries(entries)))
3383 }
3384
3385 async fn oldest_retained_batch(&self) -> Result<Option<u64>, String> {
3386 self.inner.oldest_retained_batch().await
3387 }
3388 }
3389
3390 #[tokio::test]
3391 async fn custom_filtered_override_keeps_lookahead_bound_and_order() {
3392 let last_sequence = SUBSCRIBE_GET_BATCH_LOOKAHEAD as u64 + 1;
3393 let engine = Arc::new(GatedFilteredLog::default());
3394 for sequence_number in 1..=last_sequence {
3395 engine.inner.set_batch(
3396 sequence_number,
3397 vec![matching_kv(b"replay", &[sequence_number as u8])],
3398 );
3399 }
3400 for sequence_number in 2..=last_sequence {
3401 engine.inner.gate(sequence_number);
3402 }
3403
3404 let notifier = Arc::new(ManualNotifier::new(last_sequence));
3405 let connect = StreamConnect::new(StreamState::new(engine.clone(), notifier));
3406 let mut stream = subscribe_stream(&connect, Some(1))
3407 .await
3408 .expect("subscribe");
3409
3410 let (sender, mut frames) = tokio::sync::mpsc::unbounded_channel();
3411 let reader = tokio::spawn(async move {
3412 while let Some(frame) = stream.next().await {
3413 if sender.send(frame).is_err() {
3414 return;
3415 }
3416 }
3417 });
3418
3419 assert_eq!(next_subscribe_frame(&mut frames).await.sequence_number, 1);
3420 engine.inner.wait_for_started(last_sequence as usize).await;
3421 assert_eq!(engine.inner.in_flight(), SUBSCRIBE_GET_BATCH_LOOKAHEAD);
3422 assert_eq!(engine.inner.max_in_flight(), SUBSCRIBE_GET_BATCH_LOOKAHEAD);
3423
3424 for sequence_number in (2..=last_sequence).rev() {
3427 engine.inner.release(sequence_number);
3428 }
3429 for expected in 2..=last_sequence {
3430 assert_eq!(
3431 next_subscribe_frame(&mut frames).await.sequence_number,
3432 expected
3433 );
3434 }
3435 for sequence_number in 1..=last_sequence {
3436 assert_eq!(engine.inner.get_count(sequence_number), 1);
3437 }
3438
3439 reader.abort();
3440 let _ = reader.await;
3441 }
3442
3443 #[tokio::test]
3444 async fn replay_lookahead_emits_earliest_error_and_terminates() {
3445 let engine = Arc::new(GatedLog::default());
3446 engine.set_batch(1, vec![matching_kv(b"first", b"v1")]);
3447 engine.set_batch(3, vec![matching_kv(b"later", b"v3")]);
3448 engine.set_error(2, "batch read failed");
3449 engine.gate(2);
3450
3451 let notifier = Arc::new(ManualNotifier::new(3));
3452 let connect = StreamConnect::new(StreamState::new(engine.clone(), notifier));
3453 let mut stream = subscribe_stream(&connect, Some(1))
3454 .await
3455 .expect("subscribe");
3456
3457 let first = stream.next().await.unwrap().unwrap();
3458 assert_eq!(first.sequence_number, 1);
3459 let mut next = Box::pin(stream.next());
3460 assert!(
3461 tokio::time::timeout(Duration::from_millis(50), &mut next)
3462 .await
3463 .is_err(),
3464 "later successes must wait behind an earlier error",
3465 );
3466 engine.wait_for_started(3).await;
3467 assert_eq!(engine.get_count(3), 1);
3468
3469 engine.release(2);
3470 let error = next.await.unwrap().expect_err("stream error");
3471 assert_eq!(error.code, connectrpc::ErrorCode::Internal);
3472 assert!(stream.next().await.is_none());
3473 }
3474
3475 #[tokio::test]
3476 async fn replay_to_live_catch_up_extends_bounded_lookahead() {
3477 const REPLAY_BOUND: u64 = 4;
3478
3479 let first_live_bound = REPLAY_BOUND + SUBSCRIBE_GET_BATCH_LOOKAHEAD as u64;
3480 let second_live_bound = first_live_bound + SUBSCRIBE_GET_BATCH_LOOKAHEAD as u64;
3481 let engine = Arc::new(GatedLog::default());
3482 for sequence_number in 1..=second_live_bound {
3483 engine.set_batch(
3484 sequence_number,
3485 vec![matching_kv(b"batch", &[sequence_number as u8])],
3486 );
3487 }
3488 engine.gate(2);
3489 for sequence_number in (REPLAY_BOUND + 1)..=second_live_bound {
3490 engine.gate(sequence_number);
3491 }
3492
3493 let notifier = Arc::new(ManualNotifier::new(REPLAY_BOUND));
3494 let connect = StreamConnect::new(StreamState::new(engine.clone(), notifier.clone()));
3495 let mut stream = subscribe_stream(&connect, Some(1))
3496 .await
3497 .expect("subscribe");
3498 let (sender, mut frames) = tokio::sync::mpsc::unbounded_channel();
3499 let reader = tokio::spawn(async move {
3500 while let Some(frame) = stream.next().await {
3501 if sender.send(frame).is_err() {
3502 return;
3503 }
3504 }
3505 });
3506
3507 assert_eq!(next_subscribe_frame(&mut frames).await.sequence_number, 1);
3508 engine.wait_for_started(REPLAY_BOUND as usize).await;
3509 engine.wait_for_gate_waits(2, 1).await;
3510
3511 notifier.advance(first_live_bound);
3512 engine.wake(2);
3513 engine.wait_for_gate_waits(2, 2).await;
3514 assert_eq!(
3515 engine.started_sequences(),
3516 (1..=REPLAY_BOUND).collect::<Vec<_>>()
3517 );
3518
3519 engine.release(2);
3520 for expected in 2..=REPLAY_BOUND {
3521 assert_eq!(
3522 next_subscribe_frame(&mut frames).await.sequence_number,
3523 expected
3524 );
3525 }
3526
3527 engine.wait_for_started(first_live_bound as usize).await;
3528 assert_eq!(engine.in_flight(), SUBSCRIBE_GET_BATCH_LOOKAHEAD);
3529
3530 notifier.advance(second_live_bound);
3531 for sequence_number in ((REPLAY_BOUND + 2)..=first_live_bound).rev() {
3532 engine.release(sequence_number);
3533 }
3534 assert!(
3535 tokio::time::timeout(Duration::from_millis(50), frames.recv())
3536 .await
3537 .is_err(),
3538 "live batches must remain ordered within the lookahead",
3539 );
3540 assert_eq!(engine.started_sequences().len(), first_live_bound as usize);
3541
3542 engine.release(REPLAY_BOUND + 1);
3543 for expected in (REPLAY_BOUND + 1)..=first_live_bound {
3544 assert_eq!(
3545 next_subscribe_frame(&mut frames).await.sequence_number,
3546 expected
3547 );
3548 }
3549
3550 engine.wait_for_started(second_live_bound as usize).await;
3551 assert_eq!(engine.in_flight(), SUBSCRIBE_GET_BATCH_LOOKAHEAD);
3552 assert_eq!(engine.max_in_flight(), SUBSCRIBE_GET_BATCH_LOOKAHEAD);
3553
3554 for sequence_number in ((first_live_bound + 2)..=second_live_bound).rev() {
3555 engine.release(sequence_number);
3556 }
3557 assert!(
3558 tokio::time::timeout(Duration::from_millis(50), frames.recv())
3559 .await
3560 .is_err(),
3561 "batches read past the old frontier must preserve batch order",
3562 );
3563
3564 engine.release(first_live_bound + 1);
3565 for expected in (first_live_bound + 1)..=second_live_bound {
3566 assert_eq!(
3567 next_subscribe_frame(&mut frames).await.sequence_number,
3568 expected
3569 );
3570 }
3571
3572 reader.abort();
3573 let _ = reader.await;
3574 }
3575
3576 #[tokio::test]
3577 async fn live_lookahead_extends_while_earlier_reads_are_in_flight() {
3578 let last_sequence = 12u64;
3579 let engine = Arc::new(GatedLog::default());
3580 for sequence_number in 3..=last_sequence {
3581 engine.set_batch(
3582 sequence_number,
3583 vec![matching_kv(b"live", &[sequence_number as u8])],
3584 );
3585 engine.gate(sequence_number);
3586 }
3587
3588 let notifier = Arc::new(ManualNotifier::new(2));
3589 let connect = StreamConnect::new(StreamState::new(engine.clone(), notifier.clone()));
3590 let mut stream = subscribe_stream(&connect, None).await.expect("subscribe");
3591 let (sender, mut frames) = tokio::sync::mpsc::unbounded_channel();
3592 let reader = tokio::spawn(async move {
3593 while let Some(frame) = stream.next().await {
3594 if sender.send(frame).is_err() {
3595 return;
3596 }
3597 }
3598 });
3599
3600 notifier.advance(4);
3601 engine.wait_for_started(2).await;
3602 assert_eq!(engine.in_flight(), 2);
3603
3604 notifier.advance(last_sequence);
3607 engine.wait_for_started(SUBSCRIBE_GET_BATCH_LOOKAHEAD).await;
3608 assert_eq!(engine.in_flight(), SUBSCRIBE_GET_BATCH_LOOKAHEAD);
3609 assert_eq!(
3610 engine.started_sequences(),
3611 (3..=2 + SUBSCRIBE_GET_BATCH_LOOKAHEAD as u64).collect::<Vec<_>>()
3612 );
3613
3614 for sequence_number in 3..=last_sequence {
3615 engine.release(sequence_number);
3616 }
3617 for expected in 3..=last_sequence {
3618 assert_eq!(
3619 next_subscribe_frame(&mut frames).await.sequence_number,
3620 expected
3621 );
3622 }
3623 assert_eq!(engine.max_in_flight(), SUBSCRIBE_GET_BATCH_LOOKAHEAD);
3624
3625 reader.abort();
3626 let _ = reader.await;
3627 }
3628
3629 #[tokio::test]
3630 async fn subscribe_past_end_reads_only_future_live_batches() {
3631 let engine = Arc::new(FakeEngine::default());
3632 engine.set_current_sequence(5);
3633 for seq in 1..=5 {
3634 engine.set_batch(seq, Some(vec![matching_kv(b"seed", b"v")]));
3635 }
3636 let state = AppState::new(engine.clone());
3637 let connect = StreamConnect::new(state.clone());
3638 let mut stream = subscribe_stream(&connect, Some(15))
3639 .await
3640 .expect("subscribe");
3641
3642 assert!(
3643 tokio::time::timeout(Duration::from_millis(200), stream.next())
3644 .await
3645 .is_err(),
3646 "past-end cursor should not replay synthetic or historical frames",
3647 );
3648
3649 engine.publish_live(state.stream.clone(), 6, vec![matching_kv(b"live", b"n")]);
3650 let frame = tokio::time::timeout(Duration::from_secs(1), stream.next())
3651 .await
3652 .expect("stream should yield")
3653 .expect("frame should exist")
3654 .expect("frame should be ok");
3655 assert_eq!(frame.sequence_number, 6);
3656 assert_eq!(frame.entries.len(), 1);
3657 assert_eq!(frame.entries[0].value.as_ref(), b"n");
3658 }
3659
3660 #[tokio::test]
3661 async fn replay_hole_returns_batch_evicted_error_instead_of_empty_frame() {
3662 let engine = Arc::new(FakeEngine::default());
3663 engine.set_current_sequence(3);
3664 engine.set_oldest_retained(Some(2));
3665 engine.set_batch(2, Some(vec![matching_kv(b"replay", b"v2")]));
3666
3667 let state = AppState::new(engine);
3668 let connect = StreamConnect::new(state);
3669 let mut stream = subscribe_stream(&connect, Some(2))
3670 .await
3671 .expect("subscribe");
3672
3673 let first = tokio::time::timeout(Duration::from_secs(1), stream.next())
3674 .await
3675 .expect("stream should yield")
3676 .expect("first replay frame should exist")
3677 .expect("first replay frame should be ok");
3678 assert_eq!(first.sequence_number, 2);
3679 assert_eq!(first.entries.len(), 1);
3680
3681 let err = tokio::time::timeout(Duration::from_secs(1), stream.next())
3682 .await
3683 .expect("stream should yield error")
3684 .expect("error item should exist")
3685 .expect_err("replay hole must be surfaced as an error");
3686 let decoded = decode_connect_error(&err).expect("decode connect error");
3687 assert_eq!(
3688 decoded.error_info.expect("error info").reason,
3689 crate::stream::REASON_BATCH_EVICTED,
3690 );
3691 assert!(
3692 tokio::time::timeout(Duration::from_secs(1), stream.next())
3693 .await
3694 .expect("stream should terminate")
3695 .is_none(),
3696 "stream must terminate after surfacing the replay hole",
3697 );
3698 }
3699
3700 #[tokio::test]
3701 async fn replay_with_live_burst_under_capacity_still_delivers_in_order() {
3702 const REPLAY_BATCHES: u64 = 100;
3703
3704 let engine = Arc::new(FakeEngine::default());
3705 engine.set_current_sequence(REPLAY_BATCHES);
3706 engine.set_oldest_retained(Some(1));
3707 for seq in 1..=REPLAY_BATCHES {
3708 engine.set_batch(seq, Some(vec![matching_kv(b"replay", b"v")]));
3709 }
3710
3711 let state = AppState::new(engine.clone());
3712 engine.publish_on_every_get_batch(
3713 state.stream.clone(),
3714 REPLAY_BATCHES,
3715 vec![matching_kv(b"live", b"tail")],
3716 );
3717
3718 let connect = StreamConnect::new(state);
3719 let mut stream = subscribe_stream(&connect, Some(1))
3720 .await
3721 .expect("subscribe");
3722 let mut sequence_numbers = Vec::with_capacity((REPLAY_BATCHES * 2) as usize);
3723 while sequence_numbers.len() < (REPLAY_BATCHES * 2) as usize {
3724 let frame = tokio::time::timeout(Duration::from_secs(2), stream.next())
3725 .await
3726 .expect("stream should keep yielding")
3727 .expect("frame should exist")
3728 .expect("frame should be ok");
3729 sequence_numbers.push(frame.sequence_number);
3730 }
3731
3732 let expected: Vec<u64> = (1..=(REPLAY_BATCHES * 2)).collect();
3733 assert_eq!(sequence_numbers, expected);
3734 }
3735
3736 #[tokio::test]
3737 async fn replay_large_live_burst_is_paced_by_client_reads() {
3738 const REPLAY_BATCHES: u64 = 300;
3739
3740 let engine = Arc::new(FakeEngine::default());
3741 engine.set_current_sequence(REPLAY_BATCHES);
3742 engine.set_oldest_retained(Some(1));
3743 for seq in 1..=REPLAY_BATCHES {
3744 engine.set_batch(seq, Some(vec![matching_kv(b"replay", b"v")]));
3745 }
3746
3747 let state = AppState::new(engine.clone());
3748 engine.publish_on_every_get_batch(
3749 state.stream.clone(),
3750 REPLAY_BATCHES,
3751 vec![matching_kv(b"live", b"tail")],
3752 );
3753
3754 let connect = StreamConnect::new(state);
3755 let mut stream = subscribe_stream(&connect, Some(1))
3756 .await
3757 .expect("subscribe");
3758 let mut sequence_numbers = Vec::with_capacity((REPLAY_BATCHES * 2) as usize);
3759 while sequence_numbers.len() < (REPLAY_BATCHES * 2) as usize {
3760 let frame = tokio::time::timeout(Duration::from_secs(2), stream.next())
3761 .await
3762 .expect("stream should keep yielding")
3763 .expect("frame should exist")
3764 .expect("frame should be ok");
3765 sequence_numbers.push(frame.sequence_number);
3766 }
3767 let expected: Vec<u64> = (1..=(REPLAY_BATCHES * 2)).collect();
3768 assert_eq!(sequence_numbers, expected);
3769 }
3770}