Skip to main content

exoware_server/
connect.rs

1//! Ingest, query, prune, retention, and stream services; storage is provided by capability
2//! traits.
3
4#![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
64// TODO (#57): Make limits configurable.
65const 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;
68// Per-subscription bound on concurrent subscribe log reads. Each slot can pin
69// a fully materialized batch (in flight, or completed but held for in-order
70// delivery), so server-wide memory and engine read pressure scale with this
71// bound times the subscriber count. Engines stay responsible for protecting
72// their own read path (for example via caching or request collapsing).
73const 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
154/// All-in-one single-process composition for a backend that serves every store capability.
155/// Split deployments construct the narrower capability states directly.
156pub struct AppState<E> {
157    /// Backend that implements every store capability.
158    pub engine: Arc<E>,
159    /// Limits enforced by the ingest service before writing.
160    pub ingest_limits: IngestLimits,
161    /// Gates ingest (writes) only. The read and administrative services remain available during
162    /// drains so that in-flight reads can complete while the worker sheds write traffic.
163    pub ready: Arc<AtomicBool>,
164    /// Shared fan-out hub for `log.stream.v1.Subscribe`.
165    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
199/// State for an ingest-only service.
200pub struct IngestState<I> {
201    /// Backend used for writes.
202    pub ingest: Arc<I>,
203    /// Limits enforced before writes reach the backend.
204    pub limits: IngestLimits,
205    /// Gates ingest writes only.
206    pub ready: Arc<AtomicBool>,
207    /// Optional live-stream notifier.
208    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
261/// State for a query-only service.
262pub struct QueryState<Q> {
263    /// Backend used for point and range reads.
264    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    /// Creates a query service with DataFusion's shared, unbounded memory pool.
282    ///
283    /// Group state must fit in memory by default. Supply a native runtime with a
284    /// bounded pool through [`Self::with_runtime`] to enable native spilling.
285    /// The pool excludes transient input, backend, transport, and client buffers.
286    pub fn new(query: Arc<Q>) -> Self {
287        Self {
288            query,
289            context: Arc::default(),
290        }
291    }
292
293    /// Supplies the worker's shared native memory pool and spill configuration.
294    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
309/// State for a prune-only service.
310pub struct PruneState<P> {
311    /// Backend used for prune requests.
312    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
340/// State for a retention-only service.
341pub struct RetentionState<R> {
342    /// Backend that owns the sequence-log retention rule.
343    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
371/// State for a stream-only service.
372pub struct StreamState<L> {
373    /// Backend used to load committed batches.
374    pub log: Arc<L>,
375    /// In-process notifier used to wake subscribers after new batches commit.
376    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
429/// Backoff floor advertised to `RetryInfo`-aware clients for transient store conditions.
430const RETRY_HINT_DELAY: std::time::Duration = std::time::Duration::from_secs(1);
431/// `ErrorInfo.reason` when the ingest worker has not passed its readiness gate.
432const REASON_WORKER_NOT_READY: &str = "WORKER_NOT_READY";
433/// `ErrorInfo.reason` when the backend discovers a transient ingest write failure.
434const REASON_INGEST_UNAVAILABLE: &str = "INGEST_UNAVAILABLE";
435
436/// Attaches an explicit retry hint for "come back soon" responses.
437fn 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        // Advance any attached stream frontier after the write is committed.
505        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
920// Keep each result paired with its requested sequence number. Subscribe
921// frames use that number as the resume cursor instead of trusting sequence
922// metadata supplied by a custom filtered-read implementation.
923type 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                // The sequence stream is consumed in order, so only the first
941                // closure invocation observes the eagerly fetched batch and it
942                // pairs with the first sequence.
943                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
960// Live sequences are generated on demand and re-check the published frontier
961// on every pull. The lookahead therefore keeps extending through new commits
962// while earlier reads drain, instead of stopping at a frontier snapshot. When
963// the subscriber is caught up the next pull parks on a publish notification,
964// which keeps one live stream serving the whole subscription.
965fn 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            // Re-check the frontier after arming the notifier so a publish
976            // racing this pull is not lost.
977            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    // Both streams are dropped at termination so buffered batches free at the
1002    // failure boundary instead of when the client tears the stream down.
1003    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                // The live sequence source only ends past u64::MAX, so an
1040                // exhausted live stream ends the subscription.
1041                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        // Prefix length is validated when compile_matchers runs the shared
1105        // stream_filter::validate_filter over the assembled filter.
1106        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        // Snapshot the published frontier to bound replay and subscribe for
1145        // live wakeups. Bounded lookahead overlaps log reads while retaining
1146        // client-driven backpressure.
1147        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        // Optional replay. The starting batch is read and filtered eagerly, so
1153        // an already-evicted cursor and a first batch that fails to decode both
1154        // fail the RPC at setup. Later replay holes and decode failures surface
1155        // on the established stream itself so callers reconnect from a safe
1156        // point instead of silently continuing.
1157        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                // Distinguish "never existed" (seq > current) vs "evicted".
1220                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        // Parse the wire shape, then authoritatively validate: buf.validate
1269        // annotations on the proto are documentation, so the handler enforces
1270        // the rule (e.g. keep_latest count > 0), mirroring the Prune handler.
1271        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        // A Keys scope without its required selector fails to parse into a
2367        // domain policy, so the handler rejects it before reaching the engine.
2368        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        // The floor is whatever the backend reports after one enforcement.
2410        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        // Validation is authoritative, so the backend is never touched.
2428        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        // Clearing keeps enforcement off; no floor exists so none is returned.
2442        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        // Both the gate and a backend-discovered outage surface `unavailable`; the reason string is
2596        // what keeps them distinguishable, so pin it against drift toward `INGEST_UNAVAILABLE`.
2597        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        // Completions land in reverse order to prove delivery stays sequence
3425        // ordered and duplicate free through the override path.
3426        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        // Reads past the old frontier must start while 3 and 4 are still in
3605        // flight, rather than after the pre-advance window fully drains.
3606        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}