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