Skip to main content

umadb_server/
lib.rs

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