Skip to main content

runtara_core/
runtime.rs

1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Embeddable runtime for runtara-core.
4//!
5//! This module provides [`CoreRuntime`] which allows embedding runtara-core
6//! into an existing tokio application instead of running it as a standalone server.
7//!
8//! # Example
9//!
10//! ```rust,ignore
11//! use std::sync::Arc;
12//! use runtara_core::runtime::CoreRuntime;
13//! use runtara_core::persistence::PostgresPersistence;
14//!
15//! #[tokio::main]
16//! async fn main() -> anyhow::Result<()> {
17//!     let pool = sqlx::PgPool::connect("postgres://...").await?;
18//!     let persistence = Arc::new(PostgresPersistence::new(pool));
19//!
20//!     let runtime = CoreRuntime::builder()
21//!         .persistence(persistence)
22//!         .bind_addr("0.0.0.0:8001".parse()?)
23//!         .build()?
24//!         .start()
25//!         .await?;
26//!
27//!     // ... run your application ...
28//!
29//!     // Graceful shutdown
30//!     runtime.shutdown().await?;
31//!     Ok(())
32//! }
33//! ```
34
35use std::net::SocketAddr;
36use std::sync::Arc;
37
38use anyhow::Result;
39use tokio::task::JoinHandle;
40use tracing::{error, info};
41
42use crate::instance_handlers::InstanceHandlerState;
43use crate::persistence::Persistence;
44use crate::server::InstanceServerState;
45
46/// Builder for creating a [`CoreRuntime`].
47pub struct CoreRuntimeBuilder {
48    persistence: Option<Arc<dyn Persistence>>,
49    bind_addr: SocketAddr,
50}
51
52impl std::fmt::Debug for CoreRuntimeBuilder {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        f.debug_struct("CoreRuntimeBuilder")
55            .field("persistence", &self.persistence.as_ref().map(|_| "..."))
56            .field("bind_addr", &self.bind_addr)
57            .finish()
58    }
59}
60
61impl Default for CoreRuntimeBuilder {
62    fn default() -> Self {
63        Self {
64            persistence: None,
65            bind_addr: "0.0.0.0:8001".parse().unwrap(),
66        }
67    }
68}
69
70impl CoreRuntimeBuilder {
71    /// Create a new builder with default settings.
72    pub fn new() -> Self {
73        Self::default()
74    }
75
76    /// Set the persistence layer (required).
77    pub fn persistence(mut self, persistence: Arc<dyn Persistence>) -> Self {
78        self.persistence = Some(persistence);
79        self
80    }
81
82    /// Set the bind address for the HTTP server.
83    ///
84    /// Default: `0.0.0.0:8001`
85    pub fn bind_addr(mut self, addr: SocketAddr) -> Self {
86        self.bind_addr = addr;
87        self
88    }
89
90    /// Build the runtime configuration.
91    ///
92    /// Returns an error if required fields are missing.
93    pub fn build(self) -> Result<CoreRuntimeConfig> {
94        let persistence = self
95            .persistence
96            .ok_or_else(|| anyhow::anyhow!("persistence is required"))?;
97
98        Ok(CoreRuntimeConfig {
99            persistence,
100            bind_addr: self.bind_addr,
101        })
102    }
103}
104
105/// Configuration for a [`CoreRuntime`].
106pub struct CoreRuntimeConfig {
107    persistence: Arc<dyn Persistence>,
108    bind_addr: SocketAddr,
109}
110
111impl std::fmt::Debug for CoreRuntimeConfig {
112    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113        f.debug_struct("CoreRuntimeConfig")
114            .field("persistence", &"...")
115            .field("bind_addr", &self.bind_addr)
116            .finish()
117    }
118}
119
120impl CoreRuntimeConfig {
121    /// Start the runtime, spawning the HTTP server task.
122    pub async fn start(self) -> Result<CoreRuntime> {
123        let state = Arc::new(InstanceHandlerState::new(self.persistence));
124
125        let bind_addr = self.bind_addr;
126        let server_state = state.clone();
127        let server_handle = tokio::spawn(async move {
128            crate::server::http_server::run_http_server(bind_addr, server_state).await
129        });
130
131        info!(addr = %bind_addr, "CoreRuntime started");
132
133        Ok(CoreRuntime {
134            server_handle,
135            state,
136            bind_addr,
137        })
138    }
139}
140
141/// A running runtara-core instance that can be embedded in an application.
142///
143/// The runtime manages:
144/// - HTTP server for instance connections (checkpoints, signals, events)
145///
146/// Call [`shutdown`](Self::shutdown) for graceful termination.
147pub struct CoreRuntime {
148    server_handle: JoinHandle<anyhow::Result<()>>,
149    state: Arc<InstanceServerState>,
150    bind_addr: SocketAddr,
151}
152
153impl CoreRuntime {
154    /// Create a new builder for configuring the runtime.
155    pub fn builder() -> CoreRuntimeBuilder {
156        CoreRuntimeBuilder::new()
157    }
158
159    /// Get the bind address of the HTTP server.
160    pub fn bind_addr(&self) -> SocketAddr {
161        self.bind_addr
162    }
163
164    /// Get a reference to the shared instance handler state.
165    ///
166    /// This can be used for direct access to persistence or other shared resources.
167    pub fn state(&self) -> &Arc<InstanceServerState> {
168        &self.state
169    }
170
171    /// Get a reference to the persistence layer.
172    pub fn persistence(&self) -> &Arc<dyn Persistence> {
173        &self.state.persistence
174    }
175
176    /// Gracefully shut down the runtime.
177    ///
178    /// This aborts the HTTP server and waits for it to complete.
179    pub async fn shutdown(self) -> Result<()> {
180        info!("CoreRuntime shutting down...");
181
182        // Abort HTTP server
183        self.server_handle.abort();
184
185        match self.server_handle.await {
186            Ok(Ok(())) => {
187                info!("CoreRuntime shutdown complete");
188                Ok(())
189            }
190            Ok(Err(e)) => {
191                error!("CoreRuntime server error during shutdown: {}", e);
192                Err(e)
193            }
194            Err(e) if e.is_cancelled() => {
195                info!("CoreRuntime shutdown complete");
196                Ok(())
197            }
198            Err(e) => {
199                error!("CoreRuntime server task panicked: {}", e);
200                Err(anyhow::anyhow!("server task panicked: {}", e))
201            }
202        }
203    }
204
205    /// Check if the runtime is still running.
206    pub fn is_running(&self) -> bool {
207        !self.server_handle.is_finished()
208    }
209}
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214    use crate::error::CoreError;
215    use crate::persistence::{
216        CheckpointRecord, CustomSignalRecord, EventRecord, InstanceRecord, ListEventsFilter,
217        ListStepSummariesFilter, Persistence, SignalRecord, StepSummaryRecord,
218    };
219    use async_trait::async_trait;
220    use chrono::{DateTime, Utc};
221
222    /// Mock persistence for testing the runtime builder without database.
223    struct MockPersistence;
224
225    #[async_trait]
226    impl Persistence for MockPersistence {
227        async fn register_instance(
228            &self,
229            _instance_id: &str,
230            _tenant_id: &str,
231        ) -> Result<(), CoreError> {
232            Ok(())
233        }
234
235        async fn get_instance(
236            &self,
237            _instance_id: &str,
238        ) -> Result<Option<InstanceRecord>, CoreError> {
239            Ok(None)
240        }
241
242        async fn update_instance_status(
243            &self,
244            _instance_id: &str,
245            _status: &str,
246            _started_at: Option<DateTime<Utc>>,
247        ) -> Result<(), CoreError> {
248            Ok(())
249        }
250
251        async fn update_instance_checkpoint(
252            &self,
253            _instance_id: &str,
254            _checkpoint_id: &str,
255        ) -> Result<(), CoreError> {
256            Ok(())
257        }
258
259        async fn complete_instance(
260            &self,
261            _instance_id: &str,
262            _output: Option<&[u8]>,
263            _error: Option<&str>,
264        ) -> Result<(), CoreError> {
265            Ok(())
266        }
267
268        async fn save_checkpoint(
269            &self,
270            _instance_id: &str,
271            _checkpoint_id: &str,
272            _state: &[u8],
273        ) -> Result<(), CoreError> {
274            Ok(())
275        }
276
277        async fn load_checkpoint(
278            &self,
279            _instance_id: &str,
280            _checkpoint_id: &str,
281        ) -> Result<Option<CheckpointRecord>, CoreError> {
282            Ok(None)
283        }
284
285        async fn list_checkpoints(
286            &self,
287            _instance_id: &str,
288            _checkpoint_id: Option<&str>,
289            _limit: i64,
290            _offset: i64,
291            _created_after: Option<DateTime<Utc>>,
292            _created_before: Option<DateTime<Utc>>,
293        ) -> Result<Vec<CheckpointRecord>, CoreError> {
294            Ok(Vec::new())
295        }
296
297        async fn count_checkpoints(
298            &self,
299            _instance_id: &str,
300            _checkpoint_id: Option<&str>,
301            _created_after: Option<DateTime<Utc>>,
302            _created_before: Option<DateTime<Utc>>,
303        ) -> Result<i64, CoreError> {
304            Ok(0)
305        }
306
307        async fn insert_event(&self, _event: &EventRecord) -> Result<(), CoreError> {
308            Ok(())
309        }
310
311        async fn insert_signal(
312            &self,
313            _instance_id: &str,
314            _signal_type: &str,
315            _payload: &[u8],
316        ) -> Result<(), CoreError> {
317            Ok(())
318        }
319
320        async fn get_pending_signal(
321            &self,
322            _instance_id: &str,
323        ) -> Result<Option<SignalRecord>, CoreError> {
324            Ok(None)
325        }
326
327        async fn acknowledge_signal(&self, _instance_id: &str) -> Result<(), CoreError> {
328            Ok(())
329        }
330
331        async fn insert_custom_signal(
332            &self,
333            _instance_id: &str,
334            _checkpoint_id: &str,
335            _payload: &[u8],
336        ) -> Result<(), CoreError> {
337            Ok(())
338        }
339
340        async fn take_pending_custom_signal(
341            &self,
342            _instance_id: &str,
343            _checkpoint_id: &str,
344        ) -> Result<Option<CustomSignalRecord>, CoreError> {
345            Ok(None)
346        }
347
348        async fn save_retry_attempt(
349            &self,
350            _instance_id: &str,
351            _checkpoint_id: &str,
352            _attempt: i32,
353            _error_message: Option<&str>,
354        ) -> Result<(), CoreError> {
355            Ok(())
356        }
357
358        async fn list_instances(
359            &self,
360            _tenant_id: Option<&str>,
361            _status: Option<&str>,
362            _limit: i64,
363            _offset: i64,
364        ) -> Result<Vec<InstanceRecord>, CoreError> {
365            Ok(Vec::new())
366        }
367
368        async fn health_check_db(&self) -> Result<bool, CoreError> {
369            Ok(true)
370        }
371
372        async fn count_active_instances(&self) -> Result<i64, CoreError> {
373            Ok(0)
374        }
375
376        async fn set_instance_sleep(
377            &self,
378            _instance_id: &str,
379            _sleep_until: DateTime<Utc>,
380        ) -> Result<(), CoreError> {
381            Ok(())
382        }
383
384        async fn clear_instance_sleep(&self, _instance_id: &str) -> Result<(), CoreError> {
385            Ok(())
386        }
387
388        async fn get_sleeping_instances_due(
389            &self,
390            _limit: i64,
391        ) -> Result<Vec<InstanceRecord>, CoreError> {
392            Ok(Vec::new())
393        }
394
395        async fn list_events(
396            &self,
397            _instance_id: &str,
398            _filter: &ListEventsFilter,
399            _limit: i64,
400            _offset: i64,
401        ) -> Result<Vec<EventRecord>, CoreError> {
402            Ok(Vec::new())
403        }
404
405        async fn count_events(
406            &self,
407            _instance_id: &str,
408            _filter: &ListEventsFilter,
409        ) -> Result<i64, CoreError> {
410            Ok(0)
411        }
412
413        async fn list_step_summaries(
414            &self,
415            _instance_id: &str,
416            _filter: &ListStepSummariesFilter,
417            _limit: i64,
418            _offset: i64,
419        ) -> Result<Vec<StepSummaryRecord>, CoreError> {
420            Ok(Vec::new())
421        }
422
423        async fn count_step_summaries(
424            &self,
425            _instance_id: &str,
426            _filter: &ListStepSummariesFilter,
427        ) -> Result<i64, CoreError> {
428            Ok(0)
429        }
430    }
431
432    #[test]
433    fn test_builder_default() {
434        let builder = CoreRuntimeBuilder::default();
435        assert!(builder.persistence.is_none());
436        assert_eq!(builder.bind_addr.port(), 8001);
437    }
438
439    #[test]
440    fn test_builder_new() {
441        let builder = CoreRuntimeBuilder::new();
442        assert!(builder.persistence.is_none());
443        assert_eq!(builder.bind_addr.port(), 8001);
444    }
445
446    #[test]
447    fn test_builder_persistence() {
448        let persistence = Arc::new(MockPersistence);
449        let builder = CoreRuntimeBuilder::new().persistence(persistence);
450        assert!(builder.persistence.is_some());
451    }
452
453    #[test]
454    fn test_builder_bind_addr() {
455        let addr: SocketAddr = "127.0.0.1:9000".parse().unwrap();
456        let builder = CoreRuntimeBuilder::new().bind_addr(addr);
457        assert_eq!(builder.bind_addr.port(), 9000);
458    }
459
460    #[test]
461    fn test_builder_chaining() {
462        let persistence = Arc::new(MockPersistence);
463        let addr: SocketAddr = "127.0.0.1:9001".parse().unwrap();
464        let builder = CoreRuntimeBuilder::new()
465            .persistence(persistence)
466            .bind_addr(addr);
467        assert!(builder.persistence.is_some());
468        assert_eq!(builder.bind_addr.port(), 9001);
469    }
470
471    #[test]
472    fn test_builder_debug() {
473        let builder = CoreRuntimeBuilder::new();
474        let debug_str = format!("{:?}", builder);
475        assert!(debug_str.contains("CoreRuntimeBuilder"));
476        assert!(debug_str.contains("bind_addr"));
477    }
478
479    #[test]
480    fn test_builder_debug_with_persistence() {
481        let persistence = Arc::new(MockPersistence);
482        let builder = CoreRuntimeBuilder::new().persistence(persistence);
483        let debug_str = format!("{:?}", builder);
484        assert!(debug_str.contains("CoreRuntimeBuilder"));
485        // persistence is shown as "..." to avoid leaking details
486        assert!(debug_str.contains("..."));
487    }
488
489    #[test]
490    fn test_builder_build_missing_persistence() {
491        let result = CoreRuntimeBuilder::new().build();
492        assert!(result.is_err());
493        let err = result.unwrap_err();
494        assert!(err.to_string().contains("persistence is required"));
495    }
496
497    #[test]
498    fn test_builder_build_success() {
499        let persistence = Arc::new(MockPersistence);
500        let result = CoreRuntimeBuilder::new().persistence(persistence).build();
501        assert!(result.is_ok());
502        let config = result.unwrap();
503        assert_eq!(config.bind_addr.port(), 8001);
504    }
505
506    #[test]
507    fn test_builder_build_with_custom_addr() {
508        let persistence = Arc::new(MockPersistence);
509        let addr: SocketAddr = "0.0.0.0:9002".parse().unwrap();
510        let result = CoreRuntimeBuilder::new()
511            .persistence(persistence)
512            .bind_addr(addr)
513            .build();
514        assert!(result.is_ok());
515        let config = result.unwrap();
516        assert_eq!(config.bind_addr.port(), 9002);
517    }
518
519    #[test]
520    fn test_core_runtime_builder_static_method() {
521        let builder = CoreRuntime::builder();
522        assert!(builder.persistence.is_none());
523    }
524
525    #[tokio::test]
526    async fn test_runtime_start_and_shutdown() {
527        let persistence = Arc::new(MockPersistence);
528        // Use port 0 to let OS assign an available port
529        let addr: SocketAddr = "127.0.0.1:0".parse().unwrap();
530
531        let config = CoreRuntimeBuilder::new()
532            .persistence(persistence)
533            .bind_addr(addr)
534            .build()
535            .unwrap();
536
537        let runtime = config.start().await;
538        // Start may fail in CI environments without network access
539        if let Ok(runtime) = runtime {
540            assert!(runtime.is_running());
541
542            // bind_addr() returns the configured addr (port 0 if OS-assigned)
543            // Just verify we can call it
544            let _actual_addr = runtime.bind_addr();
545
546            // Get persistence reference
547            let _persistence = runtime.persistence();
548            let _state = runtime.state();
549
550            // Shutdown
551            let result = runtime.shutdown().await;
552            assert!(result.is_ok());
553        }
554    }
555}