use chrono::Utc;
use crate::application::runtime::runtime_factory::RuntimeBackend;
use crate::application::runtime::stasis_runtime_builder::StasisRuntimeBuilder;
use crate::application::runtime::runtime_factory::RuntimeComposition;
use crate::domain::errors::Result;
use crate::domain::runtime::job::{JobState, NewJob};
use crate::domain::runtime::recurring::RecurringDefinition;
use crate::ports::outbound::runtime::job_store::JobStore;
use crate::ports::outbound::runtime::outbox_store::OutboxStore;
use crate::ports::outbound::runtime::recurring_store::RecurringStore;
#[derive(Clone, Debug, Default)]
pub struct RuntimeStatsSnapshot {
pub enqueued_jobs: usize,
pub running_jobs: usize,
pub succeeded_jobs: usize,
pub failed_jobs: usize,
pub dead_letter_jobs: usize,
pub pending_outbox_events: usize,
pub recurring_definitions: usize,
}
#[derive(Clone)]
pub struct RuntimeSdk {
runtime: RuntimeComposition,
}
pub type StasisRuntime = RuntimeSdk;
impl RuntimeSdk {
pub fn new(runtime: RuntimeComposition) -> Self {
Self { runtime }
}
pub async fn in_memory() -> Result<Self> {
Self::from_builder(StasisRuntimeBuilder::new(RuntimeBackend::InMemory)).await
}
pub async fn surreal_mem(
namespace: impl Into<String>,
database: impl Into<String>,
) -> Result<Self> {
Self::from_builder(StasisRuntimeBuilder::new(RuntimeBackend::SurrealMem {
namespace: namespace.into(),
database: database.into(),
}))
.await
}
pub async fn surreal_ws(
endpoint: impl Into<String>,
namespace: impl Into<String>,
database: impl Into<String>,
) -> Result<Self> {
Self::from_builder(StasisRuntimeBuilder::new(RuntimeBackend::SurrealWs {
endpoint: endpoint.into(),
namespace: namespace.into(),
database: database.into(),
}))
.await
}
pub async fn surreal_kv(
path: impl Into<String>,
namespace: impl Into<String>,
database: impl Into<String>,
) -> Result<Self> {
Self::from_builder(StasisRuntimeBuilder::new(RuntimeBackend::SurrealKv {
path: path.into(),
namespace: namespace.into(),
database: database.into(),
}))
.await
}
pub async fn from_builder(builder: StasisRuntimeBuilder) -> Result<Self> {
let runtime = builder.build().await?;
Ok(Self::new(runtime))
}
pub fn runtime(&self) -> &RuntimeComposition {
&self.runtime
}
pub fn into_runtime(self) -> RuntimeComposition {
self.runtime
}
pub async fn enqueue(&self, job: NewJob) -> Result<()> {
match &self.runtime {
RuntimeComposition::InMemory(rt) => rt.enqueue(job).await,
RuntimeComposition::Surreal(rt) => rt.enqueue(job).await,
}
}
pub async fn register_recurring(&self, definition: RecurringDefinition) -> Result<()> {
match &self.runtime {
RuntimeComposition::InMemory(rt) => rt.register_recurring(definition).await,
RuntimeComposition::Surreal(rt) => rt.register_recurring(definition).await,
}
}
pub async fn process_once(&self, queue: &str, worker_id: &str) -> Result<Option<String>> {
let now = Utc::now();
match &self.runtime {
RuntimeComposition::InMemory(rt) => rt.process_once(queue, worker_id, now).await,
RuntimeComposition::Surreal(rt) => rt.process_once(queue, worker_id, now).await,
}
}
pub async fn publish_pending_events(&self, limit: usize) -> Result<usize> {
let now = Utc::now();
match &self.runtime {
RuntimeComposition::InMemory(rt) => rt.publish_pending_events(limit, now).await,
RuntimeComposition::Surreal(rt) => rt.publish_pending_events(limit, now).await,
}
}
pub async fn materialize_recurring_now(&self, scheduler_id: &str) -> Result<usize> {
match &self.runtime {
RuntimeComposition::InMemory(rt) => rt.materialize_recurring_now(scheduler_id).await,
RuntimeComposition::Surreal(rt) => rt.materialize_recurring_now(scheduler_id).await,
}
}
pub async fn stats_snapshot(&self, pending_limit: usize) -> Result<RuntimeStatsSnapshot> {
Ok(RuntimeStatsSnapshot {
enqueued_jobs: self.job_count_by_state(JobState::Enqueued).await?,
running_jobs: self.job_count_by_state(JobState::Running).await?,
succeeded_jobs: self.job_count_by_state(JobState::Succeeded).await?,
failed_jobs: self.job_count_by_state(JobState::Failed).await?,
dead_letter_jobs: self.job_count_by_state(JobState::DeadLetter).await?,
pending_outbox_events: self.pending_outbox_count(pending_limit).await?,
recurring_definitions: self.recurring_count().await?,
})
}
pub async fn job_count_by_state(&self, state: JobState) -> Result<usize> {
let jobs = match &self.runtime {
RuntimeComposition::InMemory(rt) => rt.job_store.list_by_state(state).await?,
RuntimeComposition::Surreal(rt) => rt.job_store.list_by_state(state).await?,
};
Ok(jobs.len())
}
pub async fn pending_outbox_count(&self, limit: usize) -> Result<usize> {
let pending = match &self.runtime {
RuntimeComposition::InMemory(rt) => rt.outbox_store.list_pending(limit).await?,
RuntimeComposition::Surreal(rt) => rt.outbox_store.list_pending(limit).await?,
};
Ok(pending.len())
}
pub async fn recurring_count(&self) -> Result<usize> {
let definitions = match &self.runtime {
RuntimeComposition::InMemory(rt) => rt.recurring_store.list().await?,
RuntimeComposition::Surreal(rt) => rt.recurring_store.list().await?,
};
Ok(definitions.len())
}
}
#[cfg(test)]
mod tests {
use std::env;
use std::fs;
use std::time::{SystemTime, UNIX_EPOCH};
use crate::application::runtime::runtime_factory::RuntimeComposition;
use super::RuntimeSdk;
#[tokio::test]
async fn runtime_sdk_in_memory_constructor_builds() {
let runtime = RuntimeSdk::in_memory()
.await
.expect("in-memory runtime should build");
let stats = runtime
.stats_snapshot(10)
.await
.expect("stats snapshot should succeed");
assert_eq!(stats.enqueued_jobs, 0);
}
#[tokio::test]
async fn runtime_sdk_surreal_mem_constructor_builds() {
let runtime = RuntimeSdk::surreal_mem("stasis", "runtime")
.await
.expect("surreal-mem runtime should build");
assert!(matches!(runtime.runtime(), RuntimeComposition::Surreal(_)));
}
#[tokio::test]
async fn runtime_sdk_surreal_ws_constructor_rejects_invalid_endpoint() {
let result = RuntimeSdk::surreal_ws("not-a-valid-endpoint", "stasis", "runtime").await;
assert!(result.is_err(), "invalid websocket endpoint should fail");
let err = result.err().expect("result should contain an error");
assert!(err.to_string().contains("connect surreal db"));
}
#[tokio::test]
async fn runtime_sdk_surreal_kv_constructor_builds() {
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock should be after epoch")
.as_nanos();
let path = env::temp_dir().join(format!("stasis-surrealkv-{nanos}"));
let path_str = path.to_string_lossy().into_owned();
let runtime = RuntimeSdk::surreal_kv(path_str, "stasis", "runtime")
.await
.expect("surreal-kv runtime should build");
assert!(matches!(runtime.runtime(), RuntimeComposition::Surreal(_)));
drop(runtime);
let _ = fs::remove_dir_all(path);
}
}