use core::any::TypeId;
use core::fmt::Debug;
use alloc::{
boxed::Box,
string::{String, ToString},
sync::Arc,
vec::Vec,
};
use hashbrown::HashMap;
use crate::extensions::Extensions;
use crate::graph::DependencyGraph;
pub type BoxFuture = crate::executor::BoxFuture;
type StartFnType = Box<dyn FnOnce(crate::RuntimeContext) -> BoxFuture + Send>;
type SpawnFnType = Box<dyn FnOnce(&Arc<AimDb>, RecordId) -> DbResult<Vec<BoxFuture>> + Send>;
use crate::record_id::{RecordId, RecordKey, StringKey};
use crate::typed_api::RecordRegistrar;
use crate::typed_record::{AnyRecord, AnyRecordExt, RecordFutureCollector, TypedRecord};
use crate::{DbError, DbResult};
pub struct OutboundRoute {
pub topic: String,
pub source: Box<dyn crate::connector::SerializedSource>,
pub config: Vec<(String, String)>,
}
struct RecordEntry {
key: StringKey,
type_id: TypeId,
record: Box<dyn AnyRecord>,
}
pub struct AimDbInner {
storages: Vec<RecordEntry>,
by_key: HashMap<StringKey, RecordId>,
dependency_graph: crate::graph::DependencyGraph,
pub(crate) extensions: Extensions,
}
impl AimDbInner {
#[inline]
pub fn resolve<K: RecordKey>(&self, key: &K) -> Option<RecordId> {
self.by_key.get(key.as_str()).copied()
}
#[inline]
pub fn resolve_str(&self, name: &str) -> Option<RecordId> {
self.by_key.get(name).copied()
}
#[inline]
pub fn storage(&self, id: RecordId) -> Option<&dyn AnyRecord> {
self.storages.get(id.index()).map(|e| e.record.as_ref())
}
#[inline]
pub fn key_for(&self, id: RecordId) -> Option<&StringKey> {
self.storages.get(id.index()).map(|e| &e.key)
}
pub fn records_of_type<T: 'static>(&self) -> Vec<RecordId> {
let wanted = TypeId::of::<T>();
self.storages
.iter()
.enumerate()
.filter(|(_, e)| e.type_id == wanted)
.map(|(i, _)| RecordId::new(i as u32))
.collect()
}
#[inline]
pub fn record_count(&self) -> usize {
self.storages.len()
}
#[inline]
pub fn dependency_graph(&self) -> &crate::graph::DependencyGraph {
&self.dependency_graph
}
pub fn get_typed_record_by_key<T>(&self, key: impl AsRef<str>) -> DbResult<&TypedRecord<T>>
where
T: Send + 'static + Debug + Clone,
{
let key_str = key.as_ref();
let id = self
.resolve_str(key_str)
.ok_or_else(|| DbError::record_key_not_found(key_str))?;
self.get_typed_record_by_id::<T>(id)
}
pub fn get_typed_record_by_id<T>(&self, id: RecordId) -> DbResult<&TypedRecord<T>>
where
T: Send + 'static + Debug + Clone,
{
use crate::typed_record::AnyRecordExt;
let entry = self
.storages
.get(id.index())
.ok_or(DbError::InvalidRecordId { id: id.raw() })?;
entry
.record
.as_typed::<T>()
.ok_or_else(|| DbError::TypeMismatch {
record_id: id.raw(),
expected_type: core::any::type_name::<T>().to_string(),
})
}
#[cfg(feature = "remote")]
pub fn list_records(&self) -> Vec<crate::remote::RecordMetadata> {
self.storages
.iter()
.enumerate()
.map(|(i, e)| {
let id = RecordId::new(i as u32);
e.record.collect_metadata(e.type_id, e.key, id)
})
.collect()
}
#[cfg(feature = "remote")]
pub fn try_latest_as_json(&self, record_key: &str) -> Option<serde_json::Value> {
let id = self.resolve_str(record_key)?;
self.storages
.get(id.index())?
.record
.json_access()?
.latest_json()
}
#[cfg(feature = "remote")]
pub fn try_latest_as_json_bytes(&self, record_key: &str) -> Option<Vec<u8>> {
let id = self.resolve_str(record_key)?;
self.storages
.get(id.index())?
.record
.json_access()?
.latest_json_bytes()
}
#[cfg(feature = "remote")]
pub fn set_record_from_json(
&self,
record_key: &str,
json_value: serde_json::Value,
) -> DbResult<()> {
let id = self
.resolve_str(record_key)
.ok_or_else(|| DbError::record_key_not_found(record_key.to_string()))?;
self.storages[id.index()]
.record
.json_access()
.ok_or_else(|| {
DbError::runtime_error(alloc::format!(
"Record '{record_key}' does not support JSON remote access"
))
})?
.set_from_json(json_value)
}
}
pub struct AimDbBuilder {
records: Vec<(StringKey, TypeId, Box<dyn AnyRecord>)>,
record_index: HashMap<StringKey, usize>,
runtime: Option<Arc<dyn crate::executor::RuntimeOps>>,
connector_builders: Vec<Box<dyn crate::connector::ConnectorBuilder>>,
spawn_fns: Vec<(StringKey, SpawnFnType)>,
start_fns: Vec<StartFnType>,
extensions: Extensions,
config_errors: Vec<crate::error::ConfigError>,
}
impl AimDbBuilder {
pub fn new() -> Self {
Self {
records: Vec::new(),
record_index: HashMap::new(),
runtime: None,
connector_builders: Vec::new(),
spawn_fns: Vec::new(),
start_fns: Vec::new(),
extensions: Extensions::new(),
config_errors: Vec::new(),
}
}
pub fn runtime(mut self, rt: Arc<impl crate::executor::RuntimeOps + 'static>) -> Self {
self.runtime = Some(rt);
self
}
pub fn extensions(&self) -> &Extensions {
&self.extensions
}
pub fn extensions_mut(&mut self) -> &mut Extensions {
&mut self.extensions
}
pub fn on_start<F, Fut>(&mut self, f: F) -> &mut Self
where
F: FnOnce(crate::RuntimeContext) -> Fut + Send + 'static,
Fut: core::future::Future<Output = ()> + Send + 'static,
{
self.start_fns.push(Box::new(move |ctx| Box::pin(f(ctx))));
self
}
pub fn with_connector(
mut self,
builder: impl crate::connector::ConnectorBuilder + 'static,
) -> Self {
self.connector_builders.push(Box::new(builder));
self
}
pub fn configure<T>(
&mut self,
key: impl RecordKey,
f: impl FnOnce(&mut RecordRegistrar<'_, T>),
) -> &mut Self
where
T: Send + Sync + 'static + Debug + Clone,
{
let record_key: StringKey = StringKey::from_dynamic(key.as_str());
let type_id = TypeId::of::<T>();
let record_index = self.record_index.get(&record_key).copied();
#[allow(
clippy::expect_used,
clippy::unwrap_used,
reason = "unreachable given the TypeId check above"
)]
let (rec, is_new_record) = match record_index {
Some(idx) => {
let (_, existing_type, record) = &mut self.records[idx];
if *existing_type != type_id {
self.config_errors.push(crate::error::ConfigError::new(
record_key.as_str(),
None,
"key already registered with a different type",
));
return self;
}
(
record.as_typed_mut::<T>().expect(
"record registry type mismatch despite TypeId check — \
this is a bug in aimdb-core",
),
false,
)
}
None => {
self.record_index.insert(record_key, self.records.len());
self.records
.push((record_key, type_id, Box::new(TypedRecord::<T>::new())));
let (_, _, record) = self.records.last_mut().unwrap();
(
record.as_typed_mut::<T>().expect(
"record registry type mismatch despite TypeId check — \
this is a bug in aimdb-core",
),
true,
)
}
};
let mut reg = RecordRegistrar {
rec,
connector_builders: &self.connector_builders,
record_key: record_key.as_str().to_string(),
extensions: &self.extensions,
last_stage: None,
};
f(&mut reg);
if is_new_record {
let spawn_key = record_key;
let spawn_fn: SpawnFnType = Box::new(move |db: &Arc<AimDb>, id: RecordId| {
let typed_record = db.inner().get_typed_record_by_id::<T>(id)?;
let key = db
.inner()
.key_for(id)
.map(|k| k.as_str().to_string())
.unwrap_or_else(|| alloc::format!("__record_{}", id.index()));
RecordFutureCollector::<T>::collect_all_futures(typed_record, db, key.as_str())
});
self.spawn_fns.push((spawn_key, spawn_fn));
}
self
}
pub async fn run(self) -> DbResult<()> {
log_info!("Building database and spawning background tasks...");
let (_db, runner) = self.build().await?;
log_info!("Database running, runner driving collected futures.");
runner.run().await;
Ok(())
}
pub async fn build(mut self) -> DbResult<(AimDb, AimDbRunner)> {
use crate::error::ConfigError;
let mut errors = core::mem::take(&mut self.config_errors);
for (key, _, record) in self.records.iter_mut() {
for mut e in record.drain_config_errors() {
if e.record_key.is_empty() {
e.record_key = key.as_str().to_string();
}
errors.push(e);
}
let mut writers: Vec<&str> = Vec::new();
if record.has_producer() {
writers.push(".source()");
}
if record.has_transform() {
writers.push(".transform()");
}
if !record.inbound_connectors().is_empty() {
writers.push(".link_from()");
}
if writers.len() > 1 {
let url = record
.inbound_connectors()
.first()
.map(|l| l.url.to_string());
errors.push(ConfigError::new(
key.as_str(),
url,
alloc::format!(
"conflicting writers: record has {}; these are mutually \
exclusive (all write the same buffer)",
writers.join(" + ")
),
));
}
let has_links =
!record.outbound_connectors().is_empty() || !record.inbound_connectors().is_empty();
if has_links && !record.has_buffer() {
let url = record
.outbound_connectors()
.first()
.map(|l| l.url.to_string())
.or_else(|| {
record
.inbound_connectors()
.first()
.map(|l| l.url.to_string())
});
errors.push(ConfigError::new(
key.as_str(),
url,
"linked record requires a buffer (call .buffer(...))",
));
}
}
if self.runtime.is_none() {
errors.push(ConfigError::new(
"",
None,
"runtime not set (use .runtime())",
));
}
let record_count = self.records.len();
let mut storages: Vec<RecordEntry> = Vec::with_capacity(record_count);
let mut by_key: HashMap<StringKey, RecordId> = HashMap::with_capacity(record_count);
for (key, type_id, record) in self.records.into_iter() {
if by_key.contains_key(&key) {
errors.push(ConfigError::new(key.as_str(), None, "duplicate record key"));
continue;
}
let id = RecordId::new(storages.len() as u32);
storages.push(RecordEntry {
key,
type_id,
record,
});
by_key.insert(key, id);
}
let graph_nodes: Vec<crate::graph::GraphNode> = storages
.iter()
.map(|e| {
let record = &e.record;
let (buffer_type, buffer_capacity) = record.buffer_info();
crate::graph::GraphNode {
key: e.key.as_str().to_string(),
origin: record.record_origin(),
buffer_type,
buffer_capacity,
tap_count: record.consumer_count(),
has_outbound_link: !record.outbound_connectors().is_empty(),
}
})
.collect();
let dependency_graph = match DependencyGraph::build_and_validate(graph_nodes) {
Ok(graph) => graph,
Err(e) => {
errors.push(ConfigError::new("", None, alloc::format!("{e}")));
return Err(DbError::InvalidConfiguration { errors });
}
};
let runtime = match (self.runtime.take(), errors.is_empty()) {
(Some(rt), true) => rt,
_ => return Err(DbError::InvalidConfiguration { errors }),
};
log_debug!(
"Dependency graph built successfully ({} nodes, {} edges, topo order: {:?})",
dependency_graph.nodes.len(),
dependency_graph.edges.len(),
dependency_graph.topo_order
);
let inner = Arc::new(AimDbInner {
storages,
by_key,
dependency_graph,
extensions: self.extensions,
});
#[cfg(feature = "observability")]
let profiling_clock = crate::profiling::make_clock(runtime.clone());
let db = Arc::new(AimDb {
inner: inner.clone(),
runtime: runtime.clone(),
#[cfg(feature = "observability")]
profiling_clock,
});
let mut futures_acc: Vec<BoxFuture> = Vec::new();
log_info!("Collecting futures for {} records", self.spawn_fns.len());
let mut spawn_fn_map: HashMap<StringKey, SpawnFnType> =
self.spawn_fns.into_iter().collect();
for key_str in inner.dependency_graph.topo_order() {
let Some((&key, &id)) = inner.by_key.get_key_value(key_str.as_str()) else {
continue;
};
let Some(spawn_fn) = spawn_fn_map.remove(&key) else {
continue;
};
futures_acc.extend(spawn_fn(&db, id)?);
}
log_info!("Record future collection complete");
let mut duplicate_schemes: Vec<crate::error::ConfigError> = Vec::new();
for (i, builder) in self.connector_builders.iter().enumerate() {
if !builder.owns_scheme() {
continue;
}
let first = self.connector_builders[..i]
.iter()
.position(|earlier| earlier.scheme() == builder.scheme());
if first.is_some()
&& !duplicate_schemes
.iter()
.any(|e| e.url.as_deref() == Some(builder.scheme()))
{
duplicate_schemes.push(crate::error::ConfigError::new(
"",
Some(builder.scheme().into()),
alloc::format!(
"More than one connector registered for scheme '{}'. Routes are \
collected by scheme alone, so each connector would claim all of \
them: every `link_to` would publish twice, and the routes cannot \
be divided between the two. Register one connector for this \
scheme, or give one of them a distinct scheme.",
builder.scheme()
),
));
}
}
if !duplicate_schemes.is_empty() {
return Err(DbError::InvalidConfiguration {
errors: duplicate_schemes,
});
}
for builder in self.connector_builders {
log_debug!("Building connector for scheme: {}", builder.scheme());
let connector_futures = builder.build(&db).await?;
let n_futures = connector_futures.len();
futures_acc.extend(connector_futures);
log_info!(
"Connector '{}' contributed {} future(s)",
builder.scheme(),
n_futures
);
}
if !self.start_fns.is_empty() {
log_debug!("Collecting {} on_start future(s)", self.start_fns.len());
for start_fn in self.start_fns {
futures_acc.push(start_fn(crate::RuntimeContext::new(runtime.clone())));
}
}
let db_owned = (*db).clone();
Ok((db_owned, AimDbRunner::new(futures_acc)))
}
}
impl Default for AimDbBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
pub struct AimDb {
inner: Arc<AimDbInner>,
runtime: Arc<dyn crate::executor::RuntimeOps>,
#[cfg(feature = "observability")]
profiling_clock: crate::profiling::Clock,
}
pub struct AimDbRunner {
futures: Vec<BoxFuture>,
}
impl AimDbRunner {
pub(crate) fn new(futures: Vec<BoxFuture>) -> Self {
Self { futures }
}
pub fn future_count(&self) -> usize {
self.futures.len()
}
pub async fn run(self) {
use futures_util::stream::{FuturesUnordered, StreamExt};
if self.futures.is_empty() {
return;
}
let mut set: FuturesUnordered<BoxFuture> = self.futures.into_iter().collect();
while set.next().await.is_some() {}
}
}
impl AimDb {
#[doc(hidden)]
pub fn inner(&self) -> &Arc<AimDbInner> {
&self.inner
}
pub fn extensions(&self) -> &Extensions {
&self.inner.extensions
}
#[cfg(feature = "observability")]
pub(crate) fn profiling_clock(&self) -> &crate::profiling::Clock {
&self.profiling_clock
}
pub async fn build_with(
rt: Arc<impl crate::executor::RuntimeOps + 'static>,
f: impl FnOnce(&mut AimDbBuilder),
) -> DbResult<()> {
let mut b = AimDbBuilder::new().runtime(rt);
f(&mut b);
b.run().await
}
pub fn produce<T>(&self, key: impl AsRef<str>, value: T) -> DbResult<()>
where
T: Send + 'static + Debug + Clone,
{
let typed_rec = self.inner.get_typed_record_by_key::<T>(key)?;
typed_rec.writer_handle().push(value);
Ok(())
}
pub fn subscribe<T>(&self, key: impl AsRef<str>) -> DbResult<crate::buffer::Reader<T>>
where
T: Send + Sync + 'static + Debug + Clone,
{
let typed_rec = self.inner.get_typed_record_by_key::<T>(key)?;
typed_rec.subscribe()
}
pub fn producer<T>(
&self,
key: impl Into<alloc::string::String>,
) -> DbResult<crate::typed_api::Producer<T>>
where
T: Send + 'static + Debug + Clone,
{
let key_str: alloc::string::String = key.into();
let typed_rec = self.inner.get_typed_record_by_key::<T>(&key_str)?;
Ok(crate::typed_api::Producer::new(typed_rec.writer_handle()))
}
pub fn consumer<T>(
&self,
key: impl Into<alloc::string::String>,
) -> DbResult<crate::typed_api::Consumer<T>>
where
T: Send + Sync + 'static + Debug + Clone,
{
let key_str: alloc::string::String = key.into();
let typed_rec = self.inner.get_typed_record_by_key::<T>(&key_str)?;
let buffer = typed_rec.buffer_handle().ok_or_else(|| {
DbError::missing_configuration(alloc::format!("buffer for record '{}'", key_str))
})?;
Ok(crate::typed_api::Consumer::new(buffer))
}
pub fn resolve_key(&self, key: &str) -> Option<crate::record_id::RecordId> {
self.inner.resolve_str(key)
}
pub fn records_of_type<T: 'static>(&self) -> Vec<crate::record_id::RecordId> {
self.inner.records_of_type::<T>()
}
pub fn runtime_ops(&self) -> Arc<dyn crate::executor::RuntimeOps> {
self.runtime.clone()
}
pub fn runtime_ctx(&self) -> crate::RuntimeContext {
crate::RuntimeContext::new(self.runtime.clone())
}
#[cfg(feature = "remote")]
pub fn list_records(&self) -> Vec<crate::remote::RecordMetadata> {
self.inner.list_records()
}
#[cfg(feature = "observability")]
pub fn reset_stage_profiling(&self) {
for entry in &self.inner.storages {
entry.record.reset_profiling();
}
}
#[cfg(feature = "observability")]
pub fn reset_buffer_metrics(&self) {
for entry in &self.inner.storages {
entry.record.reset_buffer_metrics();
}
}
#[cfg(feature = "remote")]
pub fn try_latest_as_json(&self, record_name: &str) -> Option<serde_json::Value> {
self.inner.try_latest_as_json(record_name)
}
#[cfg(feature = "remote")]
pub fn try_latest_as_json_bytes(&self, record_name: &str) -> Option<Vec<u8>> {
self.inner.try_latest_as_json_bytes(record_name)
}
#[cfg(feature = "remote")]
pub fn set_record_from_json(
&self,
record_name: &str,
json_value: serde_json::Value,
) -> DbResult<()> {
self.inner.set_record_from_json(record_name, json_value)
}
pub fn collect_inbound_routes(
&self,
scheme: &str,
) -> Vec<(String, crate::connector::IngestFn)> {
let mut routes = Vec::new();
for entry in &self.inner.storages {
let inbound_links = entry.record.inbound_connectors();
for link in inbound_links {
if link.url.scheme() != scheme {
continue;
}
let topic = link.resolve_topic();
routes.push((topic, link.create_ingest(self)));
}
}
if !routes.is_empty() {
log_debug!(
"Collected {} inbound routes for scheme '{}'",
routes.len(),
scheme
);
}
routes
}
pub fn collect_outbound_routes(&self, scheme: &str) -> Vec<OutboundRoute> {
let mut routes = Vec::new();
for entry in &self.inner.storages {
let outbound_links = entry.record.outbound_connectors();
for link in outbound_links {
if link.url.scheme() != scheme {
continue;
}
routes.push(OutboundRoute {
topic: link.url.resource_id().to_string(),
source: link.create_source(self),
config: link.config.clone(),
});
}
}
if !routes.is_empty() {
log_debug!(
"Collected {} outbound routes for scheme '{}'",
routes.len(),
scheme
);
}
routes
}
}