tangle


git clone https://radroots.dev/git/tangle.git
Log | Files | Refs | README | LICENSE

commit 46e2600522f1a02262735ed7d00ccc46afb0befb
parent 5e9c443e18185cee553a333ccbda44f3ec253e78
Author: triesap <tyson@radroots.org>
Date:   Sun, 14 Jun 2026 01:41:35 -0700

runtime: extract config module

- move base relay runtime config parsing into config
- re-export config types through the existing base relay surface
- keep runtime behavior unchanged for the mechanical split
- verify tangle_runtime and workspace checks stay green

Diffstat:
Mcrates/tangle_runtime/src/base_relay.rs | 278+------------------------------------------------------------------------------
Acrates/tangle_runtime/src/config.rs | 284+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mcrates/tangle_runtime/src/lib.rs | 7+++----
3 files changed, 290 insertions(+), 279 deletions(-)

diff --git a/crates/tangle_runtime/src/base_relay.rs b/crates/tangle_runtime/src/base_relay.rs @@ -7,7 +7,7 @@ use axum::{ use core::fmt; use http::{HeaderMap, HeaderValue, StatusCode, header}; use serde::{Deserialize, Serialize}; -use std::{collections::BTreeMap, collections::BTreeSet, net::SocketAddr, path::PathBuf, str}; +use std::{collections::BTreeMap, collections::BTreeSet, str}; use tangle_crypto::{RelaySigner, verify_event_signature}; use tangle_groups::{ GroupAuthContext, GroupAuthority, GroupError, GroupErrorKind, GroupEventClass, @@ -27,7 +27,7 @@ use tangle_protocol::{ }; use tangle_store_pocket::{ PocketEvent, PocketEventId, PocketOwnedEvent, PocketOwnedFilter, PocketStoreConfig, - PocketStoreHandle, PocketSyncPolicy, TANGLE_GROUP_CHECKPOINT_TABLE, TANGLE_GROUP_OUTBOX_TABLE, + PocketStoreHandle, TANGLE_GROUP_CHECKPOINT_TABLE, TANGLE_GROUP_OUTBOX_TABLE, TANGLE_GROUP_PROJECTION_TABLE, parse_pocket_event_json, parse_pocket_filter_json, }; @@ -136,249 +136,6 @@ pub struct BaseRelayInfoLimitationDocument { pub restricted_writes: bool, } -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct BaseRelayRuntimeConfig { - listen_addr: SocketAddr, - relay_url: String, - pocket: PocketStoreConfig, - groups: GroupRuntimeConfig, - auth_ttl_seconds: u64, - max_pending_events: usize, - tracing: BaseRelayTracingConfig, -} - -impl BaseRelayRuntimeConfig { - pub fn listen_addr(&self) -> SocketAddr { - self.listen_addr - } - - pub fn relay_url(&self) -> &str { - &self.relay_url - } - - pub fn pocket_config(&self) -> &PocketStoreConfig { - &self.pocket - } - - pub fn groups(&self) -> &GroupRuntimeConfig { - &self.groups - } - - pub fn auth_ttl_seconds(&self) -> u64 { - self.auth_ttl_seconds - } - - pub fn max_pending_events(&self) -> usize { - self.max_pending_events - } - - pub fn tracing(&self) -> &BaseRelayTracingConfig { - &self.tracing - } - - pub fn open_relay(&self) -> Result<BaseRelay, BaseRelayError> { - BaseRelay::open_with_groups(&self.pocket, self.max_pending_events, &self.groups) - } - - pub fn auth_state(&self) -> Result<BaseAuthState, BaseRelayError> { - BaseAuthState::new(self.relay_url.clone(), self.auth_ttl_seconds) - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum BaseRelayTracingFormat { - Compact, - Json, -} - -impl BaseRelayTracingFormat { - pub fn as_str(self) -> &'static str { - match self { - Self::Compact => "compact", - Self::Json => "json", - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct BaseRelayTracingConfig { - enabled: bool, - filter: String, - format: BaseRelayTracingFormat, -} - -impl BaseRelayTracingConfig { - pub fn new( - enabled: bool, - filter: impl Into<String>, - format: BaseRelayTracingFormat, - ) -> Result<Self, BaseRelayError> { - let filter = filter.into(); - if filter.trim().is_empty() { - return Err(BaseRelayError::invalid( - "observability.tracing.filter must not be empty", - )); - } - Ok(Self { - enabled, - filter: filter.trim().to_owned(), - format, - }) - } - - pub fn enabled(&self) -> bool { - self.enabled - } - - pub fn filter(&self) -> &str { - &self.filter - } - - pub fn format(&self) -> BaseRelayTracingFormat { - self.format - } -} - -impl Default for BaseRelayTracingConfig { - fn default() -> Self { - Self::new( - true, - "info,tangle=info,tangle_runtime=info,tangle_groups=info,tangle_store_pocket=info", - BaseRelayTracingFormat::Json, - ) - .expect("default tracing config is valid") - } -} - -#[derive(Debug, Deserialize)] -struct BaseRelayRuntimeConfigDocument { - server: BaseRelayServerConfigDocument, - pocket: BaseRelayPocketConfigDocument, - groups: serde_json::Value, - auth: BaseRelayAuthConfigDocument, - limits: BaseRelayRuntimeLimitsDocument, - #[serde(default)] - observability: BaseRelayObservabilityConfigDocument, -} - -#[derive(Debug, Deserialize)] -struct BaseRelayServerConfigDocument { - listen_addr: String, - relay_url: String, -} - -#[derive(Debug, Deserialize)] -struct BaseRelayPocketConfigDocument { - data_directory: String, - map_size_bytes: u64, - reader_slots: u32, - sync_policy: BaseRelayPocketSyncPolicyDocument, -} - -#[derive(Debug, Clone, Copy, Deserialize)] -#[serde(rename_all = "snake_case")] -enum BaseRelayPocketSyncPolicyDocument { - FlushOnWrite, - FlushOnShutdown, -} - -#[derive(Debug, Deserialize)] -struct BaseRelayAuthConfigDocument { - challenge_ttl_seconds: u64, -} - -#[derive(Debug, Deserialize)] -struct BaseRelayRuntimeLimitsDocument { - max_pending_events: usize, -} - -#[derive(Debug, Default, Deserialize)] -struct BaseRelayObservabilityConfigDocument { - #[serde(default)] - tracing: BaseRelayTracingConfigDocument, -} - -#[derive(Debug, Default, Deserialize)] -struct BaseRelayTracingConfigDocument { - enabled: Option<bool>, - filter: Option<String>, - format: Option<BaseRelayTracingFormatDocument>, -} - -#[derive(Debug, Clone, Copy, Deserialize)] -#[serde(rename_all = "snake_case")] -enum BaseRelayTracingFormatDocument { - Compact, - Json, -} - -pub fn parse_base_relay_runtime_config_json( - raw: &str, -) -> Result<BaseRelayRuntimeConfig, BaseRelayError> { - let document = - serde_json::from_str::<BaseRelayRuntimeConfigDocument>(raw).map_err(|error| { - BaseRelayError::invalid(format!( - "base relay runtime config JSON is invalid: {error}" - )) - })?; - let listen_addr = document - .server - .listen_addr - .parse::<SocketAddr>() - .map_err(|error| { - BaseRelayError::invalid(format!("server.listen_addr is invalid: {error}")) - })?; - let pocket = PocketStoreConfig::new( - PathBuf::from(document.pocket.data_directory), - document.pocket.map_size_bytes, - document.pocket.reader_slots, - match document.pocket.sync_policy { - BaseRelayPocketSyncPolicyDocument::FlushOnWrite => PocketSyncPolicy::FlushOnWrite, - BaseRelayPocketSyncPolicyDocument::FlushOnShutdown => PocketSyncPolicy::FlushOnShutdown, - }, - ) - .map_err(|error| BaseRelayError::invalid(error.to_string()))?; - let groups_raw = serde_json::to_string(&document.groups).map_err(|error| { - BaseRelayError::invalid(format!("groups config JSON is invalid: {error}")) - })?; - let groups = tangle_groups::parse_group_runtime_config_json(&groups_raw) - .map_err(|error| BaseRelayError::invalid(error.to_string()))?; - if document.limits.max_pending_events == 0 { - return Err(BaseRelayError::invalid( - "limits.max_pending_events must be greater than zero", - )); - } - let tracing = base_relay_tracing_config_from_document(document.observability.tracing)?; - Ok(BaseRelayRuntimeConfig { - listen_addr, - relay_url: document.server.relay_url, - pocket, - groups, - auth_ttl_seconds: document.auth.challenge_ttl_seconds, - max_pending_events: document.limits.max_pending_events, - tracing, - }) -} - -fn base_relay_tracing_config_from_document( - document: BaseRelayTracingConfigDocument, -) -> Result<BaseRelayTracingConfig, BaseRelayError> { - BaseRelayTracingConfig::new( - document.enabled.unwrap_or(true), - document.filter.unwrap_or_else(|| { - "info,tangle=info,tangle_runtime=info,tangle_groups=info,tangle_store_pocket=info" - .to_owned() - }), - match document - .format - .unwrap_or(BaseRelayTracingFormatDocument::Json) - { - BaseRelayTracingFormatDocument::Compact => BaseRelayTracingFormat::Compact, - BaseRelayTracingFormatDocument::Json => BaseRelayTracingFormat::Json, - }, - ) -} - pub fn base_relay_info_router(document: BaseRelayInfoDocument) -> Router { Router::new() .route("/", get(base_relay_info)) @@ -1722,12 +1479,10 @@ fn pocket_event_id(event_id: &EventId) -> Result<PocketEventId, BaseRelayError> mod tests { use super::{ BaseAuthState, BaseRelay, BaseRelayInfoConfig, BaseRelayReadinessCheckStatus, - BaseRelayReadinessState, BaseRelayTracingFormat, CloseResult, base_relay_info_router, - base_relay_ops_router, parse_base_relay_runtime_config_json, + BaseRelayReadinessState, CloseResult, base_relay_info_router, base_relay_ops_router, }; use axum::body::to_bytes; use http::{Request, StatusCode, header}; - use std::path::Path; use tangle_crypto::RelaySigner; use tangle_groups::{ GroupId, KIND_GROUP_ADMINS, KIND_GROUP_CREATE_GROUP, KIND_GROUP_CREATE_INVITE, @@ -1805,33 +1560,6 @@ mod tests { assert_eq!(rejected.status(), StatusCode::NOT_FOUND); } - #[test] - fn base_relay_runtime_config_parses_v2_production_example() { - let config = parse_base_relay_runtime_config_json(include_str!( - "../../../ops/production/tangle-v2.example.json" - )) - .expect("config"); - - assert_eq!(config.listen_addr().to_string(), "0.0.0.0:7000"); - assert_eq!(config.relay_url(), "wss://relay.radroots.test"); - assert_eq!( - config.pocket_config().data_directory(), - Path::new("runtime/pocket") - ); - assert_eq!(config.pocket_config().map_size_bytes(), 1_099_511_627_776); - assert_eq!(config.pocket_config().reader_slots(), 512); - assert_eq!( - config.pocket_config().sync_policy(), - PocketSyncPolicy::FlushOnShutdown - ); - assert!(config.groups().enabled()); - assert_eq!(config.auth_ttl_seconds(), 300); - assert_eq!(config.max_pending_events(), 1024); - assert!(config.tracing().enabled()); - assert_eq!(config.tracing().format(), BaseRelayTracingFormat::Json); - config.auth_state().expect("auth"); - } - #[tokio::test] async fn base_relay_ops_router_reports_health_and_readiness() { let health = base_relay_ops_router(BaseRelayReadinessState::ready()) diff --git a/crates/tangle_runtime/src/config.rs b/crates/tangle_runtime/src/config.rs @@ -0,0 +1,284 @@ +#![forbid(unsafe_code)] + +use crate::base_relay::{BaseAuthState, BaseRelay, BaseRelayError}; +use serde::Deserialize; +use std::{net::SocketAddr, path::PathBuf}; +use tangle_groups::GroupRuntimeConfig; +use tangle_store_pocket::{PocketStoreConfig, PocketSyncPolicy}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BaseRelayRuntimeConfig { + listen_addr: SocketAddr, + relay_url: String, + pocket: PocketStoreConfig, + groups: GroupRuntimeConfig, + auth_ttl_seconds: u64, + max_pending_events: usize, + tracing: BaseRelayTracingConfig, +} + +impl BaseRelayRuntimeConfig { + pub fn listen_addr(&self) -> SocketAddr { + self.listen_addr + } + + pub fn relay_url(&self) -> &str { + &self.relay_url + } + + pub fn pocket_config(&self) -> &PocketStoreConfig { + &self.pocket + } + + pub fn groups(&self) -> &GroupRuntimeConfig { + &self.groups + } + + pub fn auth_ttl_seconds(&self) -> u64 { + self.auth_ttl_seconds + } + + pub fn max_pending_events(&self) -> usize { + self.max_pending_events + } + + pub fn tracing(&self) -> &BaseRelayTracingConfig { + &self.tracing + } + + pub fn open_relay(&self) -> Result<BaseRelay, BaseRelayError> { + BaseRelay::open_with_groups(&self.pocket, self.max_pending_events, &self.groups) + } + + pub fn auth_state(&self) -> Result<BaseAuthState, BaseRelayError> { + BaseAuthState::new(self.relay_url.clone(), self.auth_ttl_seconds) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum BaseRelayTracingFormat { + Compact, + Json, +} + +impl BaseRelayTracingFormat { + pub fn as_str(self) -> &'static str { + match self { + Self::Compact => "compact", + Self::Json => "json", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct BaseRelayTracingConfig { + enabled: bool, + filter: String, + format: BaseRelayTracingFormat, +} + +impl BaseRelayTracingConfig { + pub fn new( + enabled: bool, + filter: impl Into<String>, + format: BaseRelayTracingFormat, + ) -> Result<Self, BaseRelayError> { + let filter = filter.into(); + if filter.trim().is_empty() { + return Err(BaseRelayError::invalid( + "observability.tracing.filter must not be empty", + )); + } + Ok(Self { + enabled, + filter: filter.trim().to_owned(), + format, + }) + } + + pub fn enabled(&self) -> bool { + self.enabled + } + + pub fn filter(&self) -> &str { + &self.filter + } + + pub fn format(&self) -> BaseRelayTracingFormat { + self.format + } +} + +impl Default for BaseRelayTracingConfig { + fn default() -> Self { + Self::new( + true, + "info,tangle=info,tangle_runtime=info,tangle_groups=info,tangle_store_pocket=info", + BaseRelayTracingFormat::Json, + ) + .expect("default tracing config is valid") + } +} + +#[derive(Debug, Deserialize)] +struct BaseRelayRuntimeConfigDocument { + server: BaseRelayServerConfigDocument, + pocket: BaseRelayPocketConfigDocument, + groups: serde_json::Value, + auth: BaseRelayAuthConfigDocument, + limits: BaseRelayRuntimeLimitsDocument, + #[serde(default)] + observability: BaseRelayObservabilityConfigDocument, +} + +#[derive(Debug, Deserialize)] +struct BaseRelayServerConfigDocument { + listen_addr: String, + relay_url: String, +} + +#[derive(Debug, Deserialize)] +struct BaseRelayPocketConfigDocument { + data_directory: String, + map_size_bytes: u64, + reader_slots: u32, + sync_policy: BaseRelayPocketSyncPolicyDocument, +} + +#[derive(Debug, Clone, Copy, Deserialize)] +#[serde(rename_all = "snake_case")] +enum BaseRelayPocketSyncPolicyDocument { + FlushOnWrite, + FlushOnShutdown, +} + +#[derive(Debug, Deserialize)] +struct BaseRelayAuthConfigDocument { + challenge_ttl_seconds: u64, +} + +#[derive(Debug, Deserialize)] +struct BaseRelayRuntimeLimitsDocument { + max_pending_events: usize, +} + +#[derive(Debug, Default, Deserialize)] +struct BaseRelayObservabilityConfigDocument { + #[serde(default)] + tracing: BaseRelayTracingConfigDocument, +} + +#[derive(Debug, Default, Deserialize)] +struct BaseRelayTracingConfigDocument { + enabled: Option<bool>, + filter: Option<String>, + format: Option<BaseRelayTracingFormatDocument>, +} + +#[derive(Debug, Clone, Copy, Deserialize)] +#[serde(rename_all = "snake_case")] +enum BaseRelayTracingFormatDocument { + Compact, + Json, +} + +pub fn parse_base_relay_runtime_config_json( + raw: &str, +) -> Result<BaseRelayRuntimeConfig, BaseRelayError> { + let document = + serde_json::from_str::<BaseRelayRuntimeConfigDocument>(raw).map_err(|error| { + BaseRelayError::invalid(format!( + "base relay runtime config JSON is invalid: {error}" + )) + })?; + let listen_addr = document + .server + .listen_addr + .parse::<SocketAddr>() + .map_err(|error| { + BaseRelayError::invalid(format!("server.listen_addr is invalid: {error}")) + })?; + let pocket = PocketStoreConfig::new( + PathBuf::from(document.pocket.data_directory), + document.pocket.map_size_bytes, + document.pocket.reader_slots, + match document.pocket.sync_policy { + BaseRelayPocketSyncPolicyDocument::FlushOnWrite => PocketSyncPolicy::FlushOnWrite, + BaseRelayPocketSyncPolicyDocument::FlushOnShutdown => PocketSyncPolicy::FlushOnShutdown, + }, + ) + .map_err(|error| BaseRelayError::invalid(error.to_string()))?; + let groups_raw = serde_json::to_string(&document.groups).map_err(|error| { + BaseRelayError::invalid(format!("groups config JSON is invalid: {error}")) + })?; + let groups = tangle_groups::parse_group_runtime_config_json(&groups_raw) + .map_err(|error| BaseRelayError::invalid(error.to_string()))?; + if document.limits.max_pending_events == 0 { + return Err(BaseRelayError::invalid( + "limits.max_pending_events must be greater than zero", + )); + } + let tracing = base_relay_tracing_config_from_document(document.observability.tracing)?; + Ok(BaseRelayRuntimeConfig { + listen_addr, + relay_url: document.server.relay_url, + pocket, + groups, + auth_ttl_seconds: document.auth.challenge_ttl_seconds, + max_pending_events: document.limits.max_pending_events, + tracing, + }) +} + +fn base_relay_tracing_config_from_document( + document: BaseRelayTracingConfigDocument, +) -> Result<BaseRelayTracingConfig, BaseRelayError> { + BaseRelayTracingConfig::new( + document.enabled.unwrap_or(true), + document.filter.unwrap_or_else(|| { + "info,tangle=info,tangle_runtime=info,tangle_groups=info,tangle_store_pocket=info" + .to_owned() + }), + match document + .format + .unwrap_or(BaseRelayTracingFormatDocument::Json) + { + BaseRelayTracingFormatDocument::Compact => BaseRelayTracingFormat::Compact, + BaseRelayTracingFormatDocument::Json => BaseRelayTracingFormat::Json, + }, + ) +} + +#[cfg(test)] +mod tests { + use super::{BaseRelayTracingFormat, parse_base_relay_runtime_config_json}; + use std::path::Path; + use tangle_store_pocket::PocketSyncPolicy; + + #[test] + fn base_relay_runtime_config_parses_v2_production_example() { + let config = parse_base_relay_runtime_config_json(include_str!( + "../../../ops/production/tangle-v2.example.json" + )) + .expect("config"); + + assert_eq!(config.listen_addr().to_string(), "0.0.0.0:7000"); + assert_eq!(config.relay_url(), "wss://relay.radroots.test"); + assert_eq!( + config.pocket_config().data_directory(), + Path::new("runtime/pocket") + ); + assert_eq!(config.pocket_config().map_size_bytes(), 1_099_511_627_776); + assert_eq!(config.pocket_config().reader_slots(), 512); + assert_eq!( + config.pocket_config().sync_policy(), + PocketSyncPolicy::FlushOnShutdown + ); + assert!(config.groups().enabled()); + assert_eq!(config.auth_ttl_seconds(), 300); + assert_eq!(config.max_pending_events(), 1024); + assert!(config.tracing().enabled()); + assert_eq!(config.tracing().format(), BaseRelayTracingFormat::Json); + config.auth_state().expect("auth"); + } +} diff --git a/crates/tangle_runtime/src/lib.rs b/crates/tangle_runtime/src/lib.rs @@ -2,13 +2,12 @@ pub mod base_relay; pub mod chorus_pocket; +pub mod config; use std::{fmt, fs, path::Path, path::PathBuf}; -use base_relay::{ - BaseRelayError, BaseRelayReadinessState, BaseRelayRuntimeConfig, - parse_base_relay_runtime_config_json, -}; +use base_relay::{BaseRelayError, BaseRelayReadinessState}; +use config::{BaseRelayRuntimeConfig, parse_base_relay_runtime_config_json}; pub const TANGLE_SUPPORTED_NIPS: [u16; 6] = [1, 11, 29, 42, 45, 70]; pub const TANGLE_RELAY_SOFTWARE: &str = "https://github.com/radrootslabs/tangle";