use dataflow_rs::datalogic_rs;
use std::collections::HashMap;
use std::sync::Arc;
use arc_swap::ArcSwap;
use datalogic_rs::{Engine as DatalogicEngine, Logic};
use tokio::sync::{Mutex, Semaphore};
use super::config::ChannelConfig;
use super::rate_limit_backend::{LocalRateLimitBackend, RateLimitBackend, RedisRateLimitBackend};
use super::routing::{RouteMatch, RouteTable};
use crate::config::{TraceStorageConfig, TraceStorageMode};
use crate::connector::ConnectorConfig;
use crate::connector::ConnectorRegistry;
use crate::connector::cache_backend::{CacheBackend, CachePool, CachePurpose};
use crate::storage::models::Channel;
#[derive(Debug, Clone, Copy)]
pub struct EffectiveTraceConfig {
pub mode: TraceStorageMode,
pub sample_rate: f64,
pub errors_only: bool,
pub task_details: bool,
}
impl EffectiveTraceConfig {
pub fn draw_sample(&self) -> bool {
self.sample_rate >= 1.0 || rand::random::<f64>() < self.sample_rate
}
pub fn should_drop(&self, has_errors: bool, sampled_in: bool) -> Option<&'static str> {
if matches!(self.mode, TraceStorageMode::Off) {
return Some("off");
}
if self.errors_only && !has_errors {
return Some("errors_only");
}
if !sampled_in {
return Some("sampled_out");
}
None
}
pub fn for_async_submission(self) -> Self {
Self {
mode: match self.mode {
TraceStorageMode::Off => TraceStorageMode::Sync,
other => other,
},
sample_rate: 1.0,
..self
}
}
pub fn resolve(
global: &TraceStorageConfig,
channel: Option<&super::config::ChannelTracingConfig>,
) -> Self {
let (mode, sample_rate, errors_only, task_details) = match channel {
Some(c) => (
c.mode.unwrap_or(global.mode),
c.sample_rate.unwrap_or(global.sample_rate),
c.errors_only.unwrap_or(global.errors_only),
c.task_details.unwrap_or(false),
),
None => (global.mode, global.sample_rate, global.errors_only, false),
};
Self {
mode,
sample_rate,
errors_only,
task_details,
}
}
}
pub struct ChannelRuntimeConfig {
pub channel: Channel,
pub parsed_config: ChannelConfig,
pub rate_limiter: Option<Arc<dyn RateLimitBackend>>,
pub rate_limit_key_logic: Option<Logic>,
pub validation_logic: Option<Logic>,
pub backpressure_semaphore: Option<Arc<Semaphore>>,
pub dedup_store: Option<Arc<dyn CacheBackend>>,
pub response_cache: Option<Arc<dyn CacheBackend>>,
pub trace_storage: EffectiveTraceConfig,
pub auth: Option<crate::channel::auth::CompiledAuth>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ChannelLoadIssue {
pub channel: String,
pub reason: String,
}
pub struct ClusterBackends {
pub default_cache: Option<Arc<dyn CacheBackend>>,
pub redis: Option<redis::aio::ConnectionManager>,
}
#[derive(Clone, PartialEq, Eq)]
struct ChannelRow {
channel_id: String,
version: i64,
updated_at: chrono::NaiveDateTime,
}
impl ChannelRow {
fn of(channel: &Channel) -> Self {
Self {
channel_id: channel.channel_id.clone(),
version: channel.version,
updated_at: channel.updated_at,
}
}
fn same_row(a: &Channel, b: &Channel) -> bool {
a.channel_id == b.channel_id && a.version == b.version && a.updated_at == b.updated_at
}
}
#[derive(Clone, PartialEq)]
struct DepsFingerprint {
connectors: u64,
trace_storage: TraceStorageConfig,
}
struct RegistrySnapshot {
by_name: HashMap<String, Arc<ChannelRuntimeConfig>>,
route_table: Arc<RouteTable>,
quarantined: HashMap<String, String>,
route_key: Vec<ChannelRow>,
deps: DepsFingerprint,
}
impl RegistrySnapshot {
fn empty() -> Self {
Self {
by_name: HashMap::new(),
route_table: Arc::new(RouteTable::default()),
quarantined: HashMap::new(),
route_key: Vec::new(),
deps: DepsFingerprint {
connectors: u64::MAX,
trace_storage: TraceStorageConfig::default(),
},
}
}
}
struct RuntimeDeps<'a> {
connector_registry: &'a ConnectorRegistry,
cache_pool: &'a CachePool,
datalogic: &'a DatalogicEngine,
global_trace_storage: &'a TraceStorageConfig,
cluster_redis: Option<redis::aio::ConnectionManager>,
}
pub struct ChannelRegistry {
snapshot: ArcSwap<RegistrySnapshot>,
reload_lock: Mutex<()>,
cluster: Option<ClusterBackends>,
}
impl Default for ChannelRegistry {
fn default() -> Self {
Self::new()
}
}
impl ChannelRegistry {
pub fn new() -> Self {
Self {
snapshot: ArcSwap::from_pointee(RegistrySnapshot::empty()),
reload_lock: Mutex::new(()),
cluster: None,
}
}
pub fn with_cluster(cluster: ClusterBackends) -> Self {
Self {
cluster: Some(cluster),
..Self::new()
}
}
pub fn quarantine_reason(&self, name: &str) -> Option<String> {
self.snapshot.load().quarantined.get(name).cloned()
}
pub fn quarantined(&self) -> Vec<ChannelLoadIssue> {
self.snapshot
.load()
.quarantined
.iter()
.map(|(channel, reason)| ChannelLoadIssue {
channel: channel.clone(),
reason: reason.clone(),
})
.collect()
}
pub fn require_serviceable(
&self,
name: &str,
) -> Result<Option<Arc<ChannelRuntimeConfig>>, crate::errors::OrionError> {
let snapshot = self.snapshot.load();
if let Some(reason) = snapshot.quarantined.get(name) {
return Err(crate::errors::OrionError::ServiceUnavailable(format!(
"Channel '{name}' failed to load and is not being served: {reason}"
)));
}
Ok(snapshot.by_name.get(name).cloned())
}
async fn resolve_backend(
&self,
connector_registry: &ConnectorRegistry,
cache_pool: &CachePool,
connector: Option<&str>,
purpose: CachePurpose,
channel_name: &str,
) -> Result<Arc<dyn CacheBackend>, ChannelLoadIssue> {
let Some(connector_name) = connector else {
return Ok(self
.cluster
.as_ref()
.and_then(|c| c.default_cache.clone())
.unwrap_or_else(|| cache_pool.default_memory(purpose)));
};
let strict = self.cluster.is_some();
let resolved = match connector_registry.get(connector_name).await {
Some(cfg) => match cfg.as_ref() {
ConnectorConfig::Cache(cache_cfg) => {
if !cache_cfg.operations.write {
Err(format!(
"connector '{connector_name}' has operations.write = false, \
and {purpose} writes through it"
))
} else if strict && cache_cfg.backend == "memory" {
Err(format!(
"connector '{connector_name}' uses the in-memory backend — \
per-node state in cluster mode is a silent correctness loss"
))
} else {
cache_pool
.get_backend(purpose, connector_name, cache_cfg)
.await
.map_err(|e| {
format!(
"failed to create backend from connector '{connector_name}': {e}"
)
})
}
}
_ => Err(format!(
"connector '{connector_name}' is not a cache connector"
)),
},
None => Err(format!("connector '{connector_name}' not found")),
};
match resolved {
Ok(backend) => Ok(backend),
Err(reason) if strict => Err(ChannelLoadIssue {
channel: channel_name.to_string(),
reason: format!("{purpose}: {reason}"),
}),
Err(reason) => {
tracing::warn!(
channel = %channel_name,
connector = %connector_name,
reason = %reason,
"{purpose} connector unavailable, falling back to in-memory"
);
Ok(cache_pool.default_memory(purpose))
}
}
}
pub fn get_by_name(&self, name: &str) -> Option<Arc<ChannelRuntimeConfig>> {
self.snapshot.load().by_name.get(name).cloned()
}
pub fn match_route(
&self,
method: &str,
path: &str,
) -> Result<Option<RouteMatch>, crate::errors::OrionError> {
self.snapshot.load().route_table.match_route(method, path)
}
async fn build_runtime(
&self,
channel: &Channel,
prior: Option<&ChannelRuntimeConfig>,
deps: &RuntimeDeps<'_>,
) -> Result<Arc<ChannelRuntimeConfig>, ChannelLoadIssue> {
let issue = |reason: String| ChannelLoadIssue {
channel: channel.name.clone(),
reason,
};
let parsed_config: ChannelConfig =
serde_json::from_str(&channel.config_json).map_err(|e| {
tracing::error!(
channel = %channel.name,
error = %e,
"Refusing to load channel: config_json does not parse"
);
issue(format!("config_json does not parse: {e}"))
})?;
let rate_limiter: Option<Arc<dyn RateLimitBackend>> =
parsed_config.rate_limit.as_ref().map(|rl| {
if let Some(prev) = prior
&& let Some(prev_rl) = prev.parsed_config.rate_limit.as_ref()
&& let Some(prev_limiter) = prev.rate_limiter.as_ref()
&& prev_rl.requests_per_second == rl.requests_per_second
&& prev_rl.burst == rl.burst
&& prev_rl.key_logic == rl.key_logic
{
return prev_limiter.clone();
}
let burst = rl.burst.unwrap_or(rl.requests_per_second / 2 + 1);
match deps.cluster_redis.clone() {
Some(conn) => Arc::new(RedisRateLimitBackend::new(
conn,
channel.name.clone(),
rl.requests_per_second,
burst,
)) as Arc<dyn RateLimitBackend>,
None => Arc::new(LocalRateLimitBackend::new(rl.requests_per_second, burst)),
}
});
let rate_limit_key_logic = parsed_config
.rate_limit
.as_ref()
.and_then(|rl| rl.key_logic.as_ref())
.map(|logic| {
deps.datalogic.compile(logic).map_err(|e| {
tracing::error!(
channel = %channel.name,
error = %e,
"Refusing to load channel: rate_limit.key_logic does not compile"
);
issue(format!("rate_limit.key_logic does not compile: {e}"))
})
})
.transpose()?;
let validation_logic = parsed_config
.validation_logic
.as_ref()
.map(|logic| {
deps.datalogic.compile(logic).map_err(|e| {
tracing::error!(
channel = %channel.name,
error = %e,
"Refusing to load channel: validation_logic does not compile"
);
issue(format!("validation_logic does not compile: {e}"))
})
})
.transpose()?;
let backpressure_semaphore = parsed_config.backpressure.as_ref().map(|bp| {
if let Some(prev) = prior
&& let Some(prev_bp) = prev.parsed_config.backpressure.as_ref()
&& let Some(prev_sem) = prev.backpressure_semaphore.as_ref()
&& prev_bp.max_concurrent_per_node == bp.max_concurrent_per_node
{
return prev_sem.clone();
}
Arc::new(Semaphore::new(bp.max_concurrent_per_node))
});
let dedup_store: Option<Arc<dyn CacheBackend>> = match parsed_config.deduplication {
Some(ref dedup) => Some(
self.resolve_backend(
deps.connector_registry,
deps.cache_pool,
dedup.connector.as_deref(),
CachePurpose::Dedup,
&channel.name,
)
.await?,
),
None => None,
};
let response_cache: Option<Arc<dyn CacheBackend>> = match parsed_config.cache {
Some(ref cache_cfg) if cache_cfg.enabled => Some(
self.resolve_backend(
deps.connector_registry,
deps.cache_pool,
cache_cfg.connector.as_deref(),
CachePurpose::ResponseCache,
&channel.name,
)
.await?,
),
_ => None,
};
let trace_storage = EffectiveTraceConfig::resolve(
deps.global_trace_storage,
parsed_config.tracing.as_ref(),
);
let auth = match parsed_config.auth.as_ref() {
Some(cfg) => Some(
crate::channel::auth::CompiledAuth::compile(cfg)
.await
.map_err(|e| {
tracing::error!(
channel = %channel.name,
error = %e,
"Refusing to load channel: auth config cannot be compiled"
);
issue(format!("auth: {e}"))
})?,
),
None => None,
};
Ok(Arc::new(ChannelRuntimeConfig {
channel: channel.clone(),
parsed_config,
rate_limiter,
rate_limit_key_logic,
validation_logic,
backpressure_semaphore,
dedup_store,
response_cache,
trace_storage,
auth,
}))
}
pub async fn reload(
&self,
channels: &[Channel],
connector_registry: &ConnectorRegistry,
cache_pool: &CachePool,
datalogic: &DatalogicEngine,
global_trace_storage: &TraceStorageConfig,
engine_issues: Vec<ChannelLoadIssue>,
) {
let _reload_guard = self.reload_lock.lock().await;
let deps = RuntimeDeps {
connector_registry,
cache_pool,
datalogic,
global_trace_storage,
cluster_redis: self.cluster.as_ref().and_then(|c| c.redis.clone()),
};
let fingerprint = DepsFingerprint {
connectors: connector_registry.config_generation(),
trace_storage: global_trace_storage.clone(),
};
let previous = self.snapshot.load_full();
let cache_valid = previous.deps == fingerprint;
let mut issues: Vec<ChannelLoadIssue> = engine_issues;
let mut by_name: HashMap<String, Arc<ChannelRuntimeConfig>> =
HashMap::with_capacity(channels.len());
let mut reused = 0usize;
for channel in channels {
let prior = previous.by_name.get(&channel.name);
if cache_valid
&& let Some(prev) = prior
&& ChannelRow::same_row(&prev.channel, channel)
{
by_name.insert(channel.name.clone(), prev.clone());
reused += 1;
continue;
}
match self
.build_runtime(channel, prior.map(|p| p.as_ref()), &deps)
.await
{
Ok(runtime) => {
by_name.insert(channel.name.clone(), runtime);
}
Err(issue) => issues.push(issue),
}
}
let quarantined: HashMap<String, String> = issues
.iter()
.map(|i| (i.channel.clone(), i.reason.clone()))
.collect();
by_name.retain(|name, _| !quarantined.contains_key(name));
let serviceable: Vec<&Channel> = channels
.iter()
.filter(|c| !quarantined.contains_key(&c.name))
.collect();
let route_key: Vec<ChannelRow> = serviceable.iter().copied().map(ChannelRow::of).collect();
let route_table = if route_key == previous.route_key {
previous.route_table.clone()
} else {
Arc::new(RouteTable::build(serviceable.iter().copied()))
};
let loaded = by_name.len();
let refused = quarantined.len();
self.snapshot.store(Arc::new(RegistrySnapshot {
by_name,
route_table,
quarantined,
route_key,
deps: fingerprint,
}));
if refused > 0 {
tracing::error!(
quarantined = refused,
loaded,
"Some channels failed to load and are being refused at every \
ingress. See /health for the list; the rest of the instance \
is unaffected."
);
}
tracing::debug!(
channels = channels.len(),
reused,
"Channel registry snapshot published"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::connector::test_support::StubConnectorRepo;
const MEMORY_CACHE: &str = r#"{"backend":"memory"}"#;
#[tokio::test]
async fn test_channel_registry_empty() {
let registry = ChannelRegistry::new();
assert!(registry.get_by_name("nonexistent").is_none());
assert!(registry.snapshot.load().by_name.is_empty());
}
fn test_channel(name: &str, config_json: &str) -> Channel {
let now = chrono::Utc::now().naive_utc();
Channel {
tags_json: "[]".to_string(),
channel_id: format!("ch_{name}"),
version: 1,
name: name.to_string(),
description: None,
channel_type: "sync".to_string(),
protocol: "http".to_string(),
methods_json: None,
route_pattern: None,
topic: None,
consumer_group: None,
transport_config_json: "{}".to_string(),
workflow_id: None,
config_json: config_json.to_string(),
status: "active".to_string(),
priority: 0,
created_at: now,
updated_at: now,
}
}
fn rest_channel(name: &str, route_pattern: &str) -> Channel {
Channel {
protocol: "rest".to_string(),
route_pattern: Some(route_pattern.to_string()),
..test_channel(name, "{}")
}
}
struct TestDeps {
connectors: ConnectorRegistry,
cache_pool: CachePool,
datalogic: DatalogicEngine,
trace_storage: TraceStorageConfig,
}
impl TestDeps {
fn new() -> Self {
Self {
connectors: ConnectorRegistry::new(
crate::config::EngineConfig::default().circuit_breaker,
),
cache_pool: CachePool::new(4, 60, 1000),
datalogic: DatalogicEngine::new(),
trace_storage: TraceStorageConfig::default(),
}
}
async fn reload(&self, registry: &ChannelRegistry, channels: &[Channel]) {
self.reload_with_issues(registry, channels, Vec::new())
.await;
}
async fn reload_with_issues(
&self,
registry: &ChannelRegistry,
channels: &[Channel],
engine_issues: Vec<ChannelLoadIssue>,
) {
registry
.reload(
channels,
&self.connectors,
&self.cache_pool,
&self.datalogic,
&self.trace_storage,
engine_issues,
)
.await;
}
}
fn cluster_backends() -> ClusterBackends {
ClusterBackends {
default_cache: Some(CachePool::new(4, 60, 1000).default_memory(CachePurpose::Dedup)),
redis: None,
}
}
#[tokio::test]
async fn test_cluster_strict_mode_refuses_broken_dedup_connector() {
let registry = ChannelRegistry::with_cluster(cluster_backends());
let channel = test_channel(
"strict-ch",
r#"{"deduplication": {"header": "idem", "connector": "missing-connector"}}"#,
);
TestDeps::new().reload(®istry, &[channel]).await;
let issues = registry.quarantined();
assert_eq!(issues.len(), 1);
assert!(issues[0].reason.contains("missing-connector"));
assert!(registry.get_by_name("strict-ch").is_none());
}
#[tokio::test]
async fn test_cluster_mode_defaults_dedup_to_shared_cache() {
let registry = ChannelRegistry::with_cluster(cluster_backends());
let channel = test_channel("shared-ch", r#"{"deduplication": {"header": "idem"}}"#);
TestDeps::new().reload(®istry, &[channel]).await;
assert!(registry.quarantined().is_empty());
let runtime = registry.get_by_name("shared-ch").expect("loaded");
assert!(runtime.dedup_store.is_some());
}
async fn registry_with_cache(name: &str, write: bool) -> ConnectorRegistry {
let registry =
ConnectorRegistry::new(crate::config::EngineConfig::default().circuit_breaker);
let config: crate::connector::ConnectorConfig = serde_json::from_value(serde_json::json!({
"type": "cache",
"backend": "memory",
"operations": { "write": write }
}))
.expect("a cache connector config");
registry.insert_for_test(name, config).await;
registry
}
#[tokio::test]
async fn test_write_gated_connector_cannot_back_a_dedup_store() {
let registry = ChannelRegistry::with_cluster(cluster_backends());
let channel = test_channel(
"gated-dedup-ch",
r#"{"deduplication": {"header": "idem", "connector": "ro-cache"}}"#,
);
registry
.reload(
&[channel],
®istry_with_cache("ro-cache", false).await,
&CachePool::new(4, 60, 1000),
&DatalogicEngine::new(),
&TraceStorageConfig::default(),
Vec::new(),
)
.await;
let issues = registry.quarantined();
assert_eq!(issues.len(), 1, "issues = {issues:?}");
assert!(
issues[0].reason.contains("operations.write"),
"the reason must name the gate, got: {}",
issues[0].reason
);
assert!(registry.get_by_name("gated-dedup-ch").is_none());
}
#[tokio::test]
async fn test_ungated_connector_still_backs_a_dedup_store() {
let registry = ChannelRegistry::new();
let channel = test_channel(
"open-dedup-ch",
r#"{"deduplication": {"header": "idem", "connector": "rw-cache"}}"#,
);
registry
.reload(
&[channel],
®istry_with_cache("rw-cache", true).await,
&CachePool::new(4, 60, 1000),
&DatalogicEngine::new(),
&TraceStorageConfig::default(),
Vec::new(),
)
.await;
let issues = registry.quarantined();
assert!(issues.is_empty(), "issues = {issues:?}");
let runtime = registry.get_by_name("open-dedup-ch").expect("loaded");
assert!(runtime.dedup_store.is_some());
}
#[tokio::test]
async fn test_single_node_write_gated_connector_falls_back() {
let registry = ChannelRegistry::new();
let channel = test_channel(
"gated-fallback-ch",
r#"{"deduplication": {"header": "idem", "connector": "ro-cache"}}"#,
);
registry
.reload(
&[channel],
®istry_with_cache("ro-cache", false).await,
&CachePool::new(4, 60, 1000),
&DatalogicEngine::new(),
&TraceStorageConfig::default(),
Vec::new(),
)
.await;
let issues = registry.quarantined();
assert!(issues.is_empty(), "issues = {issues:?}");
assert!(registry.get_by_name("gated-fallback-ch").is_some());
}
#[tokio::test]
async fn test_single_node_broken_connector_still_falls_back() {
let registry = ChannelRegistry::new();
let channel = test_channel(
"fallback-ch",
r#"{"deduplication": {"header": "idem", "connector": "missing-connector"}}"#,
);
TestDeps::new().reload(®istry, &[channel]).await;
assert!(registry.quarantined().is_empty());
assert!(registry.get_by_name("fallback-ch").is_some());
}
async fn reload_single_node(channel: Channel) -> (ChannelRegistry, Vec<ChannelLoadIssue>) {
let registry = ChannelRegistry::new();
let issues = reload_into(®istry, channel).await;
(registry, issues)
}
async fn reload_into(registry: &ChannelRegistry, channel: Channel) -> Vec<ChannelLoadIssue> {
TestDeps::new().reload(registry, &[channel]).await;
registry.quarantined()
}
#[tokio::test]
async fn test_malformed_config_json_refuses_to_load_single_node() {
let channel = test_channel(
"broken-cfg-ch",
r#"{"rate_limit": {"requests_per_second": "100"}}"#,
);
let (registry, issues) = reload_single_node(channel).await;
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].channel, "broken-cfg-ch");
assert!(
issues[0].reason.contains("config_json does not parse"),
"unexpected reason: {}",
issues[0].reason
);
assert!(registry.get_by_name("broken-cfg-ch").is_none());
}
#[tokio::test]
async fn test_malformed_config_json_is_not_valid_json_refuses() {
let channel = test_channel("not-json-ch", "{ this is not json ");
let (registry, issues) = reload_single_node(channel).await;
assert_eq!(issues.len(), 1);
assert!(registry.get_by_name("not-json-ch").is_none());
}
#[tokio::test]
async fn test_uncompilable_validation_logic_refuses_to_load_single_node() {
let channel = test_channel(
"bad-logic-ch",
r#"{"validation_logic": {"==": [1, 1], "!=": [1, 2]}}"#,
);
let (registry, issues) = reload_single_node(channel).await;
assert_eq!(issues.len(), 1);
assert_eq!(issues[0].channel, "bad-logic-ch");
assert!(
issues[0]
.reason
.contains("validation_logic does not compile"),
"unexpected reason: {}",
issues[0].reason
);
assert!(registry.get_by_name("bad-logic-ch").is_none());
}
#[tokio::test]
async fn test_valid_validation_logic_still_loads() {
let channel = test_channel(
"good-logic-ch",
r#"{"validation_logic": {"!!": [{"var": "data.id"}]}}"#,
);
let (registry, issues) = reload_single_node(channel).await;
assert!(issues.is_empty(), "unexpected issues: {issues:?}");
let runtime = registry.get_by_name("good-logic-ch").expect("loaded");
assert!(runtime.validation_logic.is_some());
}
#[tokio::test]
async fn test_refusal_leaves_previous_registry_intact() {
let registry = ChannelRegistry::new();
let deps = TestDeps::new();
deps.reload(®istry, &[test_channel("keep-ch", "{}")])
.await;
assert!(registry.quarantined().is_empty());
deps.reload(
®istry,
&[
test_channel("keep-ch", "{}"),
test_channel("broken-ch", "{ nope"),
],
)
.await;
assert_eq!(registry.quarantined().len(), 1);
assert!(registry.get_by_name("keep-ch").is_some());
assert!(registry.get_by_name("broken-ch").is_none());
}
fn global_tracing(
mode: TraceStorageMode,
sample_rate: f64,
errors_only: bool,
) -> TraceStorageConfig {
TraceStorageConfig {
mode,
sample_rate,
errors_only,
..Default::default()
}
}
#[test]
fn effective_trace_config_overlays_channel_fields_over_global() {
let global = global_tracing(TraceStorageMode::Sync, 0.5, false);
let eff = EffectiveTraceConfig::resolve(&global, None);
assert!(matches!(eff.mode, TraceStorageMode::Sync));
assert_eq!(eff.sample_rate, 0.5);
assert!(!eff.errors_only);
assert!(!eff.task_details);
let channel = super::super::config::ChannelTracingConfig {
mode: Some(TraceStorageMode::Off),
sample_rate: Some(0.1),
errors_only: Some(true),
task_details: Some(true),
};
let eff = EffectiveTraceConfig::resolve(&global, Some(&channel));
assert!(matches!(eff.mode, TraceStorageMode::Off));
assert_eq!(eff.sample_rate, 0.1);
assert!(eff.errors_only);
assert!(eff.task_details);
let channel = super::super::config::ChannelTracingConfig {
mode: None,
sample_rate: None,
errors_only: None,
task_details: None,
};
let eff = EffectiveTraceConfig::resolve(&global, Some(&channel));
assert!(matches!(eff.mode, TraceStorageMode::Sync));
assert_eq!(eff.sample_rate, 0.5);
assert!(!eff.errors_only);
assert!(!eff.task_details, "task_details has no global to inherit");
}
#[test]
fn should_drop_filters_in_precedence_order() {
let off =
EffectiveTraceConfig::resolve(&global_tracing(TraceStorageMode::Off, 1.0, false), None);
assert_eq!(
off.should_drop(true, true),
Some("off"),
"Off wins even for errors"
);
assert_eq!(
off.should_drop(false, false),
Some("off"),
"Off outranks sampled_out"
);
let errors_only =
EffectiveTraceConfig::resolve(&global_tracing(TraceStorageMode::Sync, 1.0, true), None);
assert_eq!(errors_only.should_drop(false, true), Some("errors_only"));
assert_eq!(
errors_only.should_drop(true, true),
None,
"error traces are spared"
);
let sync = EffectiveTraceConfig::resolve(
&global_tracing(TraceStorageMode::Sync, 0.5, false),
None,
);
assert_eq!(
sync.should_drop(false, false),
Some("sampled_out"),
"a sampled-out trace is dropped"
);
assert_eq!(sync.should_drop(false, true), None);
assert_eq!(sync.should_drop(true, true), None);
}
#[test]
fn draw_sample_is_deterministic_at_the_extremes() {
let never = EffectiveTraceConfig::resolve(
&global_tracing(TraceStorageMode::Sync, 0.0, false),
None,
);
let always = EffectiveTraceConfig::resolve(
&global_tracing(TraceStorageMode::Sync, 1.0, false),
None,
);
for _ in 0..64 {
assert!(!never.draw_sample(), "rate 0.0 must never sample in");
assert!(always.draw_sample(), "rate 1.0 must always sample in");
}
}
#[test]
fn for_async_submission_never_samples_out() {
let eff =
EffectiveTraceConfig::resolve(&global_tracing(TraceStorageMode::Off, 0.0, true), None)
.for_async_submission();
assert!(matches!(eff.mode, TraceStorageMode::Sync));
assert_eq!(eff.sample_rate, 1.0);
assert!(eff.draw_sample(), "pinned rate must never enter the roll");
assert!(eff.errors_only, "errors_only is deliberately left alone");
}
#[tokio::test]
async fn test_reload_preserves_rate_limiter_state_when_unchanged() {
let registry = ChannelRegistry::new();
let config = r#"{"rate_limit": {"requests_per_second": 1, "burst": 2}}"#;
let issues = reload_into(®istry, test_channel("rl-ch", config)).await;
assert!(issues.is_empty(), "{issues:?}");
let limiter = registry
.get_by_name("rl-ch")
.expect("loaded")
.rate_limiter
.clone()
.expect("limiter configured");
assert!(limiter.check("k".to_string()).await.expect("test"));
assert!(limiter.check("k".to_string()).await.expect("test"));
assert!(
!limiter.check("k".to_string()).await.expect("test"),
"burst of 2 must be consumed"
);
let issues = reload_into(®istry, test_channel("rl-ch", config)).await;
assert!(issues.is_empty(), "{issues:?}");
let limiter = registry
.get_by_name("rl-ch")
.expect("loaded")
.rate_limiter
.clone()
.expect("limiter configured");
assert!(
!limiter.check("k".to_string()).await.expect("test"),
"reload must not refill the consumed burst"
);
}
#[tokio::test]
async fn test_reload_rebuilds_limiter_when_limits_change() {
let registry = ChannelRegistry::new();
let _ = reload_into(
®istry,
test_channel(
"rl-ch",
r#"{"rate_limit": {"requests_per_second": 1, "burst": 2}}"#,
),
)
.await;
let limiter = registry
.get_by_name("rl-ch")
.expect("loaded")
.rate_limiter
.clone()
.expect("limiter");
while limiter.check("k".to_string()).await.expect("test") {}
let _ = reload_into(
®istry,
test_channel(
"rl-ch",
r#"{"rate_limit": {"requests_per_second": 1, "burst": 10}}"#,
),
)
.await;
let limiter = registry
.get_by_name("rl-ch")
.expect("loaded")
.rate_limiter
.clone()
.expect("limiter");
assert!(
limiter.check("k".to_string()).await.expect("test"),
"a changed limit must take effect on reload"
);
}
#[tokio::test]
async fn test_reload_reuses_backpressure_semaphore_when_unchanged() {
let registry = ChannelRegistry::new();
let config = r#"{"backpressure": {"max_concurrent_per_node": 3}}"#;
let _ = reload_into(®istry, test_channel("bp-ch", config)).await;
let sem = registry
.get_by_name("bp-ch")
.expect("loaded")
.backpressure_semaphore
.clone()
.expect("semaphore");
let _ = reload_into(®istry, test_channel("bp-ch", config)).await;
let sem_after = registry
.get_by_name("bp-ch")
.expect("loaded")
.backpressure_semaphore
.clone()
.expect("semaphore");
assert!(
Arc::ptr_eq(&sem, &sem_after),
"unchanged max_concurrent_per_node must keep the same semaphore"
);
let _ = reload_into(
®istry,
test_channel(
"bp-ch",
r#"{"backpressure": {"max_concurrent_per_node": 5}}"#,
),
)
.await;
let sem_changed = registry
.get_by_name("bp-ch")
.expect("loaded")
.backpressure_semaphore
.clone()
.expect("semaphore");
assert!(
!Arc::ptr_eq(&sem, &sem_changed),
"a changed limit needs a fresh semaphore"
);
assert_eq!(sem_changed.available_permits(), 5);
}
#[tokio::test]
async fn test_reload_keeps_in_flight_backpressure_permits() {
let registry = ChannelRegistry::new();
let config = r#"{"backpressure": {"max_concurrent_per_node": 2}}"#;
let _ = reload_into(®istry, test_channel("bp-ch", config)).await;
let sem = registry
.get_by_name("bp-ch")
.expect("loaded")
.backpressure_semaphore
.clone()
.expect("semaphore");
let _held = sem.clone().try_acquire_owned().expect("permit");
let _ = reload_into(®istry, test_channel("bp-ch", config)).await;
let sem_after = registry
.get_by_name("bp-ch")
.expect("loaded")
.backpressure_semaphore
.clone()
.expect("semaphore");
assert_eq!(
sem_after.available_permits(),
1,
"the in-flight permit must survive the reload"
);
}
#[tokio::test]
async fn test_reload_reuses_runtime_config_when_nothing_changed() {
let registry = ChannelRegistry::new();
let deps = TestDeps::new();
let channel = test_channel(
"reuse-ch",
r#"{"validation_logic": {"!!": [{"var": "data.id"}]},
"rate_limit": {"requests_per_second": 10},
"deduplication": {"header": "idem"}}"#,
);
deps.reload(®istry, std::slice::from_ref(&channel)).await;
let first = registry.get_by_name("reuse-ch").expect("loaded");
deps.reload(®istry, std::slice::from_ref(&channel)).await;
let second = registry.get_by_name("reuse-ch").expect("loaded");
assert!(
Arc::ptr_eq(&first, &second),
"an unchanged channel must keep its runtime config across a reload"
);
}
#[tokio::test]
async fn test_reload_rebuilds_runtime_config_when_the_row_changes() {
let registry = ChannelRegistry::new();
let deps = TestDeps::new();
let channel = test_channel("edit-ch", r#"{"rate_limit": {"requests_per_second": 10}}"#);
deps.reload(®istry, std::slice::from_ref(&channel)).await;
let first = registry.get_by_name("edit-ch").expect("loaded");
let edited = Channel {
version: 2,
config_json: r#"{"rate_limit": {"requests_per_second": 20}}"#.to_string(),
updated_at: channel.updated_at + chrono::Duration::seconds(1),
..channel
};
deps.reload(®istry, &[edited]).await;
let second = registry.get_by_name("edit-ch").expect("loaded");
assert!(!Arc::ptr_eq(&first, &second), "an edited row must rebuild");
assert_eq!(
second
.parsed_config
.rate_limit
.as_ref()
.expect("rate limit")
.requests_per_second,
20
);
}
#[tokio::test]
async fn test_reload_rebuilds_when_the_connector_generation_moves() {
let registry = ChannelRegistry::new();
let deps = TestDeps::new();
let repo = StubConnectorRepo::with(vec![("dedup-cache", "cache", MEMORY_CACHE)]);
deps.connectors
.reload(&repo)
.await
.expect("connectors load");
let channel = test_channel(
"dep-ch",
r#"{"deduplication": {"header": "idem", "connector": "dedup-cache"}}"#,
);
deps.reload(®istry, std::slice::from_ref(&channel)).await;
let first = registry.get_by_name("dep-ch").expect("loaded");
repo.set(vec![]);
deps.connectors
.reload(&repo)
.await
.expect("connectors load");
deps.reload(®istry, std::slice::from_ref(&channel)).await;
let second = registry.get_by_name("dep-ch").expect("loaded");
assert!(
!Arc::ptr_eq(&first, &second),
"a moved connector token must re-resolve the channel's backends"
);
}
#[tokio::test]
async fn test_reload_reuses_across_a_connector_load_that_changed_nothing() {
let registry = ChannelRegistry::new();
let deps = TestDeps::new();
let repo = StubConnectorRepo::with(vec![("dedup-cache", "cache", MEMORY_CACHE)]);
deps.connectors
.reload(&repo)
.await
.expect("connectors load");
let channel = test_channel(
"resync-ch",
r#"{"deduplication": {"header": "idem", "connector": "dedup-cache"},
"validation_logic": {"!!": [{"var": "data.id"}]}}"#,
);
deps.reload(®istry, std::slice::from_ref(&channel)).await;
let first = registry.get_by_name("resync-ch").expect("loaded");
for _ in 0..3 {
deps.connectors
.reload(&repo)
.await
.expect("connectors load");
deps.reload(®istry, std::slice::from_ref(&channel)).await;
let next = registry.get_by_name("resync-ch").expect("loaded");
assert!(
Arc::ptr_eq(&first, &next),
"an epoch resync that changed no connector must not re-parse, \
re-compile and re-resolve every channel"
);
}
}
#[tokio::test]
async fn test_reload_reuses_route_table_when_the_serviceable_set_is_unchanged() {
let registry = ChannelRegistry::new();
let deps = TestDeps::new();
let alpha = rest_channel("alpha-ch", "/alpha/{id}");
deps.reload(®istry, std::slice::from_ref(&alpha)).await;
let first = registry.snapshot.load().route_table.clone();
deps.reload(®istry, std::slice::from_ref(&alpha)).await;
let second = registry.snapshot.load().route_table.clone();
assert!(
Arc::ptr_eq(&first, &second),
"an unchanged route set must keep the built table"
);
deps.reload(®istry, &[alpha, rest_channel("beta-ch", "/beta/{id}")])
.await;
let third = registry.snapshot.load().route_table.clone();
assert!(
!Arc::ptr_eq(&first, &third),
"a new route-bearing channel must rebuild the table"
);
assert!(
registry
.match_route("GET", "beta/7")
.expect("valid path")
.is_some()
);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_reader_never_sees_a_half_applied_quarantine() {
let registry = Arc::new(ChannelRegistry::new());
let deps = TestDeps::new();
let good = test_channel("flip-ch", "{}");
let broken = Channel {
version: 2,
updated_at: good.updated_at + chrono::Duration::seconds(1),
..test_channel("flip-ch", "{ not json")
};
deps.reload(®istry, std::slice::from_ref(&good)).await;
let reader_registry = registry.clone();
let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
let observations = Arc::new(std::sync::atomic::AtomicU64::new(0));
let reader_stop = stop.clone();
let reader_observations = observations.clone();
let reader = tokio::spawn(async move {
while !reader_stop.load(std::sync::atomic::Ordering::Relaxed) {
match reader_registry.require_serviceable("flip-ch") {
Ok(Some(_)) | Err(_) => {
reader_observations.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
}
Ok(None) => unreachable!(
"reader saw 'flip-ch' as neither serving nor quarantined: \
the serving map and the quarantine map disagreed"
),
}
tokio::task::yield_now().await;
}
});
while observations.load(std::sync::atomic::Ordering::Relaxed) == 0 {
tokio::task::yield_now().await;
}
let before_flips = observations.load(std::sync::atomic::Ordering::Relaxed);
for i in 0..300 {
let channels = if i % 2 == 0 {
std::slice::from_ref(&broken)
} else {
std::slice::from_ref(&good)
};
deps.reload(®istry, channels).await;
tokio::task::yield_now().await;
}
stop.store(true, std::sync::atomic::Ordering::Relaxed);
reader.await.expect("reader must not panic");
assert!(
observations.load(std::sync::atomic::Ordering::Relaxed) > before_flips,
"the reader must have read while the registry was being reloaded"
);
}
#[tokio::test]
async fn test_route_table_reuse_drops_a_newly_quarantined_channels_route() {
let registry = ChannelRegistry::new();
let deps = TestDeps::new();
let alpha = rest_channel("alpha-ch", "/alpha/{id}");
let beta = rest_channel("beta-ch", "/beta/{id}");
let channels = [alpha.clone(), beta.clone()];
deps.reload(®istry, &channels).await;
let first = registry.snapshot.load().route_table.clone();
assert!(
registry
.match_route("GET", "alpha/7")
.expect("valid path")
.is_some()
);
deps.reload_with_issues(
®istry,
&channels,
vec![ChannelLoadIssue {
channel: "alpha-ch".to_string(),
reason: "workflow 'wf_alpha' not found".to_string(),
}],
)
.await;
let second = registry.snapshot.load().route_table.clone();
assert!(
!Arc::ptr_eq(&first, &second),
"a channel leaving the serviceable set must rebuild the table, \
however unchanged its row is"
);
assert!(
registry
.match_route("GET", "alpha/7")
.expect("valid path")
.is_none(),
"a quarantined channel's route must not outlive its runtime config"
);
assert!(registry.get_by_name("alpha-ch").is_none());
let matched = registry
.match_route("GET", "beta/7")
.expect("valid path")
.expect("beta still routes");
assert!(registry.get_by_name(&matched.channel_name).is_some());
deps.reload(®istry, &channels).await;
assert!(
registry
.match_route("GET", "alpha/7")
.expect("valid path")
.is_some()
);
assert!(registry.get_by_name("alpha-ch").is_some());
}
#[tokio::test]
async fn test_engine_quarantine_and_config_failure_on_one_channel() {
let _log_guard = tracing::subscriber::set_default(
tracing_subscriber::fmt()
.with_max_level(tracing::Level::ERROR)
.with_test_writer()
.finish(),
);
let registry = ChannelRegistry::new();
let deps = TestDeps::new();
deps.reload_with_issues(
®istry,
&[test_channel("dup-ch", "{ nope")],
vec![ChannelLoadIssue {
channel: "dup-ch".to_string(),
reason: "workflow missing".to_string(),
}],
)
.await;
let quarantined = registry.quarantined();
assert_eq!(
quarantined.len(),
1,
"one broken channel is one quarantine entry, however many ways it \
is broken"
);
assert_eq!(quarantined[0].channel, "dup-ch");
assert!(
quarantined[0].reason.contains("config_json does not parse"),
"the channel's own config failure is the more specific reason, \
got: {}",
quarantined[0].reason
);
assert!(registry.get_by_name("dup-ch").is_none());
assert!(registry.snapshot.load().by_name.is_empty());
}
#[tokio::test]
async fn test_engine_quarantined_channel_does_not_serve() {
let registry = ChannelRegistry::new();
let deps = TestDeps::new();
deps.reload_with_issues(
®istry,
&[test_channel("ok-ch", "{}"), test_channel("orphan-ch", "{}")],
vec![ChannelLoadIssue {
channel: "orphan-ch".to_string(),
reason: "workflow 'wf_gone' not found".to_string(),
}],
)
.await;
assert!(registry.get_by_name("ok-ch").is_some());
assert!(
registry.get_by_name("orphan-ch").is_none(),
"a channel with no workflow behind it must not be served"
);
assert_eq!(
registry.quarantine_reason("orphan-ch").as_deref(),
Some("workflow 'wf_gone' not found")
);
assert!(registry.require_serviceable("orphan-ch").is_err());
}
#[tokio::test]
async fn test_quarantine_set_is_readable_from_the_registry_alone() {
let registry = ChannelRegistry::new();
let deps = TestDeps::new();
deps.reload(
®istry,
&[
test_channel("ok-ch", "{}"),
test_channel("bad-ch", "{ nope"),
],
)
.await;
let quarantined = registry.quarantined();
assert_eq!(quarantined.len(), 1);
assert_eq!(quarantined[0].channel, "bad-ch");
assert_eq!(
registry.quarantine_reason("bad-ch"),
Some(quarantined[0].reason.clone()),
"the list and the single lookup must be the same map"
);
assert!(registry.quarantine_reason("ok-ch").is_none());
deps.reload(®istry, &[test_channel("ok-ch", "{}")]).await;
assert!(registry.quarantined().is_empty());
}
}