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