use crate::contract::{Compute, ComputeError, TransformBackend};
use crate::registry::{Backends, DirRoot, Registry, RegistryBuilder, RegistryError, Root};
use arrow::array::RecordBatch;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
pub struct Runtime {
registry: Registry,
backends: Backends,
loaded: Mutex<HashMap<String, Arc<dyn Compute>>>,
}
impl Runtime {
pub fn builder() -> RuntimeBuilder {
RuntimeBuilder::default()
}
pub fn new(registry: Registry, backends: Backends) -> Self {
Self {
registry,
backends,
loaded: Mutex::new(HashMap::new()),
}
}
pub fn registry(&self) -> &Registry {
&self.registry
}
pub fn backends(&self) -> &Backends {
&self.backends
}
pub fn load(&self, selector: &str) -> Result<Arc<dyn Compute>, ComputeError> {
if let Some(c) = self.loaded.lock().unwrap().get(selector) {
return Ok(c.clone());
}
let compute: Arc<dyn Compute> = Arc::from(self.registry.load(selector, &self.backends)?);
self.loaded
.lock()
.unwrap()
.insert(selector.to_string(), compute.clone());
Ok(compute)
}
pub fn run(&self, selector: &str, inputs: &[RecordBatch]) -> Result<RecordBatch, ComputeError> {
self.load(selector)?.run(inputs)
}
pub fn ensure_loadable(&self, selector: &str) -> Result<(), ComputeError> {
self.load(selector).map(drop)
}
pub fn clear_cache(&self) {
self.loaded.lock().unwrap().clear();
}
}
impl std::fmt::Debug for Runtime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Runtime")
.field("transforms", &self.registry.len())
.field("backends", &self.backends.kinds().collect::<Vec<_>>())
.field("loaded", &self.loaded.lock().unwrap().len())
.finish()
}
}
#[derive(Default)]
pub struct RuntimeBuilder {
registry: RegistryBuilder,
backends: Backends,
roots: usize,
}
impl RuntimeBuilder {
pub fn root(mut self, path: impl Into<PathBuf>) -> Self {
let path = path.into();
let name = path
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| format!("root{}", self.roots));
self.roots += 1;
self.registry = self.registry.root(DirRoot::new(name, path));
self
}
pub fn named_root(mut self, name: impl Into<String>, path: impl Into<PathBuf>) -> Self {
self.roots += 1;
self.registry = self.registry.root(DirRoot::new(name, path));
self
}
pub fn source(mut self, root: impl Root + 'static) -> Self {
self.registry = self.registry.root(root);
self
}
pub fn backend(mut self, backend: impl TransformBackend + 'static) -> Self {
self.backends = self.backends.register(backend);
self
}
pub fn build(self) -> Result<Runtime, RegistryError> {
Ok(Runtime::new(self.registry.build()?, self.backends))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::manifest::{ImplKind, TransformManifest};
use crate::registry::MemoryRoot;
use std::path::Path;
use std::sync::atomic::{AtomicUsize, Ordering};
const MANIFEST: &str = r#"
id = "double"
version = "1.0.0"
impl = "wasm"
entry = "main.wasm"
[[inputs]]
columns = [{ name = "x", type = "int64" }]
[output]
columns = [{ name = "x", type = "int64" }]
"#;
struct Counting(Arc<AtomicUsize>);
struct Echo(TransformManifest);
impl Compute for Echo {
fn manifest(&self) -> &TransformManifest {
&self.0
}
fn run(&self, inputs: &[RecordBatch]) -> Result<RecordBatch, ComputeError> {
Ok(inputs[0].clone())
}
}
impl TransformBackend for Counting {
fn kind(&self) -> ImplKind {
ImplKind::Wasm
}
fn load(&self, manifest: &TransformManifest, _root: &Path) -> Result<Box<dyn Compute>, ComputeError> {
self.0.fetch_add(1, Ordering::SeqCst);
Ok(Box::new(Echo(manifest.clone())))
}
}
fn batch() -> RecordBatch {
use arrow::array::Int64Array;
use arrow::datatypes::{DataType, Field, Schema};
let schema = Arc::new(Schema::new(vec![Field::new("x", DataType::Int64, true)]));
RecordBatch::try_new(schema, vec![Arc::new(Int64Array::from(vec![1, 2, 3]))]).unwrap()
}
#[test]
fn loads_once_per_selector_and_runs() {
let loads = Arc::new(AtomicUsize::new(0));
let runtime = Runtime::builder()
.source(MemoryRoot::new("mem").unit("double", MANIFEST))
.backend(Counting(loads.clone()))
.build()
.unwrap();
runtime.ensure_loadable("double").unwrap();
let out = runtime.run("double", &[batch()]).unwrap();
let again = runtime.run("double@1.0.0", &[batch()]).unwrap();
assert_eq!(out.num_rows(), 3);
assert_eq!(again.num_rows(), 3);
assert_eq!(loads.load(Ordering::SeqCst), 2);
runtime.clear_cache();
runtime.run("double", &[batch()]).unwrap();
assert_eq!(loads.load(Ordering::SeqCst), 3);
}
#[test]
fn missing_transform_and_missing_backend_are_distinct_errors() {
let runtime = Runtime::builder()
.source(MemoryRoot::new("mem").unit("double", MANIFEST))
.build()
.unwrap();
assert!(matches!(runtime.load("nope"), Err(ComputeError::Load { .. })));
assert!(matches!(
runtime.load("double"),
Err(ComputeError::NoBackend { kind: ImplKind::Wasm })
));
}
#[test]
fn a_missing_directory_root_is_empty_not_an_error() {
let runtime = Runtime::builder().root("/definitely/not/here").build().unwrap();
assert!(runtime.registry().is_empty());
assert_eq!(
format!("{runtime:?}"),
"Runtime { transforms: 0, backends: [], loaded: 0 }"
);
}
}