Skip to main content

umadb_server/
lib.rs

1use futures::Stream;
2use std::fs;
3use std::path::Path;
4use std::pin::Pin;
5use std::sync::Arc;
6use std::sync::LazyLock;
7use std::thread;
8use std::time::Instant;
9use tokio::sync::{mpsc, oneshot, watch};
10
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_request_idempotent, 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!("UmaDB server shutdown complete");
307        })
308        .await?;
309
310    Ok(())
311}
312
313// gRPC server implementation
314pub struct DcbServer {
315    pub(crate) request_handler: RequestHandler,
316    shutdown_watch_rx: watch::Receiver<bool>,
317    api_key: Option<String>,
318}
319
320impl DcbServer {
321    pub fn new(
322        shutdown_rx: watch::Receiver<bool>,
323        api_key: Option<String>,
324        storage_options: StorageOptions,
325    ) -> DcbResult<Self> {
326        let command_handler = RequestHandler::new(storage_options)?;
327        Ok(Self {
328            request_handler: command_handler,
329            shutdown_watch_rx: shutdown_rx,
330            api_key,
331        })
332    }
333
334    pub fn into_service(self) -> umadb_proto::v1::dcb_server::DcbServer<Self> {
335        umadb_proto::v1::dcb_server::DcbServer::new(self)
336    }
337
338    fn enforce_api_key(&self, metadata: &tonic::metadata::MetadataMap) -> Result<(), Status> {
339        if let Some(expected) = &self.api_key {
340            let auth = metadata.get("authorization");
341            let expected_val = format!("Bearer {}", expected);
342            let ok = auth
343                .and_then(|m| m.to_str().ok())
344                .map(|s| s == expected_val)
345                .unwrap_or(false);
346            if !ok {
347                return Err(status_from_dcb_error(DcbError::AuthenticationError(
348                    "missing or invalid API key".to_string(),
349                )));
350            }
351        }
352        Ok(())
353    }
354}
355
356#[tonic::async_trait]
357impl umadb_proto::v1::dcb_server::Dcb for DcbServer {
358    type ReadStream =
359        Pin<Box<dyn Stream<Item = Result<umadb_proto::v1::ReadResponse, Status>> + Send + 'static>>;
360    type SubscribeStream = Pin<
361        Box<dyn Stream<Item = Result<umadb_proto::v1::SubscribeResponse, Status>> + Send + 'static>,
362    >;
363
364    async fn read(
365        &self,
366        request: Request<umadb_proto::v1::ReadRequest>,
367    ) -> Result<Response<Self::ReadStream>, Status> {
368        // Enforce API key if configured
369        self.enforce_api_key(request.metadata())?;
370        let read_request = request.into_inner();
371
372        // Convert protobuf query to DCB types
373        let query: Option<DcbQuery> = read_request.query.map(|q| q.into());
374        let start = read_request.start;
375        let backwards = read_request.backwards.unwrap_or(false);
376        let limit = read_request.limit;
377        // Cap requested batch size.
378        let batch_size = read_request
379            .batch_size
380            .unwrap_or(READ_RESPONSE_BATCH_SIZE_DEFAULT)
381            .clamp(1, READ_RESPONSE_BATCH_SIZE_MAX);
382        let subscribe = read_request.subscribe.unwrap_or(false);
383
384        // Create a channel for streaming responses (deeper buffer to reduce backpressure under concurrency)
385        let (tx, rx) = mpsc::channel(2048);
386        // Clone the request handler.
387        let request_handler = self.request_handler.clone();
388        // Clone the shutdown watch receiver.
389        let mut shutdown_watch_rx = self.shutdown_watch_rx.clone();
390
391        let cancel_signal = Arc::new(std::sync::atomic::AtomicBool::new(false));
392        let cancel_signal_for_task = cancel_signal.clone();
393
394        // Spawn a task to handle the read operation and stream multiple batches
395        tokio::spawn(async move {
396            // Ensure we can reuse the same query across batches
397            let query_clone = query;
398            let mut next_start = start;
399            let mut sent_any = false;
400            let mut remaining_limit = limit.unwrap_or(u32::MAX);
401            // Create a watch receiver for head updates only for subscriptions.
402            let mut head_rx = subscribe.then(|| request_handler.watch_head());
403            let mut captured_db_head: Option<u64> = None;
404            let mut have_captured_db_head: bool = false;
405            loop {
406                // TODO: Can remove this check when we sure that cancel_signal_for_task
407                //  is fully respected by all paths in spawn_blocking(handler.read).
408                // Exit if the client has gone away or the server is shutting down.
409                if tx.is_closed() || *shutdown_watch_rx.borrow() {
410                    cancel_signal_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
411                    break;
412                }
413                // Determine per-iteration limit.
414                let read_limit = remaining_limit.min(batch_size);
415                // If subscription and remaining exhausted (limit reached), terminate
416                if limit.is_some() && remaining_limit == 0 {
417                    break;
418                }
419                let handler = request_handler.clone();
420                let query_val = query_clone.clone();
421                let limit_val = Some(read_limit);
422                let cancel_for_blocking = cancel_signal_for_task.clone();
423                let mut blocking_handle = tokio::task::spawn_blocking(move || {
424                    handler.read(
425                        query_val,
426                        next_start,
427                        backwards,
428                        limit_val,
429                        Some(cancel_for_blocking),
430                    )
431                });
432
433                let res = tokio::select! {
434                    res = &mut blocking_handle => {
435                        res.map_err(|e| DcbError::InternalError(e.to_string())).and_then(|res| res)
436                    }
437                    _ = tx.closed() => {
438                        cancel_signal_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
439                        // Await the task to ensure it finishes and doesn't leak
440                        let _ = blocking_handle.await;
441                        break;
442                    }
443                    _ = shutdown_watch_rx.changed() => {
444                        cancel_signal_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
445                        let _ = blocking_handle.await;
446                        break;
447                    }
448                };
449
450                match res {
451                    Ok((dcb_sequenced_events, db_head)) => {
452                        // Capture the db head from the first read (only for non-subscriptions).
453                        if !have_captured_db_head && !subscribe {
454                            captured_db_head = db_head;
455                            have_captured_db_head = true;
456                        }
457
458                        // Capture the original length before consuming events
459                        let original_len = dcb_sequenced_events.len();
460                        let read_less_than_read_limit = (original_len as u32) < read_limit;
461
462                        // Map events to protobuf messages, discarding if position too large.
463                        let sequenced_event_protos: Vec<umadb_proto::v1::SequencedEvent> =
464                            dcb_sequenced_events
465                                .into_iter()
466                                .filter(|e| {
467                                    if let Some(h) = captured_db_head {
468                                        e.position <= h
469                                    } else {
470                                        true
471                                    }
472                                })
473                                .map(umadb_proto::v1::SequencedEvent::from)
474                                .collect();
475
476                        // Check if we filtered out any events
477                        let reached_captured_head = captured_db_head.is_some()
478                            && sequenced_event_protos.len() < original_len;
479
480                        if sequenced_event_protos.is_empty() {
481                            if let Some(head_rx) = head_rx.as_mut() {
482                                // For subscriptions, wait for new events instead of terminating.
483                                tokio::select! {
484                                    _ = head_rx.changed() => {},
485                                    _ = shutdown_watch_rx.changed() => break,
486                                    _ = tx.closed() => break,
487                                }
488                                // Keep looping, it's a subscription.
489                                continue;
490                            } else if !sent_any {
491                                // At least send an empty response to communicate head.
492                                let response = umadb_proto::v1::ReadResponse {
493                                    events: vec![],
494                                    head: if limit.is_some() {
495                                        None
496                                    } else {
497                                        captured_db_head
498                                    },
499                                };
500                                let _ = tx.send(Ok(response)).await;
501                            }
502                            // Stop looping, because there's nothing else to read.
503                            break;
504                        }
505
506                        // Capture values needed after sequenced_event_protos is moved.
507                        let sent_count = sequenced_event_protos.len() as u32;
508
509                        let last_event_position = sequenced_event_protos.last().map(|e| e.position);
510
511                        let response = umadb_proto::v1::ReadResponse {
512                            events: sequenced_event_protos,
513                            head: if subscribe || limit.is_some() {
514                                last_event_position
515                            } else {
516                                captured_db_head
517                            },
518                        };
519
520                        if tx.send(Ok(response)).await.is_err() {
521                            break;
522                        }
523                        sent_any = true;
524
525                        // Advance the cursor (use a new reader on the next loop iteration)
526                        next_start = last_event_position.map(|p| {
527                            if backwards {
528                                p.saturating_sub(1)
529                            } else {
530                                p.saturating_add(1)
531                            }
532                        });
533
534                        // Stop streaming further if we read less than limit or
535                        // reached the captured head boundary (non-subscriber only).
536                        if !subscribe && (read_less_than_read_limit || reached_captured_head) {
537                            break;
538                        }
539
540                        // Decrease the remaining overall limit if any, and stop if reached
541                        if limit.is_some() {
542                            remaining_limit = remaining_limit.saturating_sub(sent_count);
543                            if remaining_limit == 0 {
544                                break;
545                            }
546                        }
547
548                        // Yield to let other tasks progress under high concurrency
549                        tokio::task::yield_now().await;
550                    }
551                    Err(e) => {
552                        if matches!(e, DcbError::CancelledByUser()) {
553                            // Silently stop if cancelled by user
554                        } else {
555                            let _ = tx.send(Err(status_from_dcb_error(e))).await;
556                        }
557                        break;
558                    }
559                }
560            }
561        });
562
563        // Return the receiver as a stream
564        Ok(Response::new(
565            Box::pin(ReceiverStream::new(rx)) as Self::ReadStream
566        ))
567    }
568
569    async fn subscribe(
570        &self,
571        request: Request<umadb_proto::v1::SubscribeRequest>,
572    ) -> Result<Response<Self::SubscribeStream>, Status> {
573        // Enforce API key if configured
574        self.enforce_api_key(request.metadata())?;
575        let subscribe_request = request.into_inner();
576
577        // Convert protobuf query to DCB types
578        let query: Option<DcbQuery> = subscribe_request.query.map(|q| q.into());
579        let after = subscribe_request.after;
580        // Cap requested batch size.
581        let batch_size = subscribe_request
582            .batch_size
583            .unwrap_or(READ_RESPONSE_BATCH_SIZE_DEFAULT)
584            .clamp(1, READ_RESPONSE_BATCH_SIZE_MAX);
585
586        // Create a channel for streaming responses
587        let (tx, rx) = mpsc::channel(2048);
588        // Clone the request handler.
589        let request_handler = self.request_handler.clone();
590        // Clone the shutdown watch receiver.
591        let mut shutdown_watch_rx = self.shutdown_watch_rx.clone();
592
593        let cancel_signal = Arc::new(std::sync::atomic::AtomicBool::new(false));
594        let cancel_signal_for_task = cancel_signal.clone();
595
596        // Spawn a task to handle the subscribe operation and stream multiple batches
597        tokio::spawn(async move {
598            // Ensure we can reuse the same query across batches
599            let query_clone = query;
600            // Todo: End the subscription if after is Some(u64:MAX).
601            let mut next_after = after.map(|a| a.saturating_add(1));
602            // Create a watch receiver for head updates
603            let mut head_rx = request_handler.watch_head();
604
605            loop {
606                // TODO: Can remove this check when we sure that cancel_signal_for_task
607                //  is fully respected by all paths in spawn_blocking(handler.read).
608                // Exit if the client has gone away or the server is shutting down.
609                if tx.is_closed() || *shutdown_watch_rx.borrow() {
610                    cancel_signal_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
611                    break;
612                }
613
614                let handler = request_handler.clone();
615                let query_val = query_clone.clone();
616                let batch_size_val = Some(batch_size);
617                let cancel_for_blocking = cancel_signal_for_task.clone();
618                let mut blocking_handle = tokio::task::spawn_blocking(move || {
619                    handler.read(
620                        query_val,
621                        next_after,
622                        false,
623                        batch_size_val,
624                        Some(cancel_for_blocking),
625                    )
626                });
627
628                let res = tokio::select! {
629                    res = &mut blocking_handle => {
630                        res.map_err(|e| DcbError::InternalError(e.to_string())).and_then(|res| res)
631                    }
632                    _ = tx.closed() => {
633                        cancel_signal_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
634                        let _ = blocking_handle.await;
635                        break;
636                    }
637                    _ = shutdown_watch_rx.changed() => {
638                        cancel_signal_for_task.store(true, std::sync::atomic::Ordering::SeqCst);
639                        let _ = blocking_handle.await;
640                        break;
641                    }
642                };
643
644                match res {
645                    Ok((dcb_sequenced_events, _unused_db_head)) => {
646                        // Map events to protobuf type
647                        let sequenced_event_protos: Vec<umadb_proto::v1::SequencedEvent> =
648                            dcb_sequenced_events
649                                .into_iter()
650                                .map(umadb_proto::v1::SequencedEvent::from)
651                                .collect();
652
653                        if sequenced_event_protos.is_empty() {
654                            // For subscriptions, wait for new events instead of terminating
655                            tokio::select! {
656                                _ = head_rx.changed() => {},
657                                _ = shutdown_watch_rx.changed() => {},
658                                _ = tx.closed() => {},
659                            }
660                            continue;
661                        }
662
663                        let last_event_position = sequenced_event_protos.last().map(|e| e.position);
664
665                        let response = umadb_proto::v1::SubscribeResponse {
666                            events: sequenced_event_protos,
667                        };
668
669                        if tx.send(Ok(response)).await.is_err() {
670                            break;
671                        }
672
673                        // Advance the cursor (use a new reader on the next loop iteration)
674                        // Todo: End the subscription if last_event_position is Some(u64:MAX).
675                        next_after = last_event_position.map(|p| p.saturating_add(1));
676
677                        // Yield to let other tasks progress under high concurrency
678                        tokio::task::yield_now().await;
679                    }
680                    Err(e) => {
681                        if matches!(e, DcbError::CancelledByUser()) {
682                            // Silently stop if cancelled by user
683                        } else {
684                            let _ = tx.send(Err(status_from_dcb_error(e))).await;
685                        }
686                        break;
687                    }
688                }
689            }
690        });
691
692        // Return the receiver as a stream
693        Ok(Response::new(
694            Box::pin(ReceiverStream::new(rx)) as Self::SubscribeStream
695        ))
696    }
697
698    async fn append(
699        &self,
700        request: Request<umadb_proto::v1::AppendRequest>,
701    ) -> Result<Response<umadb_proto::v1::AppendResponse>, Status> {
702        // Enforce API key if configured
703        self.enforce_api_key(request.metadata())?;
704        let req = request.into_inner();
705
706        // Convert protobuf types to API types
707        let events: Vec<DcbEvent> = match req.events.into_iter().map(|e| e.try_into()).collect() {
708            Ok(events) => events,
709            Err(e) => {
710                return Err(status_from_dcb_error(e));
711            }
712        };
713        let condition = req.condition.map(|c| c.into());
714
715        let cancel_signal = Arc::new(std::sync::atomic::AtomicBool::new(false));
716        let cancel_signal_for_task = cancel_signal.clone();
717
718        // Create a way to watch for the request being cancelled/dropped
719        let (cancel_tx, cancel_rx) = oneshot::channel();
720        let _guard = CancellationGuard(Some(cancel_tx));
721
722        // Spawn a monitoring task that survives the gRPC future being dropped
723        let cancel_signal_for_monitoring = cancel_signal.clone();
724        tokio::spawn(async move {
725            // This resolves when _guard is dropped (client disconnects)
726            // or when the gRPC method finishes normally.
727            let _ = cancel_rx.await;
728            cancel_signal_for_monitoring.store(true, std::sync::atomic::Ordering::SeqCst);
729        });
730
731        // Call the event store append method
732        let res = self
733            .request_handler
734            .append(
735                events,
736                condition,
737                req.tracking_info.map(|t| TrackingInfo {
738                    source: t.source,
739                    position: t.position,
740                }),
741                Some(cancel_signal_for_task.clone()),
742            )
743            .await;
744
745        match res {
746            Ok(position) => Ok(Response::new(umadb_proto::v1::AppendResponse { position })),
747            Err(e) => Err(status_from_dcb_error(e)),
748        }
749    }
750
751    async fn head(
752        &self,
753        request: Request<umadb_proto::v1::HeadRequest>,
754    ) -> Result<Response<umadb_proto::v1::HeadResponse>, Status> {
755        // Enforce API key if configured
756        self.enforce_api_key(request.metadata())?;
757        // Call the event store head method
758        match self.request_handler.head() {
759            Ok(position) => {
760                // Return the position as a response
761                Ok(Response::new(umadb_proto::v1::HeadResponse { position }))
762            }
763            Err(e) => Err(status_from_dcb_error(e)),
764        }
765    }
766
767    async fn get_tracking_info(
768        &self,
769        request: Request<umadb_proto::v1::TrackingRequest>,
770    ) -> Result<Response<umadb_proto::v1::TrackingResponse>, Status> {
771        // Enforce API key if configured
772        self.enforce_api_key(request.metadata())?;
773        let req = request.into_inner();
774        match self.request_handler.get_tracking_info(req.source) {
775            Ok(position) => Ok(Response::new(umadb_proto::v1::TrackingResponse {
776                position,
777            })),
778            Err(e) => Err(status_from_dcb_error(e)),
779        }
780    }
781}
782
783// Message types for communication between the gRPC server and the request handler's writer thread
784enum WriterRequest {
785    Append {
786        events: Vec<DcbEvent>,
787        condition: Option<DcbAppendCondition>,
788        tracking_info: Option<TrackingInfo>,
789        response_tx: oneshot::Sender<DcbResult<u64>>,
790        cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
791    },
792    Shutdown,
793}
794
795// Thread-safe request handler
796struct RequestHandler {
797    mvcc: Arc<Mvcc>,
798    head_watch_tx: watch::Sender<Option<u64>>,
799    writer_request_tx: mpsc::Sender<WriterRequest>,
800}
801
802impl RequestHandler {
803    fn new(storage_options: StorageOptions) -> DcbResult<Self> {
804        // Create a channel for sending requests to the writer thread
805        let (request_tx, mut request_rx) = mpsc::channel::<WriterRequest>(1024);
806
807        // Build a shared Mvcc instance (Arc) upfront so reads can proceed concurrently
808        let mvcc = Arc::new(Mvcc::new(false, storage_options)?);
809
810        // Initialize the head watch channel with the current head.
811        let init_head = {
812            let header_page = mvcc.get_latest_header_page()?;
813            let header = header_page.as_header_node()?;
814            let last = header.next_position.0.saturating_sub(1);
815            if last == 0 { None } else { Some(last) }
816        };
817        let (head_tx, _head_rx) = watch::channel::<Option<u64>>(init_head);
818
819        // Spawn a thread for processing writer requests.
820        let mvcc_for_writer = mvcc.clone();
821        let head_tx_writer = head_tx.clone();
822        thread::spawn(move || {
823            let db = UmaDb::from_arc(mvcc_for_writer);
824
825            // Create a runtime for processing writer requests.
826            let rt = Runtime::new().unwrap();
827
828            // Process writer requests.
829            rt.block_on(async {
830                while let Some(request) = request_rx.recv().await {
831                    match request {
832                        WriterRequest::Append {
833                            events,
834                            condition,
835                            tracking_info,
836                            response_tx,
837                            cancel,
838                        } => {
839                            // Batch processing: drain any immediately available requests
840                            // let mut items: Vec<(Vec<DCBEvent>, Option<DCBAppendCondition>)> =
841                            //     Vec::new();
842
843                            let mut total_events = 0;
844                            total_events += events.len();
845                            // items.push((events, condition));
846
847                            let mvcc = &db.mvcc;
848                            let mut writer = match mvcc.writer() {
849                                Ok(writer) => writer,
850                                Err(err) => {
851                                    let _ = response_tx.send(Err(err));
852                                    continue;
853                                }
854                            };
855
856                            let mut responders: Vec<oneshot::Sender<DcbResult<u64>>> = Vec::new();
857                            let mut results: Vec<DcbResult<u64>> = Vec::new();
858
859                            // Track abort state for non-integrity error within the batch
860                            let mut abort_idx: Option<usize> = None;
861                            let mut abort_err: Option<DcbError> = None;
862
863                            responders.push(response_tx);
864                            let result = UmaDb::process_append_request(
865                                events,
866                                condition,
867                                tracking_info,
868                                mvcc,
869                                &mut writer,
870                                cancel,
871                            );
872                            // Record result and possibly mark abort
873                            match &result {
874                                Ok(_) => results.push(result),
875                                Err(e) if is_integrity_error(e) => {
876                                    results.push(Err(clone_dcb_error(e)))
877                                }
878                                Err(e) => {
879                                    abort_idx = Some(0);
880                                    abort_err = Some(clone_dcb_error(e));
881                                    results.push(Err(clone_dcb_error(e)));
882                                }
883                            }
884
885                            // Drain the channel for more pending writer requests without awaiting.
886                            // Important: do not drop a popped request when hitting the batch limit.
887                            // We stop draining BEFORE attempting to recv if we've reached the limit.
888                            loop {
889                                if total_events >= APPEND_BATCH_MAX_EVENTS {
890                                    break;
891                                }
892                                // Stop draining if we've already decided to abort
893                                if abort_idx.is_some() {
894                                    break;
895                                }
896                                match request_rx.try_recv() {
897                                    Ok(WriterRequest::Append {
898                                        events,
899                                        condition,
900                                        tracking_info,
901                                        response_tx,
902                                        cancel,
903                                    }) => {
904                                        let ev_len = events.len();
905                                        let idx_in_batch = responders.len();
906                                        responders.push(response_tx);
907                                        let res_next = UmaDb::process_append_request(
908                                            events,
909                                            condition,
910                                            tracking_info,
911                                            mvcc,
912                                            &mut writer,
913                                            cancel,
914                                        );
915                                        match &res_next {
916                                            Ok(_) => results.push(res_next),
917                                            Err(e) if is_integrity_error(e) => {
918                                                results.push(Err(clone_dcb_error(e)))
919                                            }
920                                            Err(e) => {
921                                                abort_idx = Some(idx_in_batch);
922                                                abort_err = Some(clone_dcb_error(e));
923                                                results.push(Err(clone_dcb_error(e)));
924                                                // Do not accumulate more into the batch
925                                            }
926                                        }
927                                        total_events += ev_len;
928                                    }
929                                    Ok(WriterRequest::Shutdown) => {
930                                        // Push back the shutdown signal by breaking and letting
931                                        // outer loop handle after batch. We'll process the
932                                        // current batch first, then break the outer loop on
933                                        // the next iteration when the channel is empty.
934                                        break;
935                                    }
936                                    Err(mpsc::error::TryRecvError::Empty) => {
937                                        break;
938                                    }
939                                    Err(mpsc::error::TryRecvError::Disconnected) => break,
940                                }
941                            }
942                            // println!("Total events: {total_events}");
943
944                            if let (Some(failed_at), Some(orig_err)) = (abort_idx, abort_err) {
945                                // Abort batch: skip commit; respond to all items in this batch
946                                let shadow = shadow_for_batch_abort(&orig_err);
947                                for (i, tx) in responders.into_iter().enumerate() {
948                                    if i == failed_at {
949                                        let _ = tx.send(Err(clone_dcb_error(&orig_err)));
950                                    } else {
951                                        let _ = tx.send(Err(clone_dcb_error(&shadow)));
952                                    }
953                                }
954                                // Do not update head, since nothing was committed
955                                continue;
956                            }
957
958                            // Single commit at the end of the batch
959                            let batch_result = match mvcc.commit(&mut writer) {
960                                Ok(_) => Ok(results),
961                                Err(err) => Err(err),
962                            };
963
964                            match batch_result {
965                                Ok(results) => {
966                                    // Send individual results back to requesters
967                                    for (res, tx) in results.into_iter().zip(responders.into_iter())
968                                    {
969                                        let _ = tx.send(res);
970                                    }
971                                    // After a successful batch commit, publish the updated head from writer.next_position.
972                                    let last_committed = writer.next_position.0.saturating_sub(1);
973                                    let new_head = if last_committed == 0 {
974                                        None
975                                    } else {
976                                        Some(last_committed)
977                                    };
978                                    let _ = head_tx_writer.send(new_head);
979                                }
980                                Err(e) => {
981                                    // If the batch failed as a whole (e.g., commit failed), propagate the SAME error to all responders.
982                                    // DCBError is not Clone (contains io::Error), so reconstruct a best-effort copy by using its Display text
983                                    // for Io and cloning data for other variants.
984                                    let total = responders.len();
985                                    let mut iter = responders.into_iter();
986                                    for _ in 0..total {
987                                        if let Some(tx) = iter.next() {
988                                            let _ = tx.send(Err(clone_dcb_error(&e)));
989                                        }
990                                    }
991                                }
992                            }
993                        }
994                        WriterRequest::Shutdown => {
995                            break;
996                        }
997                    }
998                }
999            });
1000        });
1001
1002        Ok(Self {
1003            mvcc,
1004            head_watch_tx: head_tx,
1005            writer_request_tx: request_tx,
1006        })
1007    }
1008
1009    fn read(
1010        &self,
1011        query: Option<DcbQuery>,
1012        start: Option<u64>,
1013        backwards: bool,
1014        limit: Option<u32>,
1015        cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
1016    ) -> DcbResult<(Vec<DcbSequencedEvent>, Option<u64>)> {
1017        let reader = self.mvcc.reader()?;
1018        let db_head = if reader.next_position > Position(1) {
1019            Some(reader.next_position.0.saturating_sub(1))
1020        } else {
1021            None
1022        };
1023
1024        let q = query.unwrap_or(DcbQuery { items: vec![] });
1025        let start_position = start.map(Position);
1026
1027        let events = read_conditional(
1028            &self.mvcc,
1029            &std::collections::HashMap::new(),
1030            reader.events_tree_root_id,
1031            reader.tags_tree_root_id,
1032            q,
1033            start_position,
1034            backwards,
1035            limit,
1036            false,
1037            cancel,
1038        )
1039        .map_err(|e| match e {
1040            DcbError::CancelledByUser() => DcbError::CancelledByUser(),
1041            _ => DcbError::Corruption(format!("{e}")),
1042        })?;
1043
1044        Ok((events, db_head))
1045    }
1046
1047    fn head(&self) -> DcbResult<Option<u64>> {
1048        let header_page = self
1049            .mvcc
1050            .get_latest_header_page()
1051            .map_err(|e| DcbError::Corruption(format!("{e}")))?;
1052        let header = header_page
1053            .as_header_node()
1054            .map_err(|e| DcbError::Corruption(format!("{e}")))?;
1055        let last = header.next_position.0.saturating_sub(1);
1056        if last == 0 { Ok(None) } else { Ok(Some(last)) }
1057    }
1058
1059    fn get_tracking_info(&self, source: String) -> DcbResult<Option<u64>> {
1060        let db = UmaDb::from_arc(self.mvcc.clone());
1061        db.get_tracking_info(&source)
1062    }
1063
1064    pub async fn append(
1065        &self,
1066        events: Vec<DcbEvent>,
1067        condition: Option<DcbAppendCondition>,
1068        tracking_info: Option<TrackingInfo>,
1069        cancel: Option<Arc<std::sync::atomic::AtomicBool>>,
1070    ) -> DcbResult<u64> {
1071        // Concurrent pre-check of the given condition using a reader in a blocking thread.
1072        let pre_append_decision = if let Some(mut given_condition) = condition {
1073            let reader = self.mvcc.reader()?;
1074            let current_head = {
1075                let last = reader.next_position.0.saturating_sub(1);
1076                if last == 0 { None } else { Some(last) }
1077            };
1078
1079            // Perform conditional read on the snapshot (limit 1) starting after the given position
1080            let from = given_condition.after.map(|after| Position(after + 1));
1081            let empty_dirty = std::collections::HashMap::new();
1082            let found = read_conditional(
1083                &self.mvcc,
1084                &empty_dirty,
1085                reader.events_tree_root_id,
1086                reader.tags_tree_root_id,
1087                given_condition.fail_if_events_match.clone(),
1088                from,
1089                false,
1090                Some(1),
1091                false,
1092                None,
1093            )?;
1094
1095            if let Some(matched) = found.first() {
1096                // Found one event — consider if the request is idempotent...
1097                match is_request_idempotent(
1098                    &self.mvcc,
1099                    &empty_dirty,
1100                    reader.events_tree_root_id,
1101                    reader.tags_tree_root_id,
1102                    &events,
1103                    given_condition.fail_if_events_match.clone(),
1104                    from,
1105                    cancel.clone(),
1106                ) {
1107                    Ok(Some(last_recorded_position)) => {
1108                        // Request is idempotent; skip actual append
1109                        PreAppendDecision::AlreadyAppended(last_recorded_position)
1110                    }
1111                    Ok(None) => {
1112                        // Integrity violation
1113                        let msg = format!(
1114                            "condition: {:?} matched: {:?}",
1115                            given_condition.clone(),
1116                            matched,
1117                        );
1118                        return Err(DcbError::IntegrityError(msg));
1119                    }
1120                    Err(err) => {
1121                        // Propagate underlying read error
1122                        return Err(err);
1123                    }
1124                }
1125            } else {
1126                // No match found: we can advance 'after' to the current head observed by this reader
1127                let new_after = std::cmp::max(
1128                    given_condition.after.unwrap_or(0),
1129                    current_head.unwrap_or(0),
1130                );
1131                given_condition.after = Some(new_after);
1132
1133                PreAppendDecision::UseCondition(Some(given_condition))
1134            }
1135        } else {
1136            // No condition provided at all
1137            PreAppendDecision::UseCondition(None)
1138        };
1139
1140        // Handle the pre-check decision
1141        match pre_append_decision {
1142            PreAppendDecision::AlreadyAppended(last_found_position) => {
1143                // Request was idempotent — just return the existing position.
1144                Ok(last_found_position)
1145            }
1146            PreAppendDecision::UseCondition(adjusted_condition) => {
1147                // Proceed with append operation on the writer thread.
1148                let (response_tx, response_rx) = oneshot::channel();
1149
1150                self.writer_request_tx
1151                    .send(WriterRequest::Append {
1152                        events,
1153                        condition: adjusted_condition,
1154                        tracking_info,
1155                        response_tx,
1156                        cancel,
1157                    })
1158                    .await
1159                    .map_err(|_| {
1160                        DcbError::Io(std::io::Error::other(
1161                            "failed to send append request to EventStore thread",
1162                        ))
1163                    })?;
1164
1165                response_rx.await.map_err(|_| {
1166                    DcbError::Io(std::io::Error::other(
1167                        "failed to receive append response from EventStore thread",
1168                    ))
1169                })?
1170            }
1171        }
1172    }
1173
1174    fn watch_head(&self) -> watch::Receiver<Option<u64>> {
1175        self.head_watch_tx.subscribe()
1176    }
1177
1178    #[allow(dead_code)]
1179    async fn shutdown(&self) {
1180        let _ = self.writer_request_tx.send(WriterRequest::Shutdown).await;
1181    }
1182}
1183
1184// Clone implementation for EventStoreHandle
1185impl Clone for RequestHandler {
1186    fn clone(&self) -> Self {
1187        Self {
1188            mvcc: self.mvcc.clone(),
1189            head_watch_tx: self.head_watch_tx.clone(),
1190            writer_request_tx: self.writer_request_tx.clone(),
1191        }
1192    }
1193}
1194
1195#[derive(Debug)]
1196enum PreAppendDecision {
1197    /// Proceed with this (possibly adjusted) condition
1198    UseCondition(Option<DcbAppendCondition>),
1199    /// Skip append operation because the request was idempotent; return last recorded position
1200    AlreadyAppended(u64),
1201}