#![deny(missing_docs)]
use crate::conductor::process::ERROR_CODE;
use crate::config::conductor::paths::DataRootPath;
use holochain_types::prelude::DbSyncStrategy;
#[cfg(all(feature = "schema", feature = "kitsune2_transport_tx5"))]
use kitsune2_transport_tx5::WebRtcConfig;
use schemars::JsonSchema;
#[cfg(feature = "schema")]
use schemars::Schema;
use serde::de::DeserializeOwned;
use serde::Deserialize;
use serde::Serialize;
use std::path::Path;
mod admin_interface_config;
#[allow(missing_docs)]
mod error;
mod keystore_config;
pub mod paths;
pub mod process;
pub use super::*;
pub use error::*;
pub use keystore_config::KeystoreConfig;
#[derive(Clone, Deserialize, Serialize, Debug, PartialEq, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ConductorConfig {
#[serde(default)]
pub tracing_override: Option<String>,
pub data_root_path: Option<DataRootPath>,
#[serde(default)]
pub keystore: KeystoreConfig,
pub admin_interfaces: Option<Vec<AdminInterfaceConfig>>,
#[serde(default)]
pub network: NetworkConfig,
#[serde(default)]
pub db_sync_strategy: DbSyncStrategy,
#[serde(default = "default_db_max_readers")]
pub db_max_readers: u16,
#[serde(default = "default_incoming_request_concurrency_limit")]
pub incoming_request_concurrency_limit: u16,
#[serde(default)]
pub tuning_params: Option<ConductorTuningParams>,
pub tracing_scope: Option<String>,
}
fn default_db_max_readers() -> u16 {
calculate_default_db_max_readers(num_cpus::get())
}
fn calculate_default_db_max_readers(num_cpus_count: usize) -> u16 {
let num_cpus_count = u16::try_from(num_cpus_count).unwrap_or(u16::MAX);
let cpus_x2 = num_cpus_count.saturating_mul(2);
std::cmp::max(cpus_x2, 8)
}
fn default_incoming_request_concurrency_limit() -> u16 {
std::cmp::max(default_db_max_readers() - 3, 1)
}
impl Default for ConductorConfig {
fn default() -> Self {
Self {
tracing_override: None,
data_root_path: None,
keystore: KeystoreConfig::default(),
admin_interfaces: None,
network: NetworkConfig::default(),
db_sync_strategy: DbSyncStrategy::default(),
db_max_readers: default_db_max_readers(),
incoming_request_concurrency_limit: default_incoming_request_concurrency_limit(),
tuning_params: None,
tracing_scope: None,
}
}
}
fn config_from_yaml<T>(yaml: &str) -> ConductorConfigResult<T>
where
T: DeserializeOwned,
{
serde_yaml::from_str(yaml).map_err(ConductorConfigError::SerializationError)
}
impl ConductorConfig {
pub fn load_yaml(path: &Path) -> ConductorConfigResult<ConductorConfig> {
let config_yaml = std::fs::read_to_string(path).map_err(|err| match err {
e @ std::io::Error { .. } if e.kind() == std::io::ErrorKind::NotFound => {
ConductorConfigError::ConfigMissing(path.into())
}
_ => err.into(),
})?;
config_from_yaml(&config_yaml)
}
pub fn tracing_scope(&self) -> Option<String> {
self.tracing_scope.clone()
}
pub fn data_root_path_or_die(&self) -> DataRootPath {
match &self.data_root_path {
Some(path) => path.clone(),
None => {
println!(
"
The conductor config does not contain a data_root_path. Please check and fix the
config file. Details:
Missing field `data_root_path`",
);
std::process::exit(ERROR_CODE);
}
}
}
pub fn reports_path(&self) -> std::path::PathBuf {
crate::conductor::paths::ReportsRootPath::try_from(self.data_root_path_or_die())
.expect("can get reports path")
.0
}
pub fn conductor_tuning_params(&self) -> ConductorTuningParams {
self.tuning_params.clone().unwrap_or_default()
}
}
#[derive(Clone, Default, Deserialize, Serialize, Debug, PartialEq, JsonSchema)]
#[serde(
rename_all = "snake_case",
rename_all_fields = "snake_case",
deny_unknown_fields
)]
pub enum ReportConfig {
#[default]
None,
JsonLines {
days_retained: u32,
fetched_op_interval_s: u32,
},
}
#[derive(Clone, Deserialize, Serialize, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case", deny_unknown_fields)]
pub struct NetworkConfig {
#[serde(default)]
pub base64_auth_material_bootstrap: Option<String>,
#[serde(default)]
pub base64_auth_material_relay: Option<String>,
#[schemars(schema_with = "holochain_util::jsonschema::url2_schema")]
pub bootstrap_url: url2::Url2,
#[schemars(schema_with = "holochain_util::jsonschema::url2_schema")]
pub signal_url: url2::Url2,
#[schemars(schema_with = "holochain_util::jsonschema::url2_schema")]
pub relay_url: url2::Url2,
#[serde(default = "default_request_timeout_s")]
pub request_timeout_s: u64,
#[cfg_attr(
all(feature = "schema", feature = "kitsune2_transport_tx5"),
schemars(schema_with = "webrtc_config_schema")
)]
pub webrtc_config: Option<serde_json::Value>,
#[serde(default = "default_target_arc_factor")]
pub target_arc_factor: u32,
#[serde(default)]
pub report: ReportConfig,
#[cfg_attr(feature = "schema", schemars(schema_with = "kitsune2_config_schema"))]
pub advanced: Option<serde_json::Value>,
#[cfg(feature = "test-utils")]
#[serde(default)]
pub disable_bootstrap: bool,
#[cfg(feature = "test-utils")]
#[serde(default)]
pub disable_publish: bool,
#[cfg(feature = "test-utils")]
#[serde(default)]
pub disable_gossip: bool,
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
base64_auth_material_bootstrap: None,
base64_auth_material_relay: None,
bootstrap_url: url2::Url2::parse("https://dev-test-bootstrap2.holochain.org"),
signal_url: url2::Url2::parse("wss://dev-test-bootstrap2.holochain.org"),
relay_url: url2::Url2::parse("https://use1-1.relay.n0.iroh-canary.iroh.link./"),
request_timeout_s: default_request_timeout_s(),
webrtc_config: None,
target_arc_factor: default_target_arc_factor(),
report: Default::default(),
advanced: None,
#[cfg(feature = "test-utils")]
disable_bootstrap: false,
#[cfg(feature = "test-utils")]
disable_publish: false,
#[cfg(feature = "test-utils")]
disable_gossip: false,
}
}
}
const fn default_request_timeout_s() -> u64 {
60
}
const fn default_target_arc_factor() -> u32 {
1
}
impl std::fmt::Debug for NetworkConfig {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut s = f.debug_struct("NetworkConfig");
s.field(
"base64_auth_material_bootstrap",
&self
.base64_auth_material_bootstrap
.as_ref()
.map(|_| "<redacted>"),
);
s.field(
"base64_auth_material_relay",
&self
.base64_auth_material_relay
.as_ref()
.map(|_| "<redacted>"),
);
s.field("bootstrap_url", &self.bootstrap_url);
s.field("signal_url", &self.signal_url);
s.field("relay_url", &self.relay_url);
s.field("request_timeout_s", &self.request_timeout_s);
s.field("webrtc_config", &self.webrtc_config);
s.field("target_arc_factor", &self.target_arc_factor);
s.field("report", &self.report);
s.field("advanced", &self.advanced);
#[cfg(feature = "test-utils")]
{
s.field("disable_bootstrap", &self.disable_bootstrap);
s.field("disable_publish", &self.disable_publish);
s.field("disable_gossip", &self.disable_gossip);
}
s.finish()
}
}
impl NetworkConfig {
#[cfg(feature = "test-utils")]
pub fn with_gossip_initiate_interval_ms(mut self, initiate_interval_ms: u32) -> Self {
self.insert_into_config(|module_config| {
Self::insert_module_config(
module_config,
"k2Gossip",
"initiateIntervalMs",
serde_json::Value::Number(serde_json::Number::from(initiate_interval_ms)),
)?;
Ok(())
})
.unwrap();
self
}
#[cfg(feature = "test-utils")]
pub fn with_gossip_initiate_jitter_ms(mut self, initiate_jitter_ms: u32) -> Self {
self.insert_into_config(|module_config| {
Self::insert_module_config(
module_config,
"k2Gossip",
"initiateJitterMs",
serde_json::Value::Number(serde_json::Number::from(initiate_jitter_ms)),
)?;
Ok(())
})
.unwrap();
self
}
#[cfg(feature = "test-utils")]
pub fn with_gossip_min_initiate_interval_ms(mut self, min_initiate_interval_ms: u32) -> Self {
self.insert_into_config(|module_config| {
Self::insert_module_config(
module_config,
"k2Gossip",
"minInitiateIntervalMs",
serde_json::Value::Number(serde_json::Number::from(min_initiate_interval_ms)),
)?;
Ok(())
})
.unwrap();
self
}
#[cfg(feature = "test-utils")]
pub fn with_gossip_round_timeout_ms(mut self, round_timeout_ms: u32) -> Self {
self.insert_into_config(|module_config| {
Self::insert_module_config(
module_config,
"k2Gossip",
"roundTimeoutMs",
serde_json::Value::Number(serde_json::Number::from(round_timeout_ms)),
)?;
Ok(())
})
.unwrap();
self
}
pub fn to_k2_config(&self) -> ConductorConfigResult<serde_json::Value> {
let mut working = self
.advanced
.clone()
.unwrap_or_else(|| serde_json::Value::Object(Default::default()));
if let Some(module_config) = working.as_object_mut() {
Self::insert_module_config(
module_config,
"coreBootstrap",
"serverUrl",
serde_json::Value::String(self.bootstrap_url.as_str().into()),
)?;
Self::insert_module_config(
module_config,
"tx5Transport",
"serverUrl",
serde_json::Value::String(self.signal_url.as_str().into()),
)?;
let timeout_s: serde_json::Number = (self.request_timeout_s / 2).into();
Self::insert_module_config(
module_config,
"tx5Transport",
"timeoutS",
serde_json::Value::Number(timeout_s),
)?;
let webrtc_connect_timeout_s: serde_json::Number =
((self.request_timeout_s * 3) / 8).into();
Self::insert_module_config(
module_config,
"tx5Transport",
"webrtcConnectTimeoutS",
serde_json::Value::Number(webrtc_connect_timeout_s),
)?;
if let Some(webrtc_config) = &self.webrtc_config {
Self::insert_module_config(
module_config,
"tx5Transport",
"webrtcConfig",
webrtc_config.clone(),
)?;
}
if tracing::enabled!(target: "NETAUDIT", tracing::Level::WARN) {
tracing::info!(
"The NETAUDIT target is enabled, turning on network backend tracing"
);
Self::insert_module_config(
module_config,
"tx5Transport",
"tracingEnabled",
serde_json::Value::Bool(true),
)?;
}
Self::insert_module_config(
module_config,
"irohTransport",
"relayUrl",
serde_json::Value::String(self.relay_url.as_str().into()),
)?;
} else {
return Err(ConductorConfigError::InvalidNetworkConfig(
"advanced field must be an object".to_string(),
));
}
Ok(working)
}
#[cfg(feature = "test-utils")]
fn insert_into_config(
&mut self,
mutator: impl Fn(&mut serde_json::Map<String, serde_json::Value>) -> ConductorConfigResult<()>,
) -> ConductorConfigResult<()> {
if self.advanced.is_none() {
self.advanced = Some(serde_json::Value::Object(Default::default()));
}
if let Some(module_config) = self
.advanced
.as_mut()
.expect("Just checked")
.as_object_mut()
{
mutator(module_config)?;
}
Ok(())
}
fn insert_module_config(
module_config: &mut serde_json::Map<String, serde_json::Value>,
module: &str,
key: &str,
value: serde_json::Value,
) -> ConductorConfigResult<()> {
if let Some(module_config) = module_config.get_mut(module) {
if let Some(module_config) = module_config.as_object_mut() {
if module_config.contains_key(key) {
tracing::warn!("The {} module configuration contains a '{}' field, which is being overwritten", module, key);
}
module_config.insert(key.into(), value);
} else {
return Err(ConductorConfigError::InvalidNetworkConfig(format!(
"advanced.{module} field must be an object"
)));
}
} else {
module_config.insert(
module.into(),
serde_json::json!({
key: value,
}),
);
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ConductorTuningParams {
pub sys_validation_retry_delay: Option<std::time::Duration>,
pub countersigning_resolution_retry_delay: Option<std::time::Duration>,
pub countersigning_resolution_retry_limit: Option<usize>,
pub min_publish_interval: Option<std::time::Duration>,
pub publish_trigger_interval: Option<std::time::Duration>,
pub disable_self_validation: bool,
#[cfg(feature = "test-utils")]
pub disable_warrant_issuance: bool,
}
impl ConductorTuningParams {
pub fn new() -> Self {
Self {
sys_validation_retry_delay: None,
countersigning_resolution_retry_delay: None,
countersigning_resolution_retry_limit: None,
min_publish_interval: None,
publish_trigger_interval: None,
disable_self_validation: false,
#[cfg(feature = "test-utils")]
disable_warrant_issuance: false,
}
}
pub fn sys_validation_retry_delay(&self) -> std::time::Duration {
self.sys_validation_retry_delay
.unwrap_or_else(|| std::time::Duration::from_secs(10))
}
pub fn countersigning_resolution_retry_delay(&self) -> std::time::Duration {
self.countersigning_resolution_retry_delay
.unwrap_or_else(|| std::time::Duration::from_secs(60 * 5))
}
pub fn min_publish_interval(&self) -> std::time::Duration {
self.min_publish_interval
.unwrap_or_else(|| std::time::Duration::from_secs(60 * 5))
}
}
impl Default for ConductorTuningParams {
fn default() -> Self {
let empty = Self::new();
Self {
sys_validation_retry_delay: Some(empty.sys_validation_retry_delay()),
countersigning_resolution_retry_delay: Some(
empty.countersigning_resolution_retry_delay(),
),
countersigning_resolution_retry_limit: None,
publish_trigger_interval: None,
min_publish_interval: None,
disable_self_validation: false,
#[cfg(feature = "test-utils")]
disable_warrant_issuance: false,
}
}
}
#[cfg(all(feature = "schema", feature = "kitsune2_transport_tx5"))]
fn webrtc_config_schema(_: &mut schemars::SchemaGenerator) -> Schema {
let schema = schemars::schema_for!(Option<WebRtcConfig>);
schema
}
#[cfg(feature = "schema")]
fn kitsune2_config_schema(generator: &mut schemars::SchemaGenerator) -> Schema {
#[allow(dead_code)]
#[derive(JsonSchema)]
#[schemars(rename_all = "camelCase")]
struct K2Config {
#[serde(flatten)]
core_bootstrap: Option<kitsune2_core::factories::CoreBootstrapModConfig>,
#[serde(flatten)]
core_fetch: Option<kitsune2_core::factories::CoreFetchModConfig>,
#[serde(flatten)]
core_publish: Option<kitsune2_core::factories::CorePublishModConfig>,
#[serde(flatten)]
core_space: Option<kitsune2_core::factories::CoreSpaceModConfig>,
#[serde(flatten)]
mem_peer_store: Option<kitsune2_core::factories::MemPeerStoreModConfig>,
#[serde(flatten)]
k2_gossip: Option<kitsune2_gossip::K2GossipModConfig>,
#[cfg(feature = "kitsune2_transport_tx5")]
#[serde(flatten)]
tx5_transport: Option<kitsune2_transport_tx5::Tx5TransportModConfig>,
#[serde(flatten)]
iroh_transport: Option<kitsune2_transport_iroh::IrohTransportModConfig>,
}
let schema = schemars::schema_for!(Option<K2Config>);
for (k, v) in schema
.get("$defs")
.and_then(|d| d.as_object())
.expect("No definitions")
{
if generator
.definitions_mut()
.insert(k.clone(), v.clone())
.is_some()
{
tracing::warn!("Conflicting definition for {k} in K2Config");
}
}
schema
}
#[cfg(test)]
mod tests {
use super::*;
use holochain_types::websocket::AllowedOrigins;
use matches::assert_matches;
use std::path::Path;
use std::path::PathBuf;
#[test]
fn config_load_yaml() {
let bad_path = Path::new("fake");
let result = ConductorConfig::load_yaml(bad_path);
assert_eq!(
"Err(ConfigMissing(\"fake\"))".to_string(),
format!("{result:?}")
);
}
#[test]
fn config_bad_yaml() {
let result: ConductorConfigResult<ConductorConfig> = config_from_yaml("this isn't yaml");
assert_matches!(result, Err(ConductorConfigError::SerializationError(_)));
}
#[test]
fn config_complete_minimal_config() {
let yaml = r#"---
data_root_path: /path/to/env
keystore:
type: danger_test_keystore
"#;
let result: ConductorConfig = config_from_yaml(yaml).unwrap();
pretty_assertions::assert_eq!(
result,
ConductorConfig {
tracing_override: None,
data_root_path: Some(PathBuf::from("/path/to/env").into()),
network: NetworkConfig::default(),
keystore: KeystoreConfig::DangerTestKeystore,
admin_interfaces: None,
db_sync_strategy: DbSyncStrategy::default(),
db_max_readers: default_db_max_readers(),
tuning_params: None,
tracing_scope: None,
incoming_request_concurrency_limit: default_incoming_request_concurrency_limit(),
}
);
}
#[test]
fn config_rejects_unrecognized_fields() {
let yaml = r#"---
data_root_path: /path/to/env
keystore:
type: danger_test_keystore
unknown_field: some_value
"#;
let result: ConductorConfigResult<ConductorConfig> = config_from_yaml(yaml);
assert_matches!(result, Err(ConductorConfigError::SerializationError(_)));
if let Err(ConductorConfigError::SerializationError(e)) = result {
let error_msg = e.to_string();
assert!(
error_msg.contains("unknown_field"),
"Error message should mention the unknown field name: {}",
error_msg
);
}
let yaml = r#"---
data_root_path: /path/to/env
keystore:
type: lair_server
unknown_keystore_field: true
"#;
let result: ConductorConfigResult<ConductorConfig> = config_from_yaml(yaml);
assert_matches!(result, Err(ConductorConfigError::SerializationError(_)));
if let Err(ConductorConfigError::SerializationError(e)) = result {
let error_msg = e.to_string();
assert!(
error_msg.contains("unknown_keystore_field"),
"Error message should mention the unknown field name: {}",
error_msg
);
}
}
#[test]
fn admin_interface_rejects_unrecognized_fields() {
let yaml = r#"---
data_root_path: /path/to/env
keystore:
type: danger_test_keystore
admin_interfaces:
- driver:
type: websocket
port: 12345
allowed_origins: "*"
unknown_driver_field: test
"#;
let result: ConductorConfigResult<ConductorConfig> = config_from_yaml(yaml);
assert_matches!(result, Err(ConductorConfigError::SerializationError(_)));
if let Err(ConductorConfigError::SerializationError(e)) = result {
let error_msg = e.to_string();
assert!(
error_msg.contains("unknown_driver_field"),
"Error message should mention the unknown field name: {}",
error_msg
);
}
let yaml = r#"---
data_root_path: /path/to/env
keystore:
type: danger_test_keystore
admin_interfaces:
- driver:
type: websocket
port: 12345
allowed_origins: "*"
unknown_admin_field: value
"#;
let result: ConductorConfigResult<ConductorConfig> = config_from_yaml(yaml);
assert_matches!(result, Err(ConductorConfigError::SerializationError(_)));
if let Err(ConductorConfigError::SerializationError(e)) = result {
let error_msg = e.to_string();
assert!(
error_msg.contains("unknown_admin_field"),
"Error message should mention the unknown field name: {}",
error_msg
);
}
}
#[test]
fn empty_config_uses_default_values() {
let result: ConductorConfig = config_from_yaml("").unwrap();
pretty_assertions::assert_eq!(result, ConductorConfig::default());
}
#[test]
#[allow(clippy::field_reassign_with_default)]
fn config_complete_config() {
holochain_trace::test_run();
let yaml = r#"---
data_root_path: /path/to/env
keystore:
type: lair_server_in_proc
admin_interfaces:
- driver:
type: websocket
port: 1234
allowed_origins: "*"
network:
bootstrap_url: https://test-boot.tld
signal_url: wss://test-sig.tld
relay_url: https://relay.tld
webrtc_config: {
"iceServers": [
{ "urls": ["stun:test-stun.tld:443"] },
]
}
request_timeout_s: 70
advanced: {
"my": {
"totally": {
"random": {
"advanced": {
"config": true
}
}
}
}
}
db_sync_strategy: Fast
db_max_readers: 100
incoming_request_concurrency_limit: 100
"#;
let result: ConductorConfigResult<ConductorConfig> = config_from_yaml(yaml);
let mut network_config = NetworkConfig::default();
network_config.bootstrap_url = url2::url2!("https://test-boot.tld");
network_config.signal_url = url2::url2!("wss://test-sig.tld");
network_config.relay_url = url2::url2!("https://relay.tld");
network_config.request_timeout_s = 70;
network_config.webrtc_config = Some(serde_json::json!({
"iceServers": [
{ "urls": ["stun:test-stun.tld:443"] },
]
}));
network_config.advanced = Some(serde_json::json!({
"my": {
"totally": {
"random": {
"advanced": {
"config": true,
}
}
}
}
}));
pretty_assertions::assert_eq!(
result.unwrap(),
ConductorConfig {
tracing_override: None,
data_root_path: Some(PathBuf::from("/path/to/env").into()),
keystore: KeystoreConfig::LairServerInProc { lair_root: None },
admin_interfaces: Some(vec![AdminInterfaceConfig {
driver: InterfaceDriver::Websocket {
port: 1234,
danger_bind_addr: None,
allowed_origins: AllowedOrigins::Any
}
}]),
network: network_config,
db_sync_strategy: DbSyncStrategy::Fast,
db_max_readers: 100,
incoming_request_concurrency_limit: 100,
tuning_params: None,
tracing_scope: None,
}
);
}
#[test]
fn config_new_lair_keystore() {
let yaml = r#"---
data_root_path: /path/to/env
keystore:
type: lair_server
connection_url: "unix:///var/run/lair-keystore/socket?k=EcRDnP3xDIZ9Rk_1E-egPE0mGZi5CcszeRxVkb2QXXQ"
"#;
let result: ConductorConfigResult<ConductorConfig> = config_from_yaml(yaml);
pretty_assertions::assert_eq!(
result.unwrap(),
ConductorConfig {
tracing_override: None,
data_root_path: Some(PathBuf::from("/path/to/env").into()),
network: NetworkConfig::default(),
keystore: KeystoreConfig::LairServer {
connection_url: url2::url2!("unix:///var/run/lair-keystore/socket?k=EcRDnP3xDIZ9Rk_1E-egPE0mGZi5CcszeRxVkb2QXXQ"),
},
admin_interfaces: None,
db_sync_strategy: DbSyncStrategy::Resilient,
db_max_readers: default_db_max_readers(),
tuning_params: None,
tracing_scope: None,
incoming_request_concurrency_limit: default_incoming_request_concurrency_limit(),
}
);
}
#[test]
fn default_network_config_accepted_by_k2() {
let network_config = NetworkConfig::default();
let k2_config = network_config.to_k2_config().unwrap();
let builder = kitsune2_core::default_test_builder()
.with_default_config()
.unwrap();
builder.config.set_module_config(&k2_config).unwrap();
builder.validate_config().unwrap();
}
#[test]
fn network_config_preserves_advanced_overrides() {
let network_config = NetworkConfig {
advanced: Some(serde_json::json!({
"coreBootstrap": {
"backoffMinMs": "3500",
},
"tx5Transport": {
"signalAllowPlainText": "true"
},
"irohTransport": {
"relayAllowPlainText": "true"
},
"coreSpace": {
"reSignFreqMs": "1000",
}
})),
..Default::default()
};
let k2_config = network_config.to_k2_config().unwrap();
let builder = kitsune2_core::default_test_builder()
.with_default_config()
.unwrap();
builder.config.set_module_config(&k2_config).unwrap();
builder.validate_config().unwrap();
assert_eq!(
k2_config,
serde_json::json!({
"coreBootstrap": {
"serverUrl": "https://dev-test-bootstrap2.holochain.org/",
"backoffMinMs": "3500",
},
"tx5Transport": {
"serverUrl": "wss://dev-test-bootstrap2.holochain.org/",
"timeoutS": 30,
"webrtcConnectTimeoutS": 22,
"signalAllowPlainText": "true"
},
"irohTransport": {
"relayUrl": "https://use1-1.relay.n0.iroh-canary.iroh.link./",
"relayAllowPlainText": "true"
},
"coreSpace": {
"reSignFreqMs": "1000",
}
})
);
}
#[test]
fn network_config_overrides_conflicting_advanced_fields() {
let network_config = NetworkConfig {
advanced: Some(serde_json::json!({
"coreBootstrap": {
"serverUrl": "https://something-else.net",
},
"tx5Transport": {
"serverUrl": "wss://sbd.nowhere.net",
"timeoutS": 10,
"webrtcConnectTimeoutS": 10
},
"irohTransport": {
"relayUrl": "https://iroh.nowhere.net",
},
})),
..Default::default()
};
let k2_config = network_config.to_k2_config().unwrap();
let builder = kitsune2_core::default_test_builder()
.with_default_config()
.unwrap();
builder.config.set_module_config(&k2_config).unwrap();
builder.validate_config().unwrap();
assert_eq!(
k2_config,
serde_json::json!({
"coreBootstrap": {
"serverUrl": "https://dev-test-bootstrap2.holochain.org/",
},
"tx5Transport": {
"serverUrl": "wss://dev-test-bootstrap2.holochain.org/",
"timeoutS": 30,
"webrtcConnectTimeoutS": 22
},
"irohTransport": {
"relayUrl": "https://use1-1.relay.n0.iroh-canary.iroh.link./",
},
})
);
}
#[test]
fn tune_kitsune_params_for_testing() {
let network_config = NetworkConfig::default()
.with_gossip_round_timeout_ms(100)
.with_gossip_initiate_interval_ms(200)
.with_gossip_initiate_jitter_ms(50)
.with_gossip_min_initiate_interval_ms(300);
let k2_config = network_config.to_k2_config().unwrap();
let builder = kitsune2_core::default_test_builder()
.with_default_config()
.unwrap();
builder.config.set_module_config(&k2_config).unwrap();
builder.validate_config().unwrap();
assert_eq!(
k2_config,
serde_json::json!({
"coreBootstrap": {
"serverUrl": "https://dev-test-bootstrap2.holochain.org/",
},
"tx5Transport": {
"serverUrl": "wss://dev-test-bootstrap2.holochain.org/",
"timeoutS": 30,
"webrtcConnectTimeoutS": 22
},
"irohTransport": {
"relayUrl": "https://use1-1.relay.n0.iroh-canary.iroh.link./",
},
"k2Gossip": {
"roundTimeoutMs": 100,
"initiateIntervalMs": 200,
"initiateJitterMs": 50,
"minInitiateIntervalMs": 300,
}
})
);
}
#[test]
fn default_db_max_readers_calculation() {
let config = ConductorConfig::default();
assert_eq!(config.db_max_readers, default_db_max_readers());
let cpu_count = 4;
assert_eq!(calculate_default_db_max_readers(cpu_count), 8);
let cpu_count = 3;
assert_eq!(calculate_default_db_max_readers(cpu_count), 8);
let cpu_count = 5;
assert_eq!(calculate_default_db_max_readers(cpu_count), 10);
let cpu_count = (u16::MAX as usize / 2) + 1;
assert_eq!(calculate_default_db_max_readers(cpu_count), u16::MAX);
let cpu_count = u32::MAX as usize;
assert_eq!(calculate_default_db_max_readers(cpu_count), u16::MAX);
}
#[cfg(feature = "schema")]
#[test]
fn schema_generation() {
let schema = schemars::schema_for!(ConductorConfig);
let schema_json = serde_json::to_value(&schema).unwrap();
let default_config = ConductorConfig::default();
let default_config_json = serde_json::to_value(&default_config).unwrap();
jsonschema::validate(&schema_json, &default_config_json).unwrap();
}
}