mod cloud;
mod local;
mod profile;
pub(crate) mod sandbox;
pub(crate) mod volume;
pub use cloud::{CloudBackend, CloudBackendBuilder};
pub use local::{LocalBackend, LocalBackendBuilder};
pub use microsandbox_types::{
CloudCreateSandboxRequest, CloudErrorBody, CloudErrorDetails, CloudMessageResponse,
CloudPaginated, CloudSandbox, CloudSandboxStatus,
};
pub use profile::{Profile, ProfileBackend, SdkConfig, load_sdk_config, resolve_default_backend};
pub use sandbox::{
SandboxBackend, SandboxCloudState, SandboxHandleCloudState, SandboxHandleInner,
SandboxHandleLocalState, SandboxInner, SandboxList, SandboxLocalState,
};
pub use volume::{
VolumeBackend, VolumeCloudState, VolumeHandleCloudState, VolumeHandleInner,
VolumeHandleLocalState, VolumeInner, VolumeLocalState,
};
use std::sync::{Arc, OnceLock, RwLock};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BackendKind {
Local,
Cloud,
}
pub trait Backend: Send + Sync + 'static {
fn kind(&self) -> BackendKind;
fn sandboxes(&self) -> &dyn SandboxBackend;
fn volumes(&self) -> &dyn VolumeBackend;
fn as_local(&self) -> Option<&LocalBackend> {
None
}
}
static DEFAULT: OnceLock<RwLock<Arc<dyn Backend>>> = OnceLock::new();
pub fn set_default_backend(backend: impl Into<Arc<dyn Backend>>) {
let cell = default_cell();
*cell.write().expect("DEFAULT backend RwLock poisoned") = backend.into();
}
pub fn swap_default_backend(backend: impl Into<Arc<dyn Backend>>) -> Arc<dyn Backend> {
let cell = default_cell();
let mut guard = cell.write().expect("DEFAULT backend RwLock poisoned");
std::mem::replace(&mut *guard, backend.into())
}
pub fn default_backend() -> Arc<dyn Backend> {
if let Ok(scoped) = SCOPED_BACKEND.try_with(|b| b.clone()) {
return scoped;
}
default_cell()
.read()
.expect("DEFAULT backend RwLock poisoned")
.clone()
}
pub async fn with_backend<F, T>(backend: impl Into<Arc<dyn Backend>>, future: F) -> T
where
F: std::future::Future<Output = T>,
{
SCOPED_BACKEND.scope(backend.into(), future).await
}
fn default_cell() -> &'static RwLock<Arc<dyn Backend>> {
DEFAULT.get_or_init(|| {
let resolved = profile::resolve_default_backend().unwrap_or_else(|e| {
tracing::warn!(
error = %e,
"default backend resolution failed; falling back to LocalBackend"
);
Arc::new(LocalBackend::lazy())
});
RwLock::new(resolved)
})
}
tokio::task_local! {
static SCOPED_BACKEND: Arc<dyn Backend>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_backend_resolves_to_local_when_unset() {
let b = default_backend();
assert!(matches!(b.kind(), BackendKind::Local | BackendKind::Cloud));
}
#[tokio::test]
async fn with_backend_overrides_for_scope() {
struct Fake(BackendKind);
impl Backend for Fake {
fn kind(&self) -> BackendKind {
self.0
}
fn sandboxes(&self) -> &dyn SandboxBackend {
unimplemented!("fake backend only tests kind routing")
}
fn volumes(&self) -> &dyn VolumeBackend {
unimplemented!("fake backend only tests kind routing")
}
}
let fake: Arc<dyn Backend> = Arc::new(Fake(BackendKind::Cloud));
let observed = with_backend(fake, async { default_backend().kind() }).await;
assert_eq!(observed, BackendKind::Cloud);
let outside = default_backend().kind();
assert!(matches!(outside, BackendKind::Local | BackendKind::Cloud));
}
#[test]
fn swap_default_backend_restores_previous_backend() {
struct Fake(BackendKind);
impl Backend for Fake {
fn kind(&self) -> BackendKind {
self.0
}
fn sandboxes(&self) -> &dyn SandboxBackend {
unimplemented!("fake backend only tests kind routing")
}
fn volumes(&self) -> &dyn VolumeBackend {
unimplemented!("fake backend only tests kind routing")
}
}
let original = default_backend();
let fake: Arc<dyn Backend> = Arc::new(Fake(BackendKind::Cloud));
let previous = swap_default_backend(fake);
assert_eq!(default_backend().kind(), BackendKind::Cloud);
set_default_backend(previous);
assert_eq!(default_backend().kind(), original.kind());
}
}