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
221pub async fn start_server_with_options(
222    options: ServerOptions,
223    shutdown_rx: oneshot::Receiver<()>,
224) -> Result<(), Box<dyn std::error::Error>> {
225    let addr = options.listen_addr.parse()?;
226    // ---- Bind incoming manually like tonic ----
227    let incoming = match TcpIncoming::bind(addr) {
228        Ok(incoming) => incoming,
229        Err(err) => {
230            return Err(Box::new(DcbError::InitializationError(format!(
231                "failed to bind to address {}: {}",
232                addr, err
233            ))));
234        }
235    }
236    .with_nodelay(Some(true))
237    .with_keepalive(Some(std::time::Duration::from_secs(60)));
238
239    // Create a shutdown broadcast channel for terminating ongoing subscriptions
240    let (srv_shutdown_tx, srv_shutdown_rx) = watch::channel(false);
241    let dcb_server = match DcbServer::new(srv_shutdown_rx, options.api_key.clone(), options.storage)
242    {
243        Ok(server) => server,
244        Err(err) => {
245            return Err(Box::new(err));
246        }
247    };
248
249    println!(
250        "UmaDB has {:?} events",
251        dcb_server
252            .request_handler
253            .head()
254            .unwrap_or(Some(0))
255            .unwrap_or(0)
256    );
257    let tls_mode_display_str = if options.tls.is_some() {
258        "with TLS"
259    } else {
260        "without TLS"
261    };
262
263    let api_key_display_str = if options.api_key.is_some() {
264        "with API key"
265    } else {
266        "without API key"
267    };
268
269    // gRPC Health service setup
270    use tonic_health::ServingStatus; // server API expects this enum
271    let (health_reporter, health_service) = tonic_health::server::health_reporter();
272    // Set overall and service-specific health to SERVING
273    health_reporter
274        .set_service_status("", ServingStatus::Serving)
275        .await;
276    health_reporter
277        .set_service_status("umadb.v1.DCB", ServingStatus::Serving)
278        .await;
279    let health_reporter_for_shutdown = health_reporter.clone();
280
281    // Apply PathRewriterLayer at the server level to intercept all requests before routing
282    let mut builder = build_server_builder_with_options(options.tls)
283        .layer(PathRewriterLayer)
284        .add_service(health_service);
285
286    // Add DCB service (auth enforced inside RPC handlers if configured)
287    builder = builder.add_service(dcb_server.into_service());
288    let router = builder;
289
290    println!("UmaDB is listening on {addr} ({tls_mode_display_str}, {api_key_display_str})");
291    println!("UmaDB started in {:?}", uptime());
292    // let incoming = router.server.bind_incoming();
293    router
294        .serve_with_incoming_shutdown(incoming, async move {
295            // Wait for an external shutdown trigger
296            let _ = shutdown_rx.await;
297            // Mark health as NOT_SERVING before shutdown
298            let _ = health_reporter_for_shutdown
299                .set_service_status("", ServingStatus::NotServing)
300                .await;
301            let _ = health_reporter_for_shutdown
302                .set_service_status("umadb.v1.DCB", ServingStatus::NotServing)
303                .await;
304            // Broadcast shutdown to all subscription tasks
305            let _ = srv_shutdown_tx.send(true);
306            println!("\nUmaDB server shutdown complete");
307        })
308        .await?;
309
310    Ok(())
311}
312
313/// Maximum number of blocking read/subscribe batch-scans allowed to execute
314/// concurrently on the blocking thread pool.
315///
316/// This bounds CPU oversubscription so that the (small, fixed) Tokio reactor
317/// threads always have CPU to service HTTP/2 keepalive frames. Permits are
318/// acquired per batch and released between batches, so this does NOT limit the
319/// number of live (mostly-parked) reads or subscriptions — only how many are
320/// actively scanning storage at any instant.
321///
322/// Defaults to `available_parallelism() * PER_CORE`; overridable via the
323/// `UMADB_READ_SCAN_CONCURRENCY` environment variable.
324fn read_scan_concurrency_limit() -> usize {
325    const PER_CORE: usize = 4;
326    if let Some(n) = std::env::var("UMADB_READ_SCAN_CONCURRENCY")
327        .ok()
328        .and_then(|s| s.parse::<usize>().ok())
329        .filter(|n| *n > 0)
330    {
331        return n;
332    }
333    std::thread::available_parallelism()
334        .map(|n| n.get())
335        .unwrap_or(4)
336        .saturating_mul(PER_CORE)
337        .max(PER_CORE)
338}
339
340// gRPC server implementation
341pub struct DcbServer {
342    pub(crate) request_handler: RequestHandler,
343    shutdown_watch_rx: watch::Receiver<bool>,
344    api_key: Option<String>,
345    // Limits concurrent blocking read/subscribe batch-scans (see `read_scan_concurrency_limit`).
346    read_scan_semaphore: Arc<Semaphore>,
347}
348
349impl DcbServer {
350    pub fn new(
351        shutdown_rx: watch::Receiver<bool>,
352        api_key: Option<String>,
353        storage_options: StorageOptions,
354    ) -> DcbResult<Self> {
355        let command_handler = RequestHandler::new(storage_options)?;
356        Ok(Self {
357            request_handler: command_handler,
358            shutdown_watch_rx: shutdown_rx,
359            api_key,
360            read_scan_semaphore: Arc::new(Semaphore::new(read_scan_concurrency_limit())),
361        })
362    }
363
364    pub fn into_service(self) -> umadb_proto::v1::dcb_server::DcbServer<Self> {
365        umadb_proto::v1::dcb_server::DcbServer::new(self)
366    }
367
368    fn enforce_api_key(&self, metadata: &tonic::metadata::MetadataMap) -> Result<(), Status> {
369        if let Some(expected) = &self.api_key {
370            let auth = metadata.get("authorization");
371            let expected_val = format!("Bearer {}", expected);
372            let ok = auth
373                .and_then(|m| m.to_str().ok())
374                .map(|s| s == expected_val)
375                .unwrap_or(false);
376            if !ok {
377                return Err(status_from_dcb_error(DcbError::AuthenticationError(
378                    "missing or invalid API key".to_string(),
379                )));
380            }
381        }
382        Ok(())
383    }
384}
385
386#[tonic::async_trait]
387impl umadb_proto::v1::dcb_server::Dcb for DcbServer {
388    type ReadStream =
389        Pin<Box<dyn Stream<Item = Result<umadb_proto::v1::ReadResponse, Status>> + Send + 'static>>;
390    type SubscribeStream = Pin<
391        Box<dyn Stream<Item = Result<umadb_proto::v1::SubscribeResponse, Status>> + Send + 'static>,
392    >;
393
394    async fn read(
395        &self,
396        request: Request<umadb_proto::v1::ReadRequest>,
397    ) -> Result<Response<Self::ReadStream>, Status> {
398        // Enforce API key if configured
399        self.enforce_api_key(request.metadata())?;
400        let read_request = request.into_inner();
401
402        // Avoid confusion by reporting the usage error with guidance.
403        #[allow(deprecated)]
404        if read_request.subscribe.unwrap_or(false) {
405            return Err(status_from_dcb_error(DcbError::InvalidArgument(
406                "The `subscribe` argument of `read()` has been deprecated. \
407                Please call the `subscribe()` method instead."
408                    .to_string(),
409            )));
410        }
411
412        // Convert protobuf query to DCB types
413        let query: Option<DcbQuery> = read_request.query.map(|q| q.into());
414        let start = read_request.start;
415        let backwards = read_request.backwards.unwrap_or(false);
416        let limit = read_request.limit;
417        // Cap requested batch size.
418        let batch_size = read_request
419            .batch_size
420            .unwrap_or(READ_RESPONSE_BATCH_SIZE_DEFAULT)
421            .clamp(1, READ_RESPONSE_BATCH_SIZE_MAX);
422
423        // Create a channel for streaming responses (deeper buffer to reduce backpressure under concurrency)
424        let (tx, rx) = mpsc::channel(2048);
425        // Clone the request handler.
426        let request_handler = self.request_handler.clone();
427        // Clone the shutdown watch receiver.
428        let mut shutdown_watch_rx = self.shutdown_watch_rx.clone();
429
430        let cancel_signal = Arc::new(std::sync::atomic::AtomicBool::new(false));
431        let cancel_signal_for_task = cancel_signal.clone();
432        let read_scan_semaphore = self.read_scan_semaphore.clone();
433
434        // Spawn a task to handle the read operation and stream multiple batches
435        tokio::spawn(async move {
436            // Ensure we can reuse the same query across batches
437            let query_clone = query;
438            let mut next_start = start;
439            let mut sent_any = false;
440            let mut remaining_limit = limit.unwrap_or(u32::MAX);
441            let mut captured_db_head: Option<u64> = None;
442            let mut have_captured_db_head: bool = false;
443            loop {
444                // TODO: Can remove this check when we sure that cancel_signal_for_task
445                //  is fully respected by all paths in spawn_blocking(handler.read).
446                // Exit if the client has gone away or the server is shutting down.
447                if tx.is_closed() || *shutdown_watch_rx.borrow() {
448                    cancel_signal_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
449                    break;
450                }
451                // Determine per-iteration limit.
452                let read_limit = remaining_limit.min(batch_size);
453                // If subscription and remaining exhausted (limit reached), terminate
454                if limit.is_some() && remaining_limit == 0 {
455                    break;
456                }
457                // Throttle concurrent blocking scans so reactor threads stay free to
458                // service HTTP/2 keepalive. The permit is held only for this batch scan
459                // (moved into the closure), and released before we send the batch below.
460                let permit = match read_scan_semaphore.clone().acquire_owned().await {
461                    Ok(permit) => permit,
462                    Err(_) => break,
463                };
464                let handler = request_handler.clone();
465                let query_val = query_clone.clone();
466                let limit_val = Some(read_limit);
467                let cancel_for_blocking = cancel_signal_for_task.clone();
468                let mut blocking_handle = tokio::task::spawn_blocking(move || {
469                    let _permit = permit;
470                    handler.read(
471                        query_val,
472                        next_start,
473                        backwards,
474                        limit_val,
475                        Some(cancel_for_blocking),
476                    )
477                });
478
479                let res = tokio::select! {
480                    res = &mut blocking_handle => {
481                        res.map_err(|e| DcbError::InternalError(e.to_string())).and_then(|res| res)
482                    }
483                    _ = tx.closed() => {
484                        cancel_signal_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
485                        // Await the task to ensure it finishes and doesn't leak
486                        let _ = blocking_handle.await;
487                        break;
488                    }
489                    _ = shutdown_watch_rx.changed() => {
490                        cancel_signal_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
491                        let _ = blocking_handle.await;
492                        break;
493                    }
494                };
495
496                match res {
497                    Ok((dcb_sequenced_events, db_head)) => {
498                        // Capture the db head from the first read.
499                        if !have_captured_db_head {
500                            captured_db_head = db_head;
501                            have_captured_db_head = true;
502                        }
503
504                        // Capture the original length before consuming events
505                        let original_len = dcb_sequenced_events.len();
506                        let read_less_than_read_limit = (original_len as u32) < read_limit;
507
508                        // Map events to protobuf messages, discarding if position too large.
509                        let sequenced_event_protos: Vec<umadb_proto::v1::SequencedEvent> =
510                            dcb_sequenced_events
511                                .into_iter()
512                                .filter(|e| {
513                                    if let Some(h) = captured_db_head {
514                                        e.position <= h
515                                    } else {
516                                        true
517                                    }
518                                })
519                                .map(umadb_proto::v1::SequencedEvent::from)
520                                .collect();
521
522                        // Check if we filtered out any events
523                        let reached_captured_head = captured_db_head.is_some()
524                            && sequenced_event_protos.len() < original_len;
525
526                        if sequenced_event_protos.is_empty() {
527                            if !sent_any {
528                                // At least send an empty response to communicate head.
529                                let response = umadb_proto::v1::ReadResponse {
530                                    events: vec![],
531                                    head: if limit.is_some() {
532                                        None
533                                    } else {
534                                        captured_db_head
535                                    },
536                                };
537                                let _ = tx.send(Ok(response)).await;
538                            }
539                            // Stop looping, because there's nothing else to read.
540                            break;
541                        }
542
543                        // Capture values needed after sequenced_event_protos is moved.
544                        let sent_count = sequenced_event_protos.len() as u32;
545
546                        let last_event_position = sequenced_event_protos.last().map(|e| e.position);
547
548                        let response = umadb_proto::v1::ReadResponse {
549                            events: sequenced_event_protos,
550                            head: if limit.is_some() {
551                                last_event_position
552                            } else {
553                                captured_db_head
554                            },
555                        };
556
557                        if tx.send(Ok(response)).await.is_err() {
558                            break;
559                        }
560                        sent_any = true;
561
562                        // Advance the cursor (use a new reader on the next loop iteration)
563                        next_start = last_event_position.map(|p| {
564                            if backwards {
565                                p.saturating_sub(1)
566                            } else {
567                                p.saturating_add(1)
568                            }
569                        });
570
571                        // Stop streaming further if we read less than limit or
572                        // reached the captured head boundary.
573                        if read_less_than_read_limit || reached_captured_head {
574                            break;
575                        }
576
577                        // Decrease the remaining overall limit if any, and stop if reached
578                        if limit.is_some() {
579                            remaining_limit = remaining_limit.saturating_sub(sent_count);
580                            if remaining_limit == 0 {
581                                break;
582                            }
583                        }
584
585                        // Yield to let other tasks progress under high concurrency
586                        tokio::task::yield_now().await;
587                    }
588                    Err(e) => {
589                        if matches!(e, DcbError::CancelledByUser()) {
590                            // Silently stop if cancelled by user
591                        } else {
592                            let _ = tx.send(Err(status_from_dcb_error(e))).await;
593                        }
594                        break;
595                    }
596                }
597            }
598        });
599
600        // Return the receiver as a stream
601        Ok(Response::new(
602            Box::pin(ReceiverStream::new(rx)) as Self::ReadStream
603        ))
604    }
605
606    async fn subscribe(
607        &self,
608        request: Request<umadb_proto::v1::SubscribeRequest>,
609    ) -> Result<Response<Self::SubscribeStream>, Status> {
610        // Enforce API key if configured
611        self.enforce_api_key(request.metadata())?;
612        let subscribe_request = request.into_inner();
613
614        // Convert protobuf query to DCB types
615        let query: Option<DcbQuery> = subscribe_request.query.map(|q| q.into());
616        let after = subscribe_request.after;
617        // Cap requested batch size.
618        let batch_size = subscribe_request
619            .batch_size
620            .unwrap_or(READ_RESPONSE_BATCH_SIZE_DEFAULT)
621            .clamp(1, READ_RESPONSE_BATCH_SIZE_MAX);
622
623        // Create a channel for streaming responses
624        let (tx, rx) = mpsc::channel(2048);
625        // Clone the request handler.
626        let request_handler = self.request_handler.clone();
627        // Clone the shutdown watch receiver.
628        let mut shutdown_watch_rx = self.shutdown_watch_rx.clone();
629
630        let cancel_signal = Arc::new(std::sync::atomic::AtomicBool::new(false));
631        let cancel_signal_for_task = cancel_signal.clone();
632        let read_scan_semaphore = self.read_scan_semaphore.clone();
633
634        // Spawn a task to handle the subscribe operation and stream multiple batches
635        tokio::spawn(async move {
636            // Ensure we can reuse the same query across batches
637            let query_clone = query;
638            // Todo: End the subscription if after is Some(u64:MAX).
639            let mut next_after = after.map(|a| a.saturating_add(1));
640            // Create a watch receiver for head updates
641            let mut head_rx = request_handler.watch_head();
642
643            loop {
644                // TODO: Can remove this check when we sure that cancel_signal_for_task
645                //  is fully respected by all paths in spawn_blocking(handler.read).
646                // Exit if the client has gone away or the server is shutting down.
647                if tx.is_closed() || *shutdown_watch_rx.borrow() {
648                    cancel_signal_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
649                    break;
650                }
651
652                // Throttle concurrent blocking scans so reactor threads stay free to
653                // service HTTP/2 keepalive. The permit is held only for this batch scan
654                // (moved into the closure), and released before we send the batch or wait
655                // for new events below — so long-lived subscriptions do not tie up a permit.
656                let permit = match read_scan_semaphore.clone().acquire_owned().await {
657                    Ok(permit) => permit,
658                    Err(_) => break,
659                };
660                let handler = request_handler.clone();
661                let query_val = query_clone.clone();
662                let batch_size_val = Some(batch_size);
663                let cancel_for_blocking = cancel_signal_for_task.clone();
664                let mut blocking_handle = tokio::task::spawn_blocking(move || {
665                    let _permit = permit;
666                    handler.read(
667                        query_val,
668                        next_after,
669                        false,
670                        batch_size_val,
671                        Some(cancel_for_blocking),
672                    )
673                });
674
675                let res = tokio::select! {
676                    res = &mut blocking_handle => {
677                        res.map_err(|e| DcbError::InternalError(e.to_string())).and_then(|res| res)
678                    }
679                    _ = tx.closed() => {
680                        cancel_signal_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
681                        let _ = blocking_handle.await;
682                        break;
683                    }
684                    _ = shutdown_watch_rx.changed() => {
685                        cancel_signal_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
686                        let _ = blocking_handle.await;
687                        break;
688                    }
689                };
690
691                match res {
692                    Ok((dcb_sequenced_events, _unused_db_head)) => {
693                        // Map events to protobuf type
694                        let sequenced_event_protos: Vec<umadb_proto::v1::SequencedEvent> =
695                            dcb_sequenced_events
696                                .into_iter()
697                                .map(umadb_proto::v1::SequencedEvent::from)
698                                .collect();
699
700                        if sequenced_event_protos.is_empty() {
701                            // For subscriptions, wait for new events instead of terminating
702                            tokio::select! {
703                                _ = head_rx.changed() => {},
704                                _ = shutdown_watch_rx.changed() => {},
705                                _ = tx.closed() => {},
706                            }
707                            continue;
708                        }
709
710                        let last_event_position = sequenced_event_protos.last().map(|e| e.position);
711
712                        let response = umadb_proto::v1::SubscribeResponse {
713                            events: sequenced_event_protos,
714                        };
715
716                        if tx.send(Ok(response)).await.is_err() {
717                            break;
718                        }
719
720                        // Advance the cursor (use a new reader on the next loop iteration)
721                        // Todo: End the subscription if last_event_position is Some(u64:MAX).
722                        next_after = last_event_position.map(|p| p.saturating_add(1));
723
724                        // Yield to let other tasks progress under high concurrency
725                        tokio::task::yield_now().await;
726                    }
727                    Err(e) => {
728                        if matches!(e, DcbError::CancelledByUser()) {
729                            // Silently stop if cancelled by user
730                        } else {
731                            let _ = tx.send(Err(status_from_dcb_error(e))).await;
732                        }
733                        break;
734                    }
735                }
736            }
737        });
738
739        // Return the receiver as a stream
740        Ok(Response::new(
741            Box::pin(ReceiverStream::new(rx)) as Self::SubscribeStream
742        ))
743    }
744
745    async fn append(
746        &self,
747        request: Request<umadb_proto::v1::AppendRequest>,
748    ) -> Result<Response<umadb_proto::v1::AppendResponse>, Status> {
749        // Enforce API key if configured
750        self.enforce_api_key(request.metadata())?;
751        let req = request.into_inner();
752
753        // Convert protobuf types to API types
754        let events: Vec<DcbEvent> = match req.events.into_iter().map(|e| e.try_into()).collect() {
755            Ok(events) => events,
756            Err(e) => {
757                return Err(status_from_dcb_error(e));
758            }
759        };
760        let condition = req.condition.map(|c| c.into());
761
762        let cancel_signal = Arc::new(std::sync::atomic::AtomicBool::new(false));
763        let cancel_signal_for_task = cancel_signal.clone();
764
765        // Create a way to watch for the request being cancelled/dropped
766        let (cancel_tx, cancel_rx) = oneshot::channel();
767        let _guard = CancellationGuard(Some(cancel_tx));
768
769        // Spawn a monitoring task that survives the gRPC future being dropped
770        let cancel_signal_for_monitoring = cancel_signal.clone();
771        tokio::spawn(async move {
772            // This resolves when _guard is dropped (client disconnects)
773            // or when the gRPC method finishes normally.
774            let _ = cancel_rx.await;
775            cancel_signal_for_monitoring.store(true, std::sync::atomic::Ordering::SeqCst);
776        });
777
778        // Call the event store append method
779        let res = self
780            .request_handler
781            .append(
782                events,
783                condition,
784                req.tracking_info.map(|t| TrackingInfo {
785                    source: t.source,
786                    position: t.position,
787                }),
788                Some(cancel_signal_for_task.clone()),
789            )
790            .await;
791
792        match res {
793            Ok(position) => Ok(Response::new(umadb_proto::v1::AppendResponse { position })),
794            Err(e) => Err(status_from_dcb_error(e)),
795        }
796    }
797
798    async fn head(
799        &self,
800        request: Request<umadb_proto::v1::HeadRequest>,
801    ) -> Result<Response<umadb_proto::v1::HeadResponse>, Status> {
802        // Enforce API key if configured
803        self.enforce_api_key(request.metadata())?;
804        // `head()` reads a single header page (O(1)), but it is still blocking storage
805        // I/O and must not run on a reactor thread, or it steals time from HTTP/2
806        // keepalive under high concurrency. It is cheap enough not to need a permit.
807        let request_handler = self.request_handler.clone();
808        let res = tokio::task::spawn_blocking(move || request_handler.head())
809            .await
810            .map_err(|e| status_from_dcb_error(DcbError::InternalError(e.to_string())))?;
811        match res {
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        // This does a tracking-tree descent (bounded, but real file I/O), so run it off
828        // the reactor and gate it with the same semaphore as read scans to bound
829        // concurrent blocking storage work. The permit is released as soon as it returns.
830        let permit = match self.read_scan_semaphore.clone().acquire_owned().await {
831            Ok(permit) => permit,
832            Err(_) => {
833                return Err(status_from_dcb_error(DcbError::InternalError(
834                    "read-scan semaphore closed".to_string(),
835                )));
836            }
837        };
838        let request_handler = self.request_handler.clone();
839        let res = tokio::task::spawn_blocking(move || {
840            let _permit = permit;
841            request_handler.get_tracking_info(req.source)
842        })
843        .await
844        .map_err(|e| status_from_dcb_error(DcbError::InternalError(e.to_string())))?;
845        match res {
846            Ok(position) => Ok(Response::new(umadb_proto::v1::TrackingResponse {
847                position,
848            })),
849            Err(e) => Err(status_from_dcb_error(e)),
850        }
851    }
852}
853
854// Message types for communication between the gRPC server and the request handler's writer thread
855enum WriterRequest {
856    Append {
857        events: Vec<DcbEvent>,
858        condition: Option<DcbAppendCondition>,
859        tracking_info: Option<TrackingInfo>,
860        response_tx: oneshot::Sender<DcbResult<u64>>,
861        cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
862    },
863    Shutdown,
864}
865
866// Thread-safe request handler
867struct RequestHandler {
868    mvcc: Arc<Mvcc>,
869    head_watch_tx: watch::Sender<Option<u64>>,
870    writer_request_tx: mpsc::Sender<WriterRequest>,
871}
872
873impl RequestHandler {
874    fn new(storage_options: StorageOptions) -> DcbResult<Self> {
875        // Create a channel for sending requests to the writer thread
876        let (request_tx, mut request_rx) = mpsc::channel::<WriterRequest>(1024);
877
878        // Build a shared Mvcc instance (Arc) upfront so reads can proceed concurrently
879        let mvcc = Arc::new(Mvcc::new(false, storage_options)?);
880
881        // Initialize the head watch channel with the current head.
882        let init_head = {
883            let header_page = mvcc.get_latest_header_page()?;
884            let header = header_page.as_header_node()?;
885            let last = header.next_position.0.saturating_sub(1);
886            if last == 0 { None } else { Some(last) }
887        };
888        let (head_tx, _head_rx) = watch::channel::<Option<u64>>(init_head);
889
890        // Spawn a thread for processing writer requests.
891        let mvcc_for_writer = mvcc.clone();
892        let head_tx_writer = head_tx.clone();
893        thread::spawn(move || {
894            let db = UmaDb::from_arc(mvcc_for_writer);
895
896            // Create a runtime for processing writer requests.
897            let rt = Runtime::new().unwrap();
898
899            // Process writer requests.
900            rt.block_on(async {
901                while let Some(request) = request_rx.recv().await {
902                    match request {
903                        WriterRequest::Append {
904                            events,
905                            condition,
906                            tracking_info,
907                            response_tx,
908                            cancel,
909                        } => {
910                            // Batch processing: drain any immediately available requests
911                            // let mut items: Vec<(Vec<DCBEvent>, Option<DCBAppendCondition>)> =
912                            //     Vec::new();
913
914                            let mut total_events = 0;
915                            total_events += events.len();
916                            // items.push((events, condition));
917
918                            let mvcc = &db.mvcc;
919                            let mut writer = match mvcc.writer() {
920                                Ok(writer) => writer,
921                                Err(err) => {
922                                    let _ = response_tx.send(Err(err));
923                                    continue;
924                                }
925                            };
926
927                            let mut responders: Vec<oneshot::Sender<DcbResult<u64>>> = Vec::new();
928                            let mut results: Vec<DcbResult<u64>> = Vec::new();
929
930                            // Track abort state for non-integrity error within the batch
931                            let mut abort_idx: Option<usize> = None;
932                            let mut abort_err: Option<DcbError> = None;
933
934                            responders.push(response_tx);
935                            let result = UmaDb::process_append_request(
936                                events,
937                                condition,
938                                tracking_info,
939                                mvcc,
940                                &mut writer,
941                                cancel,
942                            );
943                            // Record result and possibly mark abort
944                            match &result {
945                                Ok(_) => results.push(result),
946                                Err(e) if is_integrity_error(e) => {
947                                    results.push(Err(clone_dcb_error(e)))
948                                }
949                                Err(e) if is_invalid_argument_error(e) => {
950                                    results.push(Err(clone_dcb_error(e)))
951                                }
952                                Err(e) => {
953                                    abort_idx = Some(0);
954                                    abort_err = Some(clone_dcb_error(e));
955                                    results.push(Err(clone_dcb_error(e)));
956                                }
957                            }
958
959                            // Drain the channel for more pending writer requests without awaiting.
960                            // Important: do not drop a popped request when hitting the batch limit.
961                            // We stop draining BEFORE attempting to recv if we've reached the limit.
962                            loop {
963                                if total_events >= APPEND_BATCH_MAX_EVENTS {
964                                    break;
965                                }
966                                // Stop draining if we've already decided to abort
967                                if abort_idx.is_some() {
968                                    break;
969                                }
970                                match request_rx.try_recv() {
971                                    Ok(WriterRequest::Append {
972                                        events,
973                                        condition,
974                                        tracking_info,
975                                        response_tx,
976                                        cancel,
977                                    }) => {
978                                        let ev_len = events.len();
979                                        let idx_in_batch = responders.len();
980                                        responders.push(response_tx);
981                                        let res_next = UmaDb::process_append_request(
982                                            events,
983                                            condition,
984                                            tracking_info,
985                                            mvcc,
986                                            &mut writer,
987                                            cancel,
988                                        );
989                                        match &res_next {
990                                            Ok(_) => results.push(res_next),
991                                            Err(e) if is_integrity_error(e) => {
992                                                results.push(Err(clone_dcb_error(e)))
993                                            }
994                                            Err(e) if is_invalid_argument_error(e) => {
995                                                results.push(Err(clone_dcb_error(e)))
996                                            }
997                                            Err(e) => {
998                                                abort_idx = Some(idx_in_batch);
999                                                abort_err = Some(clone_dcb_error(e));
1000                                                results.push(Err(clone_dcb_error(e)));
1001                                                // Do not accumulate more into the batch
1002                                            }
1003                                        }
1004                                        total_events += ev_len;
1005                                    }
1006                                    Ok(WriterRequest::Shutdown) => {
1007                                        // Push back the shutdown signal by breaking and letting
1008                                        // outer loop handle after batch. We'll process the
1009                                        // current batch first, then break the outer loop on
1010                                        // the next iteration when the channel is empty.
1011                                        break;
1012                                    }
1013                                    Err(mpsc::error::TryRecvError::Empty) => {
1014                                        break;
1015                                    }
1016                                    Err(mpsc::error::TryRecvError::Disconnected) => break,
1017                                }
1018                            }
1019                            // println!("Total events: {total_events}");
1020
1021                            if let (Some(failed_at), Some(orig_err)) = (abort_idx, abort_err) {
1022                                // Abort batch: skip commit; respond to all items in this batch
1023                                let shadow = shadow_for_batch_abort(&orig_err);
1024                                for (i, tx) in responders.into_iter().enumerate() {
1025                                    if i == failed_at {
1026                                        let _ = tx.send(Err(clone_dcb_error(&orig_err)));
1027                                    } else {
1028                                        let _ = tx.send(Err(clone_dcb_error(&shadow)));
1029                                    }
1030                                }
1031                                // Do not update head, since nothing was committed
1032                                continue;
1033                            }
1034
1035                            // Single commit at the end of the batch
1036                            let batch_result = match mvcc.commit(&mut writer) {
1037                                Ok(_) => Ok(results),
1038                                Err(err) => Err(err),
1039                            };
1040
1041                            match batch_result {
1042                                Ok(results) => {
1043                                    // Send individual results back to requesters
1044                                    for (res, tx) in results.into_iter().zip(responders.into_iter())
1045                                    {
1046                                        let _ = tx.send(res);
1047                                    }
1048                                    // After a successful batch commit, publish the updated head from writer.next_position.
1049                                    let last_committed = writer.next_position.0.saturating_sub(1);
1050                                    let new_head = if last_committed == 0 {
1051                                        None
1052                                    } else {
1053                                        Some(last_committed)
1054                                    };
1055                                    let _ = head_tx_writer.send(new_head);
1056                                }
1057                                Err(e) => {
1058                                    // If the batch failed as a whole (e.g., commit failed), propagate the SAME error to all responders.
1059                                    // DCBError is not Clone (contains io::Error), so reconstruct a best-effort copy by using its Display text
1060                                    // for Io and cloning data for other variants.
1061                                    let total = responders.len();
1062                                    let mut iter = responders.into_iter();
1063                                    for _ in 0..total {
1064                                        if let Some(tx) = iter.next() {
1065                                            let _ = tx.send(Err(clone_dcb_error(&e)));
1066                                        }
1067                                    }
1068                                }
1069                            }
1070                        }
1071                        WriterRequest::Shutdown => {
1072                            break;
1073                        }
1074                    }
1075                }
1076            });
1077        });
1078
1079        Ok(Self {
1080            mvcc,
1081            head_watch_tx: head_tx,
1082            writer_request_tx: request_tx,
1083        })
1084    }
1085
1086    fn read(
1087        &self,
1088        query: Option<DcbQuery>,
1089        start: Option<u64>,
1090        backwards: bool,
1091        limit: Option<u32>,
1092        cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
1093    ) -> DcbResult<(Vec<DcbSequencedEvent>, Option<u64>)> {
1094        let reader = self.mvcc.reader()?;
1095        let db_head = if reader.next_position > Position(1) {
1096            Some(reader.next_position.0.saturating_sub(1))
1097        } else {
1098            None
1099        };
1100
1101        let q = query.unwrap_or(DcbQuery { items: vec![] });
1102        let start_position = start.map(Position);
1103
1104        let events = read_conditional(
1105            &self.mvcc,
1106            &std::collections::HashMap::new(),
1107            reader.events_tree_root_id,
1108            reader.tags_tree_root_id,
1109            q,
1110            start_position,
1111            backwards,
1112            limit,
1113            false,
1114            cancel,
1115        )
1116        .map_err(|e| match e {
1117            DcbError::CancelledByUser() => DcbError::CancelledByUser(),
1118            _ => DcbError::Corruption(format!("{e}")),
1119        })?;
1120
1121        Ok((events, db_head))
1122    }
1123
1124    fn head(&self) -> DcbResult<Option<u64>> {
1125        let header_page = self
1126            .mvcc
1127            .get_latest_header_page()
1128            .map_err(|e| DcbError::Corruption(format!("{e}")))?;
1129        let header = header_page
1130            .as_header_node()
1131            .map_err(|e| DcbError::Corruption(format!("{e}")))?;
1132        let last = header.next_position.0.saturating_sub(1);
1133        if last == 0 { Ok(None) } else { Ok(Some(last)) }
1134    }
1135
1136    fn get_tracking_info(&self, source: String) -> DcbResult<Option<u64>> {
1137        let db = UmaDb::from_arc(self.mvcc.clone());
1138        db.get_tracking_info(&source)
1139    }
1140
1141    pub async fn append(
1142        &self,
1143        events: Vec<DcbEvent>,
1144        condition: Option<DcbAppendCondition>,
1145        tracking_info: Option<TrackingInfo>,
1146        cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
1147    ) -> DcbResult<u64> {
1148        // The writer thread is the authoritative enforcer of the append condition and
1149        // idempotency (see `UmaDb::process_append_request`), which evaluates the condition
1150        // against the writer's snapshot — including events appended earlier in the same
1151        // uncommitted batch (`writer.dirty`), which a reader snapshot cannot see.
1152        //
1153        // We deliberately do NOT pre-check the condition here. The former pre-check ran
1154        // blocking storage I/O directly on a Tokio reactor thread, which under high
1155        // concurrency starved HTTP/2 keepalive handling (PING/PONG) and caused
1156        // `KeepAliveTimedOut` disconnects. It also duplicated work the writer must redo
1157        // anyway; its only benefit was advancing the condition's `after` to shorten the
1158        // writer's scan, which is negligible when `after` is already near the head.
1159        let (response_tx, response_rx) = oneshot::channel();
1160
1161        self.writer_request_tx
1162            .send(WriterRequest::Append {
1163                events,
1164                condition,
1165                tracking_info,
1166                response_tx,
1167                cancel,
1168            })
1169            .await
1170            .map_err(|_| {
1171                DcbError::Io(std::io::Error::other(
1172                    "failed to send append request to EventStore thread",
1173                ))
1174            })?;
1175
1176        response_rx.await.map_err(|_| {
1177            DcbError::Io(std::io::Error::other(
1178                "failed to receive append response from EventStore thread",
1179            ))
1180        })?
1181    }
1182
1183    fn watch_head(&self) -> watch::Receiver<Option<u64>> {
1184        self.head_watch_tx.subscribe()
1185    }
1186
1187    #[allow(dead_code)]
1188    async fn shutdown(&self) {
1189        let _ = self.writer_request_tx.send(WriterRequest::Shutdown).await;
1190    }
1191}
1192
1193// Clone implementation for EventStoreHandle
1194impl Clone for RequestHandler {
1195    fn clone(&self) -> Self {
1196        Self {
1197            mvcc: self.mvcc.clone(),
1198            head_watch_tx: self.head_watch_tx.clone(),
1199            writer_request_tx: self.writer_request_tx.clone(),
1200        }
1201    }
1202}