runtara_sdk/registry.rs
1// Copyright (C) 2025 SyncMyOrders Sp. z o.o.
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Global SDK registry for the #[durable] macro.
4//!
5//! This module provides global SDK registration so the #[durable] macro
6//! can access the SDK without explicit passing. It also spawns a background
7//! heartbeat task to keep instances alive during long-running operations.
8
9use std::sync::Arc;
10use std::time::Duration;
11
12use once_cell::sync::OnceCell;
13use tokio::sync::Mutex;
14use tokio_util::sync::CancellationToken;
15use tracing::{debug, warn};
16
17use crate::RuntaraSdk;
18
19/// Global storage for the SDK instance.
20static SDK_INSTANCE: OnceCell<Arc<Mutex<RuntaraSdk>>> = OnceCell::new();
21
22/// Cancellation token for the background heartbeat task.
23static HEARTBEAT_CANCEL: OnceCell<CancellationToken> = OnceCell::new();
24
25/// Register an SDK instance globally for use by #[durable] functions.
26///
27/// This should be called once at application startup after creating and
28/// connecting the SDK. If the SDK is configured with a non-zero heartbeat
29/// interval, a background task will be spawned to send periodic heartbeats.
30///
31/// # Panics
32///
33/// Panics if called more than once.
34///
35/// # Example
36///
37/// ```ignore
38/// use runtara_sdk::{RuntaraSdk, register_sdk};
39///
40/// #[tokio::main]
41/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
42/// let sdk = RuntaraSdk::localhost("my-instance", "my-tenant")?;
43/// sdk.connect().await?;
44///
45/// // Register globally for #[durable] functions
46/// // This also starts a background heartbeat task (default: every 30s)
47/// register_sdk(sdk);
48///
49/// // Now #[durable] functions will use this SDK
50/// Ok(())
51/// }
52/// ```
53pub fn register_sdk(sdk: RuntaraSdk) {
54 let heartbeat_interval_ms = sdk.heartbeat_interval_ms();
55
56 let sdk_arc = Arc::new(Mutex::new(sdk));
57
58 if SDK_INSTANCE.set(sdk_arc.clone()).is_err() {
59 panic!("SDK already registered. register_sdk() should only be called once.");
60 }
61
62 // Spawn background heartbeat task if enabled
63 if heartbeat_interval_ms > 0 {
64 let cancel_token = CancellationToken::new();
65 let _ = HEARTBEAT_CANCEL.set(cancel_token.clone());
66
67 let interval = Duration::from_millis(heartbeat_interval_ms);
68
69 tokio::spawn(async move {
70 debug!(
71 interval_ms = heartbeat_interval_ms,
72 "Background heartbeat task started"
73 );
74
75 loop {
76 tokio::select! {
77 biased;
78
79 _ = cancel_token.cancelled() => {
80 debug!("Background heartbeat task cancelled");
81 break;
82 }
83
84 _ = tokio::time::sleep(interval) => {
85 let sdk_guard = sdk_arc.lock().await;
86 if let Err(e) = sdk_guard.heartbeat().await {
87 warn!(error = %e, "Failed to send background heartbeat");
88 } else {
89 debug!("Background heartbeat sent");
90 }
91 }
92 }
93 }
94 });
95 }
96}
97
98/// Get a reference to the registered SDK.
99///
100/// # Panics
101///
102/// Panics if no SDK has been registered.
103pub fn sdk() -> &'static Arc<Mutex<RuntaraSdk>> {
104 SDK_INSTANCE
105 .get()
106 .expect("No SDK registered. Call register_sdk() at application startup.")
107}
108
109/// Try to get a reference to the registered SDK.
110///
111/// Returns `None` if no SDK has been registered.
112pub fn try_sdk() -> Option<&'static Arc<Mutex<RuntaraSdk>>> {
113 SDK_INSTANCE.get()
114}
115
116/// Stop the background heartbeat task.
117///
118/// This should be called when shutting down the SDK to cleanly stop
119/// the background heartbeat task. It's safe to call this multiple times
120/// or even if no heartbeat task was started.
121///
122/// # Example
123///
124/// ```ignore
125/// use runtara_sdk::{register_sdk, stop_heartbeat, sdk};
126///
127/// // ... at shutdown
128/// stop_heartbeat();
129/// let sdk_guard = sdk().lock().await;
130/// sdk_guard.close().await?;
131/// ```
132pub fn stop_heartbeat() {
133 if let Some(cancel_token) = HEARTBEAT_CANCEL.get() {
134 cancel_token.cancel();
135 debug!("Heartbeat cancellation requested");
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 // Note: These tests can't run in parallel due to global state.
142 // In a real test suite, you'd use a thread-local or per-test registry.
143
144 #[test]
145 fn test_try_sdk_returns_none_initially() {
146 // Can't test this reliably due to global state from other tests
147 }
148}