use std::net::SocketAddr;
use std::sync::Arc;
use std::time::Duration;
use exocortex_cache::LocalCache;
use exocortex_cluster::ClusterNode;
use exocortex_ingest::IngestServer;
use exocortex_kernel::{Ontology, OntologyFingerprint};
use exocortex_ops::{IngestPreflight, OpContext};
use exocortex_storage::{LeaseKey, Storage};
use futures::{FutureExt, Stream, StreamExt};
use crate::http_bind::{HealthSnapshot, HttpBind};
use crate::principal::PrincipalRegistry;
#[derive(Clone, Debug)]
pub enum TransportSecurity {
Tls {
certificate: std::path::PathBuf,
private_key: std::path::PathBuf,
},
PlaintextLoopback,
}
#[derive(Clone)]
pub struct BackendNodeArgs {
pub org: String,
pub bind: String,
pub transport: TransportSecurity,
pub node_id: String,
pub cluster_secret: [u8; 32],
pub principals: Arc<PrincipalRegistry>,
pub gossip_listen: SocketAddr,
pub seed_nodes: Vec<String>,
pub redis_url: Option<String>,
pub quiet_hours: exocortex_dreams::fire::QuietHours,
pub admin_source_policies: Vec<(
exocortex_ingest::service::SourcePolicyKey,
exocortex_ingest::service::AdminSourcePolicy,
)>,
}
const LEASE_TTL: Duration = Duration::from_millis(1200);
const LEASE_RENEW: Duration = Duration::from_millis(250);
const CACHE_BRIDGE_BURST: usize = 256;
const CACHE_RESEED_INITIAL_BACKOFF: Duration = Duration::from_millis(100);
const CACHE_RESEED_MAX_BACKOFF: Duration = Duration::from_secs(5);
const CACHE_AUTHORITATIVE_RECONCILE_INTERVAL: Duration = Duration::from_secs(60);
const DISCOVERY_OUTBOX_REPAIR_INTERVAL: Duration = Duration::from_secs(1);
#[doc(hidden)]
#[derive(Clone, Copy)]
pub struct CacheBridgeTiming {
pub initial_delay: Duration,
pub max_delay: Duration,
pub reconcile_interval: Duration,
}
#[derive(Default)]
struct BackgroundTasks(Vec<tokio::task::JoinHandle<()>>);
impl BackgroundTasks {
fn push(&mut self, task: tokio::task::JoinHandle<()>) {
self.0.push(task);
}
}
impl Drop for BackgroundTasks {
fn drop(&mut self) {
for task in self.0.drain(..) {
task.abort();
}
}
}
fn dreams_lease_key(org: &str) -> LeaseKey {
LeaseKey::Dreams {
org: org.into(),
region: "*:*".into(),
}
}
fn mark_dreams_follower(
elected: &std::sync::atomic::AtomicBool,
health: &arc_swap::ArcSwap<HealthSnapshot>,
) {
elected.store(false, std::sync::atomic::Ordering::SeqCst);
health.rcu(|snapshot| {
let mut next = (**snapshot).clone();
next.leader_node_id = None;
next.lease_epoch = 0;
next.last_lease_tick = Some(chrono::Utc::now());
Arc::new(next)
});
}
pub struct BackendNode<S: Storage> {
pub health: Arc<arc_swap::ArcSwap<HealthSnapshot>>,
pub local_addr: SocketAddr,
pub cache: Arc<LocalCache>,
pub leader_gate: Arc<std::sync::atomic::AtomicBool>,
pub dreams: Arc<exocortex_dreams::DreamsEngine<S>>,
#[cfg(debug_assertions)]
#[doc(hidden)]
pub reasoning: Arc<exocortex_reasoning::ReasoningEngine<S>>,
pub gossip: Option<chitchat::ChitchatHandle>,
cache_bridge: Option<tokio::task::JoinHandle<()>>,
cluster_feed: Option<tokio::task::JoinHandle<()>>,
post_ingest_effects: Option<tokio::task::JoinHandle<()>>,
leader_election: Option<tokio::task::JoinHandle<()>>,
ingress: Option<tokio::task::JoinHandle<anyhow::Result<()>>>,
_background_tasks: BackgroundTasks,
}
impl<S: Storage> BackendNode<S> {
#[doc(hidden)]
pub fn cluster_coordination_active(&self) -> bool {
self.gossip.is_some() && self.leader_election.is_some()
}
pub fn stop_leader_election(&mut self) {
mark_dreams_follower(&self.leader_gate, &self.health);
if let Some(task) = self.leader_election.take() {
task.abort();
}
}
pub async fn wait_for_ingress(&mut self) -> anyhow::Result<()> {
let task = self
.ingress
.as_mut()
.ok_or_else(|| anyhow::anyhow!("backend ingress task is not running"))?;
let result = task.await;
self.ingress.take();
match result {
Ok(Ok(())) => anyhow::bail!("backend ingress stopped unexpectedly"),
Ok(Err(error)) => Err(error),
Err(error) => Err(anyhow::anyhow!("backend ingress task failed: {error}")),
}
}
}
impl<S: Storage> Drop for BackendNode<S> {
fn drop(&mut self) {
self.leader_gate
.store(false, std::sync::atomic::Ordering::SeqCst);
if let Some(task) = self.ingress.take() {
task.abort();
}
if let Some(task) = self.cache_bridge.take() {
task.abort();
}
if let Some(task) = self.cluster_feed.take() {
task.abort();
}
if let Some(task) = self.post_ingest_effects.take() {
task.abort();
}
if let Some(task) = self.leader_election.take() {
task.abort();
}
}
}
async fn retry_with_capped_backoff<F, Fut, T, E>(
mut operation: F,
initial_delay: Duration,
max_delay: Duration,
) -> T
where
F: FnMut() -> Fut,
Fut: std::future::Future<Output = Result<T, E>>,
E: std::fmt::Display,
{
let ceiling = max_delay;
let mut delay = initial_delay.min(ceiling);
loop {
match operation().await {
Ok(value) => return value,
Err(error) => {
tracing::warn!(%error, ?delay, "cache reseed failed; retrying");
tokio::time::sleep(delay).await;
delay = delay.saturating_mul(2).min(ceiling);
}
}
}
}
struct RegistryPreflight<S: Storage> {
server: Arc<tokio::sync::OnceCell<Arc<IngestServer<S>>>>,
}
#[async_trait::async_trait]
impl<S: Storage + 'static> IngestPreflight for RegistryPreflight<S> {
async fn preflight_signed(
&self,
principal: &exocortex_storage::VisibilityContext,
mut batch: exocortex_wire::ingest::v1::IngestBatch,
) -> Result<exocortex_wire::ingest::v1::IngestAck, exocortex_ops::OpError> {
let server = self
.server
.get()
.ok_or_else(|| exocortex_ops::OpError::Other("ingest surface not ready".into()))?;
let entry = server
.source_entry(&batch.org_id, &batch.source_uri, &batch.producer_id)
.map_err(exocortex_ops::OpError::Other)?;
batch.ceiling = match entry.ceiling {
exocortex_kernel::Visibility::Private => 0,
exocortex_kernel::Visibility::Project => 1,
exocortex_kernel::Visibility::Team => 2,
exocortex_kernel::Visibility::Org | exocortex_kernel::Visibility::Public => 3,
};
batch.ontology_fingerprint = server.ontology.fingerprint.0.to_vec();
let key = server
.producer_signing_key(&batch.org_id, &batch.source_uri, &batch.producer_id)
.map_err(exocortex_ops::OpError::Other)?;
exocortex_wire::signing::prepare_batch(&key, &mut batch);
server
.preflight_batch(Some(principal), &batch)
.await
.map_err(|status| exocortex_ops::OpError::Other(status.message().to_string()))
}
}
pub fn preflight_handle<S: Storage + 'static>(
server: Arc<IngestServer<S>>,
) -> Arc<dyn IngestPreflight> {
Arc::new(RegistryPreflight {
server: Arc::new(tokio::sync::OnceCell::from(server)),
})
}
async fn reseed_cache_with_retry<S: Storage>(
cache: &LocalCache,
storage: &S,
org: &str,
health: &arc_swap::ArcSwap<HealthSnapshot>,
initial_delay: Duration,
max_delay: Duration,
observed_lsn: Option<u64>,
) {
let org_id = org.to_owned();
retry_with_capped_backoff(
|| {
let org_id = org_id.clone();
async move {
cache
.reseed_from_storage(storage, &org_id.as_str().into())
.await
}
},
initial_delay,
max_delay,
)
.await;
let published_lsn = cache
.graphs_snapshot(org)
.map_or(0, |snapshot| snapshot.last_backend_lsn);
let synchronized_lsn = observed_lsn.map_or(published_lsn, |lsn| lsn.max(published_lsn));
health.rcu(|current| {
let mut next = (**current).clone();
next.backend_lsn = next.backend_lsn.max(synchronized_lsn);
next.sync_lsn = next.sync_lsn.max(synchronized_lsn);
Arc::new(next)
});
}
#[doc(hidden)]
pub async fn apply_cache_invalidations_with_retry<S: Storage>(
cache: &LocalCache,
storage: &S,
org: &str,
health: &arc_swap::ArcSwap<HealthSnapshot>,
invalidations: Vec<exocortex_storage::Invalidation>,
initial_delay: Duration,
max_delay: Duration,
) {
let Some(lsn) = invalidations
.iter()
.map(exocortex_storage::Invalidation::lsn_of)
.max()
else {
return;
};
health.rcu(|current| {
let mut next = (**current).clone();
next.backend_lsn = next.backend_lsn.max(lsn);
Arc::new(next)
});
match cache.apply_invalidations(invalidations).await {
Ok(()) => {
health.rcu(|current| {
let mut next = (**current).clone();
next.sync_lsn = next.sync_lsn.max(lsn);
Arc::new(next)
});
}
Err(error) => {
tracing::warn!(%error, lsn, "cache invalidation burst failed; reseeding");
reseed_cache_with_retry(
cache,
storage,
org,
health,
initial_delay,
max_delay,
Some(lsn),
)
.await;
}
}
}
#[doc(hidden)]
pub async fn consume_cache_subscription<S, St>(
cache: &LocalCache,
storage: &S,
org: &str,
health: &arc_swap::ArcSwap<HealthSnapshot>,
mut subscription: St,
timing: CacheBridgeTiming,
) where
S: Storage,
St: Stream<Item = exocortex_storage::Result<exocortex_storage::Invalidation>> + Unpin,
{
let mut reconciliation = tokio::time::interval_at(
tokio::time::Instant::now() + timing.reconcile_interval,
timing.reconcile_interval,
);
loop {
let next = tokio::select! {
item = subscription.next() => item,
_ = reconciliation.tick() => {
metrics::counter!("exocortex_backend_cache_reconciliations_total").increment(1);
reseed_cache_with_retry(
cache,
storage,
org,
health,
timing.initial_delay,
timing.max_delay,
None,
)
.await;
continue;
}
};
let first = match next {
Some(Ok(invalidation)) => invalidation,
Some(Err(error)) => {
metrics::counter!("exocortex_cluster_invalidation_decode_errors_total")
.increment(1);
tracing::warn!(%error, "cache bridge stream failed; reseeding");
reseed_cache_with_retry(
cache,
storage,
org,
health,
timing.initial_delay,
timing.max_delay,
None,
)
.await;
return;
}
None => {
tracing::warn!("cache bridge stream terminated; reseeding");
reseed_cache_with_retry(
cache,
storage,
org,
health,
timing.initial_delay,
timing.max_delay,
None,
)
.await;
return;
}
};
let mut burst = Vec::with_capacity(CACHE_BRIDGE_BURST);
burst.push(first);
while burst.len() < CACHE_BRIDGE_BURST {
match subscription.next().now_or_never() {
Some(Some(Ok(invalidation))) => burst.push(invalidation),
Some(Some(Err(error))) => {
metrics::counter!("exocortex_cluster_invalidation_decode_errors_total")
.increment(1);
tracing::warn!(%error, "cache bridge burst failed; reseeding");
reseed_cache_with_retry(
cache,
storage,
org,
health,
timing.initial_delay,
timing.max_delay,
None,
)
.await;
return;
}
Some(None) => {
tracing::warn!("cache bridge stream terminated during burst; reseeding");
reseed_cache_with_retry(
cache,
storage,
org,
health,
timing.initial_delay,
timing.max_delay,
None,
)
.await;
return;
}
None => break,
}
}
apply_cache_invalidations_with_retry(
cache,
storage,
org,
health,
burst,
timing.initial_delay,
timing.max_delay,
)
.await;
}
}
pub async fn run_backend_node<S: Storage + 'static>(
storage: Arc<S>,
ontology: Arc<Ontology>,
args: BackendNodeArgs,
) -> anyhow::Result<BackendNode<S>> {
run_backend_node_inner(storage, ontology, args, None).await
}
pub async fn run_standalone_backend_node<S: Storage + 'static>(
storage: Arc<S>,
ontology: Arc<Ontology>,
args: BackendNodeArgs,
producer_key: [u8; 32],
) -> anyhow::Result<BackendNode<S>> {
run_backend_node_inner(storage, ontology, args, Some(producer_key)).await
}
async fn run_backend_node_inner<S: Storage + 'static>(
storage: Arc<S>,
ontology: Arc<Ontology>,
args: BackendNodeArgs,
standalone_producer_key: Option<[u8; 32]>,
) -> anyhow::Result<BackendNode<S>> {
let standalone = standalone_producer_key.is_some();
let ingress = BoundIngress::bind(&args.bind, &args.transport).await?;
let local_addr = ingress.local_addr()?;
let org: Arc<str> = args.org.clone().into();
let mut background_tasks = BackgroundTasks::default();
let (cache, writer_rx) = LocalCache::new(2 * 1024 * 1024 * 1024);
let cache = Arc::new(cache);
{
let cache = cache.clone();
let storage = storage.clone();
background_tasks.push(tokio::spawn(
async move { cache.run(storage, writer_rx).await },
));
}
let preflight_server: Arc<tokio::sync::OnceCell<Arc<IngestServer<S>>>> =
Arc::new(tokio::sync::OnceCell::new());
let ctx = Arc::new(OpContext {
visibility_ctx: exocortex_ops::operations::ops_vc(
&org,
"backend",
exocortex_kernel::Visibility::Org,
),
audit_admin: false,
storage: storage.clone() as Arc<dyn exocortex_storage::Storage>,
cache: cache.clone(),
deadline: chrono::Utc::now() + chrono::Duration::seconds(30),
ontology: Some(ontology.clone()),
ingest_preflight: Some(Arc::new(RegistryPreflight {
server: preflight_server.clone(),
})),
});
let bind = HttpBind::with_principals(ctx, args.principals.clone());
let health = bind.health_handle();
health.store(Arc::new(HealthSnapshot {
node_id: args.node_id.clone(),
..Default::default()
}));
let (subscription_ready_tx, subscription_ready_rx) = tokio::sync::oneshot::channel();
let (start_consuming_tx, start_consuming_rx) = tokio::sync::oneshot::channel();
let cache_bridge = {
let cache = cache.clone();
let storage = storage.clone();
let health = health.clone();
let org = org.to_string();
tokio::spawn(async move {
let region = exocortex_storage::RegionKey {
org: "*".into(),
project: "*".into(),
memory_type: 0,
};
let mut subscription_ready = Some(subscription_ready_tx);
let mut start_consuming = Some(start_consuming_rx);
loop {
match storage.subscribe_invalidations(®ion).await {
Ok(sub) => {
if let Some(ready) = subscription_ready.take() {
let _ = ready.send(());
}
if let Some(start) = start_consuming.take() {
if start.await.is_err() {
return;
}
}
consume_cache_subscription(
&cache,
&*storage,
&org,
&health,
sub,
CacheBridgeTiming {
initial_delay: CACHE_RESEED_INITIAL_BACKOFF,
max_delay: CACHE_RESEED_MAX_BACKOFF,
reconcile_interval: CACHE_AUTHORITATIVE_RECONCILE_INTERVAL,
},
)
.await;
}
Err(e) => {
tracing::warn!(%e, "cache change-feed subscribe failed; retrying");
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
})
};
if subscription_ready_rx.await.is_err() {
cache_bridge.abort();
anyhow::bail!("cache change-feed supervisor stopped before subscription");
}
if let Err(error) = cache
.reseed_from_storage(&*storage, &org.to_string().into())
.await
{
cache_bridge.abort();
return Err(error.into());
}
let mut hydrated = false;
for _ in 0..200 {
if cache.resident_orgs() > 0 {
hydrated = true;
break;
}
tokio::time::sleep(Duration::from_millis(5)).await;
}
health.rcu(|snapshot| {
let mut next = (**snapshot).clone();
next.hydrated = hydrated;
Arc::new(next)
});
let _ = start_consuming_tx.send(());
{
let storage = storage.clone();
background_tasks.push(tokio::spawn(async move {
let mut interval = tokio::time::interval(DISCOVERY_OUTBOX_REPAIR_INTERVAL);
loop {
interval.tick().await;
if let Err(error) = storage.repair_discovery_outbox().await {
tracing::warn!(?error, "discovery outbox repair failed; retrying");
}
}
}));
}
let cluster = Arc::new(ClusterNode::new(
storage.clone(),
args.node_id.clone().into(),
ontology.fingerprint,
args.cluster_secret,
));
let cluster_feed = {
let runner = cluster.clone();
let health = health.clone();
let mut feed_health = cluster.subscribe_feed_health();
tokio::spawn(async move {
let monitor = async {
loop {
let state = *feed_health.borrow_and_update();
health.rcu(|snapshot| {
let mut next = (**snapshot).clone();
next.cluster_feed_ready = state.ready;
next.cluster_feed_epoch = state.epoch;
next.cluster_feed_failures = state.failures;
Arc::new(next)
});
if feed_health.changed().await.is_err() {
break;
}
}
};
let run = runner.run();
tokio::pin!(monitor);
tokio::pin!(run);
tokio::select! {
() = &mut monitor => {
tracing::error!("cluster feed health channel ended");
}
result = &mut run => {
if let Err(error) = result {
tracing::error!(%error, "cluster invalidation supervisor stopped");
}
}
}
health.rcu(|snapshot| {
let mut next = (**snapshot).clone();
next.cluster_feed_ready = false;
next.cluster_feed_failures = next.cluster_feed_failures.saturating_add(1);
Arc::new(next)
});
})
};
let reasoning = Arc::new(exocortex_reasoning::ReasoningEngine::new(
storage.clone(),
256,
3,
));
{
let engine = reasoning.clone();
let reasoning_health = health.clone();
background_tasks.push(tokio::spawn(async move {
loop {
reasoning_health.rcu(|snapshot| {
let mut next = (**snapshot).clone();
next.reasoning_alive = true;
Arc::new(next)
});
let outcome = std::panic::AssertUnwindSafe(engine.clone().run())
.catch_unwind()
.await;
reasoning_health.rcu(|snapshot| {
let mut next = (**snapshot).clone();
next.reasoning_alive = false;
Arc::new(next)
});
match outcome {
Ok(()) => tracing::error!("reasoning worker exited; restarting"),
Err(_) => tracing::error!("reasoning worker panicked; restarting"),
}
tokio::time::sleep(Duration::from_millis(25)).await;
}
}));
}
let (distributed_fire, mut fire_drainer) = if let Some(redis_url) = &args.redis_url {
let client = redis::Client::open(redis_url.as_str())?;
let producer = client.get_multiplexed_async_connection().await?;
let drainer = client.get_multiplexed_async_connection().await?;
(
Some(Arc::new(tokio::sync::Mutex::new(
exocortex_dreams::fire::RedisFireQueue::new(
producer,
args.quiet_hours,
args.org.clone(),
),
))),
Some(exocortex_dreams::fire::RedisFireQueue::new(
drainer,
args.quiet_hours,
args.org.clone(),
)),
)
} else {
(None, None)
};
if standalone {
if let Some(queue) = fire_drainer.as_mut() {
queue
.recover_inflight()
.await
.map_err(|error| anyhow::anyhow!("recover standalone Dreams work: {error}"))?;
}
}
let leader_gate = Arc::new(std::sync::atomic::AtomicBool::new(standalone));
let mut dreams_engine = exocortex_dreams::DreamsEngine::new(
storage.clone(),
exocortex_dreams::trigger::DreamsTrigger::default(),
0.01,
0.05,
true,
args.node_id.clone().into(),
)
.with_leader_gate(leader_gate.clone());
if let Some(queue) = &distributed_fire {
dreams_engine = dreams_engine.with_distributed_fire(queue.clone());
}
let dreams = Arc::new(dreams_engine);
{
let engine = dreams.clone();
background_tasks.push(tokio::spawn(async move { engine.run().await }));
}
if let Some(mut queue) = fire_drainer {
let dreams = dreams.clone();
let elected = leader_gate.clone();
background_tasks.push(tokio::spawn(async move {
loop {
if !elected.load(std::sync::atomic::Ordering::SeqCst) {
tokio::time::sleep(LEASE_RENEW).await;
continue;
}
match queue.drain(Duration::from_secs(5)).await {
Ok(exocortex_dreams::fire::DrainResult::Ready(notification)) => {
dreams.notify_distributed(notification);
}
Ok(exocortex_dreams::fire::DrainResult::Deferred) => {
tracing::debug!("Dreams fire durably reordered");
}
Ok(exocortex_dreams::fire::DrainResult::TimedOut) => {}
Err(e) => {
tracing::warn!(%e, "fire drain error; retrying");
tokio::time::sleep(Duration::from_secs(1)).await;
}
}
}
}));
}
let ingest = match standalone_producer_key {
Some(key) => {
IngestServer::new(storage.clone(), ontology.clone(), key).allow_personal_scopes()
}
None => IngestServer::new_with_admin_policies(
storage.clone(),
ontology.clone(),
args.admin_source_policies.clone(),
),
}
.with_reasoning(reasoning.clone())
.with_dreams(dreams.clone())
.with_org(&org)
.require_request_principal();
#[cfg(feature = "fastembed")]
let ingest = ingest.with_embedder(Arc::new(
exocortex_ingest::embedding::FastEmbedder::bge_small()
.map_err(|error| anyhow::anyhow!("initialize bge-small embedder: {error}"))?,
));
let post_ingest_effects = {
let ingest = Arc::new(ingest.clone());
tokio::spawn(async move { ingest.run_post_ingest_effects().await })
};
let _ = preflight_server.set(Arc::new(ingest.clone()));
{
let storage = storage.clone();
let health = health.clone();
background_tasks.push(tokio::spawn(async move {
loop {
let ok = storage.ping().await.is_ok();
health.rcu(|h| {
let mut next = (**h).clone();
next.storage_ok = ok;
Arc::new(next)
});
tokio::time::sleep(Duration::from_secs(2)).await;
}
}));
}
health.rcu(|h| {
let mut next = (**h).clone();
next.reasoning_alive = true;
Arc::new(next)
});
let grpc = tonic::service::Routes::new(
exocortex_wire::ingest::v1::ingest_service_server::IngestServiceServer::new(ingest)
.max_decoding_message_size(exocortex_wire::limits::MAX_MCP_REQUEST_BYTES),
)
.into_axum_router();
let sse = crate::sse::sse_router(cluster.clone());
let app = bind.router(Some(sse.merge(grpc)));
tracing::info!(
%local_addr,
node = %args.node_id,
tls = matches!(args.transport, TransportSecurity::Tls { .. }),
"backend-node serving http+grpc"
);
let ingress =
tokio::spawn(async move { ingress.serve(app).await.map_err(anyhow::Error::from) });
let leader_election = if standalone {
health.rcu(|snapshot| {
let mut next = (**snapshot).clone();
next.leader_node_id = Some(args.node_id.clone());
next.lease_epoch = 0;
next.last_lease_tick = Some(chrono::Utc::now());
Arc::new(next)
});
None
} else {
let storage = storage.clone();
let health = health.clone();
let node_id = args.node_id.clone();
let org = org.to_string();
let elected = leader_gate.clone();
let distributed_fire = distributed_fire.clone();
Some(tokio::spawn(async move {
let key = dreams_lease_key(&org);
loop {
match storage.acquire_lease(&key, LEASE_TTL).await {
Ok(lease) => {
if let Some(queue) = &distributed_fire {
if let Err(error) = queue.lock().await.recover_inflight().await {
tracing::warn!(%error, "Dreams in-flight recovery failed; refusing leadership");
let _ = storage.release_lease(lease).await;
tokio::time::sleep(LEASE_RENEW).await;
continue;
}
}
let mut epoch = lease.epoch;
elected.store(true, std::sync::atomic::Ordering::SeqCst);
metrics::counter!(
"exocortex_cluster_owner_lease_transitions_total",
"role" => "dreams"
)
.increment(1);
health.rcu(|h| {
let mut next = (**h).clone();
next.leader_node_id = Some(node_id.clone());
next.lease_epoch = epoch;
next.last_lease_tick = Some(chrono::Utc::now());
Arc::new(next)
});
loop {
tokio::time::sleep(LEASE_RENEW).await;
match storage.renew_lease(&lease).await {
Ok(l) => {
epoch = l.epoch;
health.rcu(|h| {
let mut next = (**h).clone();
next.lease_epoch = epoch;
next.last_lease_tick = Some(chrono::Utc::now());
Arc::new(next)
});
}
Err(e) => {
tracing::warn!(%e, "dreams lease lost; re-electing");
mark_dreams_follower(&elected, &health);
break;
}
}
}
}
Err(_) => {
mark_dreams_follower(&elected, &health);
tokio::time::sleep(LEASE_RENEW).await;
}
}
}
}))
};
let gossip = if standalone {
None
} else {
Some(spawn_gossip(&args, &ontology.fingerprint).await?)
};
Ok(BackendNode {
health,
local_addr,
cache,
leader_gate,
dreams,
#[cfg(debug_assertions)]
reasoning,
gossip,
cache_bridge: Some(cache_bridge),
cluster_feed: Some(cluster_feed),
post_ingest_effects: Some(post_ingest_effects),
leader_election,
ingress: Some(ingress),
_background_tasks: background_tasks,
})
}
enum BoundIngress {
Plaintext(tokio::net::TcpListener),
Tls {
listener: std::net::TcpListener,
config: axum_server::tls_rustls::RustlsConfig,
},
}
impl BoundIngress {
async fn bind(bind: &str, transport: &TransportSecurity) -> anyhow::Result<Self> {
match transport {
TransportSecurity::PlaintextLoopback => {
let address: SocketAddr = bind.parse().map_err(|_| {
anyhow::anyhow!(
"plaintext loopback bind must be a literal socket address, got {bind:?}"
)
})?;
anyhow::ensure!(
address.ip().is_loopback(),
"plaintext transport is restricted to loopback; {address} is shared"
);
Ok(Self::Plaintext(
tokio::net::TcpListener::bind(address).await?,
))
}
TransportSecurity::Tls {
certificate,
private_key,
} => {
let _ = rustls::crypto::ring::default_provider().install_default();
let config =
axum_server::tls_rustls::RustlsConfig::from_pem_file(certificate, private_key)
.await
.map_err(|e| anyhow::anyhow!("load TLS certificate/private key: {e}"))?;
let listener = std::net::TcpListener::bind(bind)?;
listener.set_nonblocking(true)?;
Ok(Self::Tls { listener, config })
}
}
}
fn local_addr(&self) -> std::io::Result<SocketAddr> {
match self {
Self::Plaintext(listener) => listener.local_addr(),
Self::Tls { listener, .. } => listener.local_addr(),
}
}
async fn serve(self, app: axum::Router) -> std::io::Result<()> {
match self {
Self::Plaintext(listener) => axum::serve(listener, app).await,
Self::Tls { listener, config } => {
axum_server::from_tcp_rustls(listener, config)
.serve(app.into_make_service())
.await
}
}
}
}
async fn spawn_gossip(
args: &BackendNodeArgs,
fp: &OntologyFingerprint,
) -> anyhow::Result<chitchat::ChitchatHandle> {
use chitchat::{ChitchatConfig, ChitchatId, FailureDetectorConfig};
let config = ChitchatConfig {
chitchat_id: ChitchatId::new(
args.node_id.clone(),
chrono::Utc::now().timestamp() as u64,
args.gossip_listen,
),
cluster_id: "exocortex".into(),
gossip_interval: Duration::from_millis(500),
listen_addr: args.gossip_listen,
seed_nodes: args.seed_nodes.clone(),
failure_detector_config: FailureDetectorConfig::default(),
marked_for_deletion_grace_period: Duration::from_secs(10),
catchup_callback: None,
extra_liveness_predicate: None,
};
let initial = vec![
(
"wire_version".to_string(),
exocortex_wire::WIRE_VERSION.to_string(),
),
("ontology_fingerprint".to_string(), hex(&fp.0)),
("http_addr".to_string(), args.bind.clone()),
];
let transport = chitchat::transport::UdpTransport;
chitchat::spawn_chitchat(config, initial, &transport).await
}
fn hex(b: &[u8; 32]) -> String {
use std::fmt::Write as _;
let mut out = String::with_capacity(64);
for byte in b {
let _ = write!(out, "{byte:02x}");
}
out
}
#[cfg(test)]
mod cache_bridge_tests {
use std::sync::atomic::{AtomicUsize, Ordering};
use super::*;
#[tokio::test(start_paused = true)]
async fn failed_reseeds_back_off_exponentially_and_cap() {
let ontology = Arc::new(
exocortex_kernel::Ontology::from_packs(vec![exocortex_pack_dev_v1::pack_def()])
.unwrap(),
);
let storage = Arc::new(exocortex_storage::InMemoryStorage::new(ontology));
let (cache, writer_rx) = LocalCache::new(1024 * 1024);
let cache = Arc::new(cache);
let writer = tokio::spawn({
let cache = cache.clone();
let storage = storage.clone();
async move { cache.run(storage, writer_rx).await }
});
let attempts = AtomicUsize::new(0);
let started = tokio::time::Instant::now();
retry_with_capped_backoff(
|| {
let attempt = attempts.fetch_add(1, Ordering::SeqCst);
let cache = cache.clone();
let storage = storage.clone();
async move {
if attempt < 4 {
storage.fail_next_stream_after(Some(0), None);
}
cache.reseed_from_storage(&*storage, &"org".into()).await
}
},
Duration::from_millis(10),
Duration::from_millis(25),
)
.await;
assert_eq!(attempts.load(Ordering::SeqCst), 5);
assert_eq!(started.elapsed(), Duration::from_millis(80));
assert_eq!(cache.resident_orgs(), 1);
writer.abort();
}
}