Skip to main content

umadb_server/
lib.rs

1use futures::Stream;
2use std::fs;
3use std::path::Path;
4use std::pin::Pin;
5use std::sync::Arc;
6use std::sync::LazyLock;
7use std::thread;
8use std::time::Instant;
9use tokio::sync::{mpsc, oneshot, watch};
10use tokio_stream::wrappers::ReceiverStream;
11use tonic::transport::{Identity, ServerTlsConfig};
12use tonic::{Request, Response, Status, transport::Server};
13use umadb_core::db::{
14    DEFAULT_DB_FILENAME, DEFAULT_PAGE_SIZE, UmaDb, clone_dcb_error, is_integrity_error,
15    is_request_idempotent, read_conditional, shadow_for_batch_abort,
16};
17use umadb_core::mvcc::Mvcc;
18use umadb_dcb::{
19    DcbAppendCondition, DcbEvent, DcbQuery, DcbResult, DcbSequencedEvent, DcbError, TrackingInfo,
20};
21
22use tokio::runtime::Runtime;
23use tonic::codegen::http;
24use tonic::transport::server::TcpIncoming;
25use umadb_core::common::Position;
26
27use std::convert::Infallible;
28use std::future::Future;
29use std::task::{Context, Poll};
30use tonic::server::NamedService;
31use umadb_proto::status_from_dcb_error;
32
33// This is just to maintain compatibility for the very early unversioned API (pre-v1).
34#[derive(Clone, Debug)]
35pub struct PathRewriterService<S> {
36    inner: S,
37}
38
39impl<S> tower::Service<http::Request<tonic::body::Body>> for PathRewriterService<S>
40where
41    S: tower::Service<
42            http::Request<tonic::body::Body>,
43            Response = http::Response<tonic::body::Body>,
44            Error = Infallible,
45        > + Clone
46        + Send
47        + 'static,
48    S::Future: Send + 'static,
49{
50    type Response = S::Response;
51    type Error = S::Error;
52    type Future =
53        Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send + 'static>>;
54
55    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
56        self.inner.poll_ready(cx)
57    }
58
59    fn call(&mut self, mut req: http::Request<tonic::body::Body>) -> Self::Future {
60        let uri = req.uri().clone();
61        let path = uri.path();
62
63        // Check and rewrite the path string first
64        if path.starts_with("/umadb.UmaDBService/") {
65            let new_path_str = path.replace("/umadb.UmaDBService/", "/umadb.v1.DCB/");
66
67            // Use the existing authority and scheme if present, otherwise default to a simple path-only URI structure
68            // which is often safer than hardcoded hostnames in internal systems.
69            let new_uri = if let (Some(scheme), Some(authority)) = (uri.scheme(), uri.authority()) {
70                // If we have all components, try to build the full URI
71                http::Uri::builder()
72                    .scheme(scheme.clone())
73                    .authority(authority.clone())
74                    .path_and_query(new_path_str.as_str())
75                    .build()
76                    .ok() // Convert the final build Result into an Option
77            } else {
78                // Fallback for malformed requests (missing scheme/authority)
79                // Just try to build a path-only URI
80                new_path_str.parse::<http::Uri>().ok()
81            };
82
83            if let Some(final_uri) = new_uri {
84                *req.uri_mut() = final_uri;
85            } else {
86                eprintln!("failed to construct valid URI for path: {}", path);
87            }
88        }
89
90        let fut = self.inner.call(req);
91        Box::pin(fut)
92    }
93}
94
95// Add this implementation to satisfy the compiler error
96impl<S: NamedService> NamedService for PathRewriterService<S> {
97    const NAME: &'static str = S::NAME;
98}
99
100#[derive(Clone, Debug)]
101pub struct PathRewriterLayer;
102
103impl<S> tower::Layer<S> for PathRewriterLayer
104where
105    S: tower::Service<
106            http::Request<tonic::body::Body>,
107            Response = http::Response<tonic::body::Body>,
108            Error = Infallible,
109        > + Clone
110        + Send
111        + 'static,
112    S::Future: Send + 'static,
113{
114    type Service = PathRewriterService<S>;
115
116    fn layer(&self, inner: S) -> Self::Service {
117        PathRewriterService { inner }
118    }
119}
120
121static START_TIME: LazyLock<Instant> = LazyLock::new(Instant::now);
122
123const APPEND_BATCH_MAX_EVENTS: usize = 2000;
124const READ_RESPONSE_BATCH_SIZE_DEFAULT: u32 = 100;
125const READ_RESPONSE_BATCH_SIZE_MAX: u32 = 5000;
126
127// Optional TLS configuration helpers
128#[derive(Clone, Debug)]
129pub struct ServerTlsOptions {
130    pub cert_pem: Vec<u8>,
131    pub key_pem: Vec<u8>,
132}
133
134pub fn uptime() -> std::time::Duration {
135    START_TIME.elapsed()
136}
137
138fn build_server_builder_with_options(tls: Option<ServerTlsOptions>) -> Server {
139    use std::time::Duration;
140    let mut server_builder = Server::builder()
141        .http2_keepalive_interval(Some(Duration::from_secs(5)))
142        .http2_keepalive_timeout(Some(Duration::from_secs(10)))
143        .initial_stream_window_size(Some(4 * 1024 * 1024))
144        .initial_connection_window_size(Some(8 * 1024 * 1024))
145        .tcp_nodelay(true)
146        .concurrency_limit_per_connection(1024);
147
148    if let Some(opts) = tls {
149        let identity = Identity::from_pem(opts.cert_pem, opts.key_pem);
150        server_builder = server_builder
151            .tls_config(ServerTlsConfig::new().identity(identity))
152            .expect("failed to apply TLS config");
153    }
154
155    server_builder
156}
157
158// Function to start the gRPC server with a shutdown signal
159pub async fn start_server<P: AsRef<Path> + Send + 'static>(
160    path: P,
161    addr: &str,
162    shutdown_rx: oneshot::Receiver<()>,
163) -> Result<(), Box<dyn std::error::Error>> {
164    start_server_internal(path, addr, shutdown_rx, None, None).await
165}
166
167/// Start server with TLS using PEM-encoded cert and key.
168pub async fn start_server_secure<P: AsRef<Path> + Send + 'static>(
169    path: P,
170    addr: &str,
171    shutdown_rx: oneshot::Receiver<()>,
172    cert_pem: Vec<u8>,
173    key_pem: Vec<u8>,
174) -> Result<(), Box<dyn std::error::Error>> {
175    let tls = ServerTlsOptions { cert_pem, key_pem };
176    start_server_internal(path, addr, shutdown_rx, Some(tls), None).await
177}
178
179/// Convenience: load cert and key from filesystem paths
180pub async fn start_server_secure_from_files<
181    P: AsRef<Path> + Send + 'static,
182    CP: AsRef<Path>,
183    KP: AsRef<Path>,
184>(
185    path: P,
186    addr: &str,
187    shutdown_rx: oneshot::Receiver<()>,
188    cert_path: CP,
189    key_path: KP,
190) -> Result<(), Box<dyn std::error::Error>> {
191    let cert_path_ref = cert_path.as_ref();
192    let cert_pem = fs::read(cert_path_ref).map_err(|e| -> Box<dyn std::error::Error> {
193        format!(
194            "failed to open TLS certificate file '{}': {}",
195            cert_path_ref.display(),
196            e
197        )
198        .into()
199    })?;
200
201    let key_path_ref = key_path.as_ref();
202    let key_pem = fs::read(key_path_ref).map_err(|e| -> Box<dyn std::error::Error> {
203        format!(
204            "failed to open TLS key file '{}': {}",
205            key_path_ref.display(),
206            e
207        )
208        .into()
209    })?;
210    start_server_secure(path, addr, shutdown_rx, cert_pem, key_pem).await
211}
212
213/// Start server (insecure) requiring an API key for all RPCs.
214pub async fn start_server_with_api_key<P: AsRef<Path> + Send + 'static>(
215    path: P,
216    addr: &str,
217    shutdown_rx: oneshot::Receiver<()>,
218    api_key: String,
219) -> Result<(), Box<dyn std::error::Error>> {
220    start_server_internal(path, addr, shutdown_rx, None, Some(api_key)).await
221}
222
223/// Start TLS server requiring an API key for all RPCs.
224pub async fn start_server_secure_with_api_key<P: AsRef<Path> + Send + 'static>(
225    path: P,
226    addr: &str,
227    shutdown_rx: oneshot::Receiver<()>,
228    cert_pem: Vec<u8>,
229    key_pem: Vec<u8>,
230    api_key: String,
231) -> Result<(), Box<dyn std::error::Error>> {
232    let tls = ServerTlsOptions { cert_pem, key_pem };
233    start_server_internal(path, addr, shutdown_rx, Some(tls), Some(api_key)).await
234}
235
236/// TLS server from files requiring an API key
237pub async fn start_server_secure_from_files_with_api_key<
238    P: AsRef<Path> + Send + 'static,
239    CP: AsRef<Path>,
240    KP: AsRef<Path>,
241>(
242    path: P,
243    addr: &str,
244    shutdown_rx: oneshot::Receiver<()>,
245    cert_path: CP,
246    key_path: KP,
247    api_key: String,
248) -> Result<(), Box<dyn std::error::Error>> {
249    let cert_path_ref = cert_path.as_ref();
250    let cert_pem = fs::read(cert_path_ref).map_err(|e| -> Box<dyn std::error::Error> {
251        format!(
252            "failed to open TLS certificate file '{}': {}",
253            cert_path_ref.display(),
254            e
255        )
256        .into()
257    })?;
258
259    let key_path_ref = key_path.as_ref();
260    let key_pem = fs::read(key_path_ref).map_err(|e| -> Box<dyn std::error::Error> {
261        format!(
262            "failed to open TLS key file '{}': {}",
263            key_path_ref.display(),
264            e
265        )
266        .into()
267    })?;
268    start_server_secure_with_api_key(path, addr, shutdown_rx, cert_pem, key_pem, api_key).await
269}
270
271async fn start_server_internal<P: AsRef<Path> + Send + 'static>(
272    path: P,
273    addr: &str,
274    shutdown_rx: oneshot::Receiver<()>,
275    tls: Option<ServerTlsOptions>,
276    api_key: Option<String>,
277) -> Result<(), Box<dyn std::error::Error>> {
278    let addr = addr.parse()?;
279    // ---- Bind incoming manually like tonic ----
280    let incoming = match TcpIncoming::bind(addr) {
281        Ok(incoming) => incoming,
282        Err(err) => {
283            return Err(Box::new(DcbError::InitializationError(format!(
284                "failed to bind to address {}: {}",
285                addr, err
286            ))));
287        }
288    }
289    .with_nodelay(Some(true))
290    .with_keepalive(Some(std::time::Duration::from_secs(60)));
291
292    // Create a shutdown broadcast channel for terminating ongoing subscriptions
293    let (srv_shutdown_tx, srv_shutdown_rx) = watch::channel(false);
294    let dcb_server =
295        match DcbServer::new(path.as_ref().to_owned(), srv_shutdown_rx, api_key.clone()) {
296            Ok(server) => server,
297            Err(err) => {
298                return Err(Box::new(err));
299            }
300        };
301
302        println!(
303            "UmaDB has {:?} events",
304            dcb_server.request_handler.head().unwrap_or(Some(0)).unwrap_or(0)
305        );
306    let tls_mode_display_str = if tls.is_some() {
307        "with TLS"
308    } else {
309        "without TLS"
310    };
311
312    let api_key_display_str = if api_key.is_some() {
313        "with API key"
314    } else {
315        "without API key"
316    };
317
318    // gRPC Health service setup
319    use tonic_health::ServingStatus; // server API expects this enum
320    let (health_reporter, health_service) = tonic_health::server::health_reporter();
321    // Set overall and service-specific health to SERVING
322    health_reporter
323        .set_service_status("", ServingStatus::Serving)
324        .await;
325    health_reporter
326        .set_service_status("umadb.v1.DCB", ServingStatus::Serving)
327        .await;
328    let health_reporter_for_shutdown = health_reporter.clone();
329
330    // Apply PathRewriterLayer at the server level to intercept all requests before routing
331    let mut builder = build_server_builder_with_options(tls)
332        .layer(PathRewriterLayer)
333        .add_service(health_service);
334
335    // Add DCB service (auth enforced inside RPC handlers if configured)
336    builder = builder.add_service(dcb_server.into_service());
337    let router = builder;
338
339    println!("UmaDB is listening on {addr} ({tls_mode_display_str}, {api_key_display_str})");
340    println!("UmaDB started in {:?}", uptime());
341    // let incoming = router.server.bind_incoming();
342    router
343        .serve_with_incoming_shutdown(incoming, async move {
344            // Wait for an external shutdown trigger
345            let _ = shutdown_rx.await;
346            // Mark health as NOT_SERVING before shutdown
347            let _ = health_reporter_for_shutdown
348                .set_service_status("", ServingStatus::NotServing)
349                .await;
350            let _ = health_reporter_for_shutdown
351                .set_service_status("umadb.v1.DCB", ServingStatus::NotServing)
352                .await;
353            // Broadcast shutdown to all subscription tasks
354            let _ = srv_shutdown_tx.send(true);
355            println!("UmaDB server shutdown complete");
356        })
357        .await?;
358
359    Ok(())
360}
361
362// gRPC server implementation
363pub struct DcbServer {
364    pub(crate) request_handler: RequestHandler,
365    shutdown_watch_rx: watch::Receiver<bool>,
366    api_key: Option<String>,
367}
368
369impl DcbServer {
370    pub fn new<P: AsRef<Path> + Send + 'static>(
371        path: P,
372        shutdown_rx: watch::Receiver<bool>,
373        api_key: Option<String>,
374    ) -> DcbResult<Self> {
375        let command_handler = RequestHandler::new(path)?;
376        Ok(Self {
377            request_handler: command_handler,
378            shutdown_watch_rx: shutdown_rx,
379            api_key,
380        })
381    }
382
383    pub fn into_service(self) -> umadb_proto::v1::dcb_server::DcbServer<Self> {
384        umadb_proto::v1::dcb_server::DcbServer::new(self)
385    }
386
387    fn enforce_api_key(&self, metadata: &tonic::metadata::MetadataMap) -> Result<(), Status> {
388        if let Some(expected) = &self.api_key {
389            let auth = metadata.get("authorization");
390            let expected_val = format!("Bearer {}", expected);
391            let ok = auth
392                .and_then(|m| m.to_str().ok())
393                .map(|s| s == expected_val)
394                .unwrap_or(false);
395            if !ok {
396                return Err(status_from_dcb_error(DcbError::AuthenticationError(
397                    "missing or invalid API key".to_string(),
398                )));
399            }
400        }
401        Ok(())
402    }
403}
404
405#[tonic::async_trait]
406impl umadb_proto::v1::dcb_server::Dcb for DcbServer {
407    type ReadStream =
408        Pin<Box<dyn Stream<Item = Result<umadb_proto::v1::ReadResponse, Status>> + Send + 'static>>;
409    type SubscribeStream = Pin<
410        Box<dyn Stream<Item = Result<umadb_proto::v1::SubscribeResponse, Status>> + Send + 'static>,
411    >;
412
413    async fn read(
414        &self,
415        request: Request<umadb_proto::v1::ReadRequest>,
416    ) -> Result<Response<Self::ReadStream>, Status> {
417        // Enforce API key if configured
418        self.enforce_api_key(request.metadata())?;
419        let read_request = request.into_inner();
420
421        // Convert protobuf query to DCB types
422        let mut query: Option<DcbQuery> = read_request.query.map(|q| q.into());
423        let start = read_request.start;
424        let backwards = read_request.backwards.unwrap_or(false);
425        let limit = read_request.limit;
426        // Cap requested batch size.
427        let batch_size = read_request
428            .batch_size
429            .unwrap_or(READ_RESPONSE_BATCH_SIZE_DEFAULT)
430            .clamp(1, READ_RESPONSE_BATCH_SIZE_MAX);
431        let subscribe = read_request.subscribe.unwrap_or(false);
432
433        // Create a channel for streaming responses (deeper buffer to reduce backpressure under concurrency)
434        let (tx, rx) = mpsc::channel(2048);
435        // Clone the request handler.
436        let request_handler = self.request_handler.clone();
437        // Clone the shutdown watch receiver.
438        let mut shutdown_watch_rx = self.shutdown_watch_rx.clone();
439
440        // Spawn a task to handle the read operation and stream multiple batches
441        tokio::spawn(async move {
442            // Ensure we can reuse the same query across batches
443            let query_clone = query.take();
444            let mut next_start = start;
445            let mut sent_any = false;
446            let mut remaining_limit = limit.unwrap_or(u32::MAX);
447            // Create a watch receiver for head updates (for subscriptions)
448            // TODO: Make this an Option and only do this for subscriptions?
449            let mut head_rx = request_handler.watch_head();
450            // If non-subscription read, capture head to preserve point-in-time semantics
451            let captured_head = if !subscribe {
452                let handler = request_handler.clone();
453                tokio::task::spawn_blocking(move || handler.head())
454                    .await
455                    .unwrap_or(Ok(None))
456                    .unwrap_or(None)
457            } else {
458                None
459            };
460            loop {
461                // If this is a subscription, exit if the client
462                // has gone away or the server is shutting down.
463                if subscribe {
464                    if tx.is_closed() {
465                        break;
466                    }
467                    if *shutdown_watch_rx.borrow() {
468                        break;
469                    }
470                }
471                // Determine per-iteration limit.
472                let read_limit = remaining_limit.min(batch_size);
473                // If subscription and remaining exhausted (limit reached), terminate
474                if subscribe && limit.is_some() && remaining_limit == 0 {
475                    break;
476                }
477                let handler = request_handler.clone();
478                let query_val = query_clone.clone();
479                let limit_val = Some(read_limit);
480                match tokio::task::spawn_blocking(move || {
481                    handler.read(query_val, next_start, backwards, limit_val)
482                })
483                .await
484                .map_err(|e| DcbError::InternalError(e.to_string()))
485                .and_then(|res| res)
486                {
487                    Ok((dcb_sequenced_events, head)) => {
488                        // Capture the original length before consuming events
489                        let original_len = dcb_sequenced_events.len();
490
491                        // Filter and map events, discarding those with position > captured_head
492                        let sequenced_event_protos: Vec<umadb_proto::v1::SequencedEvent> =
493                            dcb_sequenced_events
494                                .into_iter()
495                                .filter(|e| {
496                                    if let Some(h) = captured_head {
497                                        e.position <= h
498                                    } else {
499                                        true
500                                    }
501                                })
502                                .map(umadb_proto::v1::SequencedEvent::from)
503                                .collect();
504
505                        let reached_captured_head = if captured_head.is_some() {
506                            // Check if we filtered out any events
507                            sequenced_event_protos.len() < original_len
508                        } else {
509                            false
510                        };
511
512                        // Calculate head to send based on context
513                        // For subscriptions: use current head
514                        // For unlimited non-subscription reads: use captured_head
515                        // For limited reads: use last event position (or current head if empty)
516                        let last_event_position = sequenced_event_protos.last().map(|e| e.position);
517                        let head_to_send = if subscribe {
518                            head
519                        } else if limit.is_none() {
520                            captured_head
521                        } else {
522                            last_event_position.or(head)
523                        };
524
525                        if sequenced_event_protos.is_empty() {
526                            // Only send an empty response to communicate head if this is the first
527                            if !sent_any {
528                                let response = umadb_proto::v1::ReadResponse {
529                                    events: vec![],
530                                    head: head_to_send,
531                                };
532                                let _ = tx.send(Ok(response)).await;
533                            }
534                            // For subscriptions, wait for new events instead of terminating
535                            if subscribe {
536                                // Wait for either a new head or a server shutdown signal
537                                tokio::select! {
538                                    _ = head_rx.changed() => {},
539                                    _ = shutdown_watch_rx.changed() => {},
540                                    _ = tx.closed() => {},
541                                }
542                                continue;
543                            }
544                            break;
545                        }
546
547                        // Capture values needed after sequenced_event_protos is moved
548                        let sent_count = sequenced_event_protos.len() as u32;
549
550                        let response = umadb_proto::v1::ReadResponse {
551                            events: sequenced_event_protos,
552                            head: head_to_send,
553                        };
554
555                        if tx.send(Ok(response)).await.is_err() {
556                            break;
557                        }
558                        sent_any = true;
559
560                        // Advance the cursor (use a new reader on the next loop iteration)
561                        next_start =
562                            last_event_position.map(|p| if !backwards { p + 1 } else { p - 1 });
563
564                        // Stop streaming further if we reached the
565                        // captured head boundary (non-subscriber only).
566                        if reached_captured_head && !subscribe {
567                            break;
568                        }
569
570                        // Decrease the remaining overall limit if any, and stop if reached
571                        if limit.is_some() {
572                            if remaining_limit <= sent_count {
573                                remaining_limit = 0;
574                            } else {
575                                remaining_limit -= sent_count;
576                            }
577                            if remaining_limit == 0 {
578                                break;
579                            }
580                        }
581
582                        // Yield to let other tasks progress under high concurrency
583                        tokio::task::yield_now().await;
584                    }
585                    Err(e) => {
586                        let _ = tx.send(Err(status_from_dcb_error(e))).await;
587                        break;
588                    }
589                }
590            }
591        });
592
593        // Return the receiver as a stream
594        Ok(Response::new(
595            Box::pin(ReceiverStream::new(rx)) as Self::ReadStream
596        ))
597    }
598
599    async fn subscribe(
600        &self,
601        request: Request<umadb_proto::v1::SubscribeRequest>,
602    ) -> Result<Response<Self::SubscribeStream>, Status> {
603        // Enforce API key if configured
604        self.enforce_api_key(request.metadata())?;
605        let subscribe_request = request.into_inner();
606
607        // Convert protobuf query to DCB types
608        let mut query: Option<DcbQuery> = subscribe_request.query.map(|q| q.into());
609        let after = subscribe_request.after;
610        // Cap requested batch size.
611        let batch_size = subscribe_request
612            .batch_size
613            .unwrap_or(READ_RESPONSE_BATCH_SIZE_DEFAULT)
614            .clamp(1, READ_RESPONSE_BATCH_SIZE_MAX);
615
616        // Create a channel for streaming responses
617        let (tx, rx) = mpsc::channel(2048);
618        // Clone the request handler.
619        let request_handler = self.request_handler.clone();
620        // Clone the shutdown watch receiver.
621        let mut shutdown_watch_rx = self.shutdown_watch_rx.clone();
622
623        // Spawn a task to handle the subscribe operation and stream multiple batches
624        tokio::spawn(async move {
625            // Ensure we can reuse the same query across batches
626            let query_clone = query.take();
627            // Todo: End the subscription if after is Some(u64:MAX).
628            let mut next_after = after.map(|a| a.saturating_add(1));
629            // Create a watch receiver for head updates
630            let mut head_rx = request_handler.watch_head();
631
632            loop {
633                // Exit if the client has gone away or the server is shutting down.
634                if tx.is_closed() {
635                    break;
636                }
637                if *shutdown_watch_rx.borrow() {
638                    break;
639                }
640
641                let handler = request_handler.clone();
642                let query_val = query_clone.clone();
643                let batch_size_val = Some(batch_size);
644                match tokio::task::spawn_blocking(move || {
645                    handler.read(query_val, next_after, false, batch_size_val)
646                })
647                .await
648                .map_err(|e| DcbError::InternalError(e.to_string()))
649                .and_then(|res| res)
650                {
651                    Ok((dcb_sequenced_events, _head)) => {
652                        // Map events to protobuf type
653                        let sequenced_event_protos: Vec<umadb_proto::v1::SequencedEvent> =
654                            dcb_sequenced_events
655                                .into_iter()
656                                .map(umadb_proto::v1::SequencedEvent::from)
657                                .collect();
658
659                        if sequenced_event_protos.is_empty() {
660                            // For subscriptions, wait for new events instead of terminating
661                            tokio::select! {
662                                _ = head_rx.changed() => {},
663                                _ = shutdown_watch_rx.changed() => {},
664                                _ = tx.closed() => {},
665                            }
666                            continue;
667                        }
668
669                        let last_event_position = sequenced_event_protos.last().map(|e| e.position);
670
671                        let response = umadb_proto::v1::SubscribeResponse {
672                            events: sequenced_event_protos,
673                        };
674
675                        if tx.send(Ok(response)).await.is_err() {
676                            break;
677                        }
678
679                        // Advance the cursor (use a new reader on the next loop iteration)
680                        // Todo: End the subscription if last_event_position is Some(u64:MAX).
681                        next_after = last_event_position.map(|p| p.saturating_add(1));
682
683                        // Yield to let other tasks progress under high concurrency
684                        tokio::task::yield_now().await;
685                    }
686                    Err(e) => {
687                        let _ = tx.send(Err(status_from_dcb_error(e))).await;
688                        break;
689                    }
690                }
691            }
692        });
693
694        // Return the receiver as a stream
695        Ok(Response::new(
696            Box::pin(ReceiverStream::new(rx)) as Self::SubscribeStream
697        ))
698    }
699
700    async fn append(
701        &self,
702        request: Request<umadb_proto::v1::AppendRequest>,
703    ) -> Result<Response<umadb_proto::v1::AppendResponse>, Status> {
704        // Enforce API key if configured
705        self.enforce_api_key(request.metadata())?;
706        let req = request.into_inner();
707
708        // Convert protobuf types to API types
709        let events: Vec<DcbEvent> = match req.events.into_iter().map(|e| e.try_into()).collect() {
710            Ok(events) => events,
711            Err(e) => {
712                return Err(status_from_dcb_error(e));
713            }
714        };
715        let condition = req.condition.map(|c| c.into());
716
717        // Call the event store append method
718        match self
719            .request_handler
720            .append(
721                events,
722                condition,
723                req.tracking_info.map(|t| TrackingInfo {
724                    source: t.source,
725                    position: t.position,
726                }),
727            )
728            .await
729        {
730            Ok(position) => Ok(Response::new(umadb_proto::v1::AppendResponse { position })),
731            Err(e) => Err(status_from_dcb_error(e)),
732        }
733    }
734
735    async fn head(
736        &self,
737        request: Request<umadb_proto::v1::HeadRequest>,
738    ) -> Result<Response<umadb_proto::v1::HeadResponse>, Status> {
739        // Enforce API key if configured
740        self.enforce_api_key(request.metadata())?;
741        // Call the event store head method
742        match self.request_handler.head() {
743            Ok(position) => {
744                // Return the position as a response
745                Ok(Response::new(umadb_proto::v1::HeadResponse { position }))
746            }
747            Err(e) => Err(status_from_dcb_error(e)),
748        }
749    }
750
751    async fn get_tracking_info(
752        &self,
753        request: Request<umadb_proto::v1::TrackingRequest>,
754    ) -> Result<Response<umadb_proto::v1::TrackingResponse>, Status> {
755        // Enforce API key if configured
756        self.enforce_api_key(request.metadata())?;
757        let req = request.into_inner();
758        match self.request_handler.get_tracking_info(req.source) {
759            Ok(position) => Ok(Response::new(umadb_proto::v1::TrackingResponse {
760                position,
761            })),
762            Err(e) => Err(status_from_dcb_error(e)),
763        }
764    }
765}
766
767// Message types for communication between the gRPC server and the request handler's writer thread
768enum WriterRequest {
769    Append {
770        events: Vec<DcbEvent>,
771        condition: Option<DcbAppendCondition>,
772        tracking_info: Option<TrackingInfo>,
773        response_tx: oneshot::Sender<DcbResult<u64>>,
774    },
775    Shutdown,
776}
777
778// Thread-safe request handler
779struct RequestHandler {
780    mvcc: Arc<Mvcc>,
781    head_watch_tx: watch::Sender<Option<u64>>,
782    writer_request_tx: mpsc::Sender<WriterRequest>,
783}
784
785impl RequestHandler {
786    fn new<P: AsRef<Path> + Send + 'static>(path: P) -> DcbResult<Self> {
787        // Create a channel for sending requests to the writer thread
788        let (request_tx, mut request_rx) = mpsc::channel::<WriterRequest>(1024);
789
790        // Build a shared Mvcc instance (Arc) upfront so reads can proceed concurrently
791        let p = path.as_ref();
792        let file_path = if p.is_dir() {
793            p.join(DEFAULT_DB_FILENAME)
794        } else {
795            p.to_path_buf()
796        };
797        let mvcc = Arc::new(Mvcc::new(&file_path, DEFAULT_PAGE_SIZE, false)?);
798
799        // Initialize the head watch channel with the current head.
800        let init_head = {
801            let (_, header) = mvcc.get_latest_header()?;
802            let last = header.next_position.0.saturating_sub(1);
803            if last == 0 { None } else { Some(last) }
804        };
805        let (head_tx, _head_rx) = watch::channel::<Option<u64>>(init_head);
806
807        // Spawn a thread for processing writer requests.
808        let mvcc_for_writer = mvcc.clone();
809        let head_tx_writer = head_tx.clone();
810        thread::spawn(move || {
811            let db = UmaDb::from_arc(mvcc_for_writer);
812
813            // Create a runtime for processing writer requests.
814            let rt = Runtime::new().unwrap();
815
816            // Process writer requests.
817            rt.block_on(async {
818                while let Some(request) = request_rx.recv().await {
819                    match request {
820                        WriterRequest::Append {
821                            events,
822                            condition,
823                            tracking_info,
824                            response_tx,
825                        } => {
826                            // Batch processing: drain any immediately available requests
827                            // let mut items: Vec<(Vec<DCBEvent>, Option<DCBAppendCondition>)> =
828                            //     Vec::new();
829
830                            let mut total_events = 0;
831                            total_events += events.len();
832                            // items.push((events, condition));
833
834                            let mvcc = &db.mvcc;
835                            let mut writer = match mvcc.writer() {
836                                Ok(writer) => writer,
837                                Err(err) => {
838                                    let _ = response_tx.send(Err(err));
839                                    continue;
840                                }
841                            };
842
843                            let mut responders: Vec<oneshot::Sender<DcbResult<u64>>> = Vec::new();
844                            let mut results: Vec<DcbResult<u64>> = Vec::new();
845
846                            // Track abort state for non-integrity error within the batch
847                            let mut abort_idx: Option<usize> = None;
848                            let mut abort_err: Option<DcbError> = None;
849
850                            responders.push(response_tx);
851                            let result = UmaDb::process_append_request(
852                                events,
853                                condition,
854                                tracking_info,
855                                mvcc,
856                                &mut writer,
857                            );
858                            // Record result and possibly mark abort
859                            match &result {
860                                Ok(_) => results.push(result),
861                                Err(e) if is_integrity_error(e) => {
862                                    results.push(Err(clone_dcb_error(e)))
863                                }
864                                Err(e) => {
865                                    abort_idx = Some(0);
866                                    abort_err = Some(clone_dcb_error(e));
867                                    results.push(Err(clone_dcb_error(e)));
868                                }
869                            }
870
871                            // Drain the channel for more pending writer requests without awaiting.
872                            // Important: do not drop a popped request when hitting the batch limit.
873                            // We stop draining BEFORE attempting to recv if we've reached the limit.
874                            loop {
875                                if total_events >= APPEND_BATCH_MAX_EVENTS {
876                                    break;
877                                }
878                                // Stop draining if we've already decided to abort
879                                if abort_idx.is_some() {
880                                    break;
881                                }
882                                match request_rx.try_recv() {
883                                    Ok(WriterRequest::Append {
884                                        events,
885                                        condition,
886                                        tracking_info,
887                                        response_tx,
888                                    }) => {
889                                        let ev_len = events.len();
890                                        let idx_in_batch = responders.len();
891                                        responders.push(response_tx);
892                                        let res_next = UmaDb::process_append_request(
893                                            events,
894                                            condition,
895                                            tracking_info,
896                                            mvcc,
897                                            &mut writer,
898                                        );
899                                        match &res_next {
900                                            Ok(_) => results.push(res_next),
901                                            Err(e) if is_integrity_error(e) => {
902                                                results.push(Err(clone_dcb_error(e)))
903                                            }
904                                            Err(e) => {
905                                                abort_idx = Some(idx_in_batch);
906                                                abort_err = Some(clone_dcb_error(e));
907                                                results.push(Err(clone_dcb_error(e)));
908                                                // Do not accumulate more into the batch
909                                            }
910                                        }
911                                        total_events += ev_len;
912                                    }
913                                    Ok(WriterRequest::Shutdown) => {
914                                        // Push back the shutdown signal by breaking and letting
915                                        // outer loop handle after batch. We'll process the
916                                        // current batch first, then break the outer loop on
917                                        // the next iteration when the channel is empty.
918                                        break;
919                                    }
920                                    Err(mpsc::error::TryRecvError::Empty) => {
921                                        break;
922                                    }
923                                    Err(mpsc::error::TryRecvError::Disconnected) => break,
924                                }
925                            }
926                            // println!("Total events: {total_events}");
927
928                            if let (Some(failed_at), Some(orig_err)) = (abort_idx, abort_err) {
929                                // Abort batch: skip commit; respond to all items in this batch
930                                let shadow = shadow_for_batch_abort(&orig_err);
931                                for (i, tx) in responders.into_iter().enumerate() {
932                                    if i == failed_at {
933                                        let _ = tx.send(Err(clone_dcb_error(&orig_err)));
934                                    } else {
935                                        let _ = tx.send(Err(clone_dcb_error(&shadow)));
936                                    }
937                                }
938                                // Do not update head, since nothing was committed
939                                continue;
940                            }
941
942                            // Single commit at the end of the batch
943                            let batch_result = match mvcc.commit(&mut writer) {
944                                Ok(_) => Ok(results),
945                                Err(err) => Err(err),
946                            };
947
948                            match batch_result {
949                                Ok(results) => {
950                                    // Send individual results back to requesters
951                                    for (res, tx) in results.into_iter().zip(responders.into_iter())
952                                    {
953                                        let _ = tx.send(res);
954                                    }
955                                    // After a successful batch commit, publish the updated head from writer.next_position.
956                                    let last_committed = writer.next_position.0.saturating_sub(1);
957                                    let new_head = if last_committed == 0 {
958                                        None
959                                    } else {
960                                        Some(last_committed)
961                                    };
962                                    let _ = head_tx_writer.send(new_head);
963                                }
964                                Err(e) => {
965                                    // If the batch failed as a whole (e.g., commit failed), propagate the SAME error to all responders.
966                                    // DCBError is not Clone (contains io::Error), so reconstruct a best-effort copy by using its Display text
967                                    // for Io and cloning data for other variants.
968                                    let total = responders.len();
969                                    let mut iter = responders.into_iter();
970                                    for _ in 0..total {
971                                        if let Some(tx) = iter.next() {
972                                            let _ = tx.send(Err(clone_dcb_error(&e)));
973                                        }
974                                    }
975                                }
976                            }
977                        }
978                        WriterRequest::Shutdown => {
979                            break;
980                        }
981                    }
982                }
983            });
984        });
985
986        Ok(Self {
987            mvcc,
988            head_watch_tx: head_tx,
989            writer_request_tx: request_tx,
990        })
991    }
992
993    fn read(
994        &self,
995        query: Option<DcbQuery>,
996        start: Option<u64>,
997        backwards: bool,
998        limit: Option<u32>,
999    ) -> DcbResult<(Vec<DcbSequencedEvent>, Option<u64>)> {
1000        let reader = self.mvcc.reader()?;
1001        let last_committed_position = reader.next_position.0.saturating_sub(1);
1002
1003        let q = query.unwrap_or(DcbQuery { items: vec![] });
1004        let start_position = start.map(Position);
1005
1006        let events = read_conditional(
1007            &self.mvcc,
1008            &std::collections::HashMap::new(),
1009            reader.events_tree_root_id,
1010            reader.tags_tree_root_id,
1011            q,
1012            start_position,
1013            backwards,
1014            limit,
1015            false,
1016        )
1017        .map_err(|e| DcbError::Corruption(format!("{e}")))?;
1018
1019        let head = if limit.is_none() {
1020            if last_committed_position == 0 {
1021                None
1022            } else {
1023                Some(last_committed_position)
1024            }
1025        } else {
1026            events.last().map(|e| e.position)
1027        };
1028
1029        Ok((events, head))
1030    }
1031
1032    fn head(&self) -> DcbResult<Option<u64>> {
1033        let (_, header) = self
1034            .mvcc
1035            .get_latest_header()
1036            .map_err(|e| DcbError::Corruption(format!("{e}")))?;
1037        let last = header.next_position.0.saturating_sub(1);
1038        if last == 0 { Ok(None) } else { Ok(Some(last)) }
1039    }
1040
1041    fn get_tracking_info(&self, source: String) -> DcbResult<Option<u64>> {
1042        let db = UmaDb::from_arc(self.mvcc.clone());
1043        db.get_tracking_info(&source)
1044    }
1045
1046    pub async fn append(
1047        &self,
1048        events: Vec<DcbEvent>,
1049        condition: Option<DcbAppendCondition>,
1050        tracking_info: Option<TrackingInfo>,
1051    ) -> DcbResult<u64> {
1052        // Concurrent pre-check of the given condition using a reader in a blocking thread.
1053        let pre_append_decision = if let Some(mut given_condition) = condition {
1054            let reader = self.mvcc.reader()?;
1055            let current_head = {
1056                let last = reader.next_position.0.saturating_sub(1);
1057                if last == 0 { None } else { Some(last) }
1058            };
1059
1060            // Perform conditional read on the snapshot (limit 1) starting after the given position
1061            let from = given_condition.after.map(|after| Position(after + 1));
1062            let empty_dirty = std::collections::HashMap::new();
1063            let found = read_conditional(
1064                &self.mvcc,
1065                &empty_dirty,
1066                reader.events_tree_root_id,
1067                reader.tags_tree_root_id,
1068                given_condition.fail_if_events_match.clone(),
1069                from,
1070                false,
1071                Some(1),
1072                false,
1073            )?;
1074
1075            if let Some(matched) = found.first() {
1076                // Found one event — consider if the request is idempotent...
1077                match is_request_idempotent(
1078                    &self.mvcc,
1079                    &empty_dirty,
1080                    reader.events_tree_root_id,
1081                    reader.tags_tree_root_id,
1082                    &events,
1083                    given_condition.fail_if_events_match.clone(),
1084                    from,
1085                ) {
1086                    Ok(Some(last_recorded_position)) => {
1087                        // Request is idempotent; skip actual append
1088                        PreAppendDecision::AlreadyAppended(last_recorded_position)
1089                    }
1090                    Ok(None) => {
1091                        // Integrity violation
1092                        let msg = format!(
1093                            "condition: {:?} matched: {:?}",
1094                            given_condition.clone(),
1095                            matched,
1096                        );
1097                        return Err(DcbError::IntegrityError(msg));
1098                    }
1099                    Err(err) => {
1100                        // Propagate underlying read error
1101                        return Err(err);
1102                    }
1103                }
1104            } else {
1105                // No match found: we can advance 'after' to the current head observed by this reader
1106                let new_after = std::cmp::max(
1107                    given_condition.after.unwrap_or(0),
1108                    current_head.unwrap_or(0),
1109                );
1110                given_condition.after = Some(new_after);
1111
1112                PreAppendDecision::UseCondition(Some(given_condition))
1113            }
1114        } else {
1115            // No condition provided at all
1116            PreAppendDecision::UseCondition(None)
1117        };
1118
1119        // Handle the pre-check decision
1120        match pre_append_decision {
1121            PreAppendDecision::AlreadyAppended(last_found_position) => {
1122                // ✅ Request was idempotent — just return the existing position.
1123                Ok(last_found_position)
1124            }
1125            PreAppendDecision::UseCondition(adjusted_condition) => {
1126                // ✅ Proceed with append operation on the writer thread.
1127                let (response_tx, response_rx) = oneshot::channel();
1128
1129                self.writer_request_tx
1130                    .send(WriterRequest::Append {
1131                        events,
1132                        condition: adjusted_condition,
1133                        tracking_info,
1134                        response_tx,
1135                    })
1136                    .await
1137                    .map_err(|_| {
1138                        DcbError::Io(std::io::Error::other(
1139                            "failed to send append request to EventStore thread",
1140                        ))
1141                    })?;
1142
1143                response_rx.await.map_err(|_| {
1144                    DcbError::Io(std::io::Error::other(
1145                        "failed to receive append response from EventStore thread",
1146                    ))
1147                })?
1148            }
1149        }
1150    }
1151
1152    fn watch_head(&self) -> watch::Receiver<Option<u64>> {
1153        self.head_watch_tx.subscribe()
1154    }
1155
1156    #[allow(dead_code)]
1157    async fn shutdown(&self) {
1158        let _ = self.writer_request_tx.send(WriterRequest::Shutdown).await;
1159    }
1160}
1161
1162// Clone implementation for EventStoreHandle
1163impl Clone for RequestHandler {
1164    fn clone(&self) -> Self {
1165        Self {
1166            mvcc: self.mvcc.clone(),
1167            head_watch_tx: self.head_watch_tx.clone(),
1168            writer_request_tx: self.writer_request_tx.clone(),
1169        }
1170    }
1171}
1172
1173#[derive(Debug)]
1174enum PreAppendDecision {
1175    /// Proceed with this (possibly adjusted) condition
1176    UseCondition(Option<DcbAppendCondition>),
1177    /// Skip append operation because the request was idempotent; return last recorded position
1178    AlreadyAppended(u64),
1179}