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