Skip to main content

mecha10_core/
prelude.rs

1pub use crate::context::{
2    Context, InfrastructureConfig, MinioConfig, MlflowConfig, PostgresConfig, Receiver, RedisConfig,
3};
4
5#[cfg(feature = "messaging")]
6pub use crate::stream::ReceiverExt;
7
8#[cfg(feature = "messaging")]
9pub use crate::publish::ContextPublishExt;
10
11#[cfg(feature = "messaging")]
12pub use crate::rpc::{Request, Response, RpcExt};
13
14#[cfg(feature = "messaging")]
15pub use crate::rpc_streaming::{
16    StreamError, StreamMessage, StreamPayload, StreamReceiver, StreamSender, StreamingRpcExt,
17};
18
19#[cfg(feature = "messaging")]
20pub use crate::discovery::DiscoveryExt;
21pub use crate::discovery::{NodeMetadata, NodeStatus};
22
23pub use crate::schema::{
24    CompatibilityCheck, CompatibilityMode, MessageSchema, MessageVersionInfo, SchemaEvolution, SchemaField,
25    SchemaRegistryExt, SchemaVersioningExt,
26};
27
28pub use crate::auth::{AuthExt, Permission, Policy, Rule};
29
30pub use crate::secrets::{
31    EnvironmentBackend, RedisBackend, SecretBackend, SecretsConfig, SecretsManager, VaultBackend,
32};
33
34#[cfg(feature = "messaging")]
35pub use crate::health::{
36    HealthMonitor, HealthReport, HealthReporter, HealthReportingExt, NodeHealthInfo, SystemHealth,
37};
38
39#[cfg(feature = "messaging")]
40pub use crate::behavior_interrupt::{
41    BehaviorControl, BehaviorInterruptConfig, BehaviorInterruptTrigger, InterruptMode,
42};
43
44#[cfg(feature = "messaging")]
45pub use crate::on_demand::subscribe_on_demand;
46pub use crate::on_demand::OnDemandRequest;
47
48pub use crate::metrics::{Counter, Gauge, Histogram, MetricsRegistry, NodeMetrics};
49
50pub use crate::rate_limit::{MultiKeyRateLimiter, RateLimit, RateLimiter, RateLimiterManager};
51
52pub use crate::circuit_breaker::{CircuitBreaker, CircuitBreakerConfig, CircuitState};
53
54#[cfg(feature = "messaging")]
55pub use crate::dead_letter::{DeadLetterConfig, DeadLetterQueue, FailedMessage};
56
57#[cfg(feature = "schema-validation")]
58pub use crate::schema_validation::{
59    export_project_schema, get_project_schema, load_and_validate_project, validate_config_file,
60    validate_node_config_file, validate_node_config_json, validate_project_config, validate_project_json,
61};
62
63pub use crate::dependency::{create_graph, DependencyGraph, ShutdownPlan, StartupPlan};
64
65#[cfg(feature = "messaging")]
66pub use crate::lifecycle::{
67    get_timestamp_ms, topic_node_lifecycle, topic_node_status, topic_node_trigger, ModeChanged, ModeError, ModeRequest,
68    NodeLifecycleAction, NodeLifecycleEvent, NodeLifecycleStatus, NodeStatusEvent, NodeTriggerEvent,
69    TOPIC_MODE_CHANGED, TOPIC_MODE_ERROR, TOPIC_MODE_REQUEST,
70};
71
72pub use crate::topic_registry::{TopicInfo, TopicRegistry, TopologyStats};
73
74pub use crate::topic_utils::{extract_category, format_elapsed_time, TopicMetadata, Topics};
75
76pub use crate::state::{
77    FilesystemStateManager, MemoryStateManager, StateManager, StateOptions, StateSnapshot, VersionedState,
78};
79
80pub use crate::recovery::{
81    BackoffStrategy, DegradationMode, RecoveryAction, RecoveryAttempt, RecoveryHistory, RecoveryManager,
82    RecoveryPolicy, RecoveryPolicyBuilder, RecoveryStats,
83};
84
85#[cfg(feature = "messaging")]
86pub use crate::tracing_otel::{
87    JaegerExporter, SpanBuilder, SpanData, TraceContext, TraceExporter, TracedMessage, TracingConfig, TracingExt,
88    TracingHandle, ZipkinExporter,
89};
90
91pub use crate::profiling::{
92    measure_latency, ExecutionStats, LatencyStats, MemoryStats, PerformanceMetrics, PerformanceReport, ProfilingExt,
93    ThroughputStats, TimerGuard,
94};
95
96pub use crate::service::Service;
97#[cfg(feature = "messaging")]
98pub use crate::service_manager::ServiceManager;
99
100pub use crate::node::{
101    fmt_node_with_controller, run_node, HealthPriority, HealthReportingConfig, HealthStatusExt, Node, NodeImpl,
102    NodeTag, NodeType, RateConfig,
103};
104
105pub use crate::config::{
106    env_or_default, load_and_validate, load_config, load_config_from_env, substitute_env_vars, Validate,
107};
108
109pub use crate::model::{load_labels, load_model_config, CustomLabelsConfig, ModelConfig, PreprocessingConfig};
110
111pub use crate::builder_setter;
112pub use crate::enum_str_method;
113
114pub use crate::messages::Message;
115
116pub use crate::messages::Timestamped;
117
118pub use crate::types::*;
119
120pub use crate::error::{Mecha10Error, Result};
121
122pub use async_trait::async_trait;
123
124pub use tokio::{
125    self, select,
126    time::{interval, sleep, timeout, Duration, Instant},
127};
128
129pub use tokio::task::JoinHandle;
130
131pub use serde::{Deserialize, Serialize};
132pub use serde_json::{self, json, Value as JsonValue};
133
134pub use schemars::JsonSchema;
135
136pub use tracing::{debug, error, info, instrument, trace, warn, Level};
137
138#[cfg(feature = "macros")]
139pub use mecha10_macros::Node;
140
141#[cfg(feature = "macros")]
142pub use mecha10_macros::Config;
143
144#[cfg(feature = "macros")]
145pub use mecha10_macros::ConfigDefaults;
146
147/// Shared implementation behind `now_micros`/`now_secs`/`now_millis`, which differ only in
148/// which `Duration` unit they read off the epoch offset.
149fn since_epoch() -> std::time::Duration {
150    std::time::SystemTime::now()
151        .duration_since(std::time::UNIX_EPOCH)
152        .expect("Time went backwards")
153}
154
155/// Get current time in microseconds since UNIX epoch
156pub fn now_micros() -> u64 {
157    since_epoch().as_micros() as u64
158}
159
160/// Get current time in whole seconds since UNIX epoch
161pub fn now_secs() -> u64 {
162    since_epoch().as_secs()
163}
164
165/// Create a periodic interval from a frequency in Hz
166///
167/// This is a convenience helper that converts frequency (Hz) to a tokio interval.
168/// Commonly used for periodic publishing in drivers and nodes.
169///
170/// # Arguments
171///
172/// * `hz` - Frequency in Hertz (cycles per second)
173///
174/// # Returns
175///
176/// A tokio::time::Interval that ticks at the specified frequency
177///
178/// # Example
179///
180/// Marked `no_run`: the `loop` below never breaks, so running it to completion
181/// would hang the doctest forever.
182/// ```rust,no_run
183/// use mecha10_core::prelude::*;
184///
185/// # async fn example() {
186/// // Instead of:
187/// // let interval_duration = Duration::from_secs_f32(1.0 / 30.0);
188/// // let mut interval = tokio::time::interval(interval_duration);
189///
190/// // Simply use:
191/// let mut interval = interval_at_hz(30.0);
192///
193/// loop {
194///     interval.tick().await;
195///     // Publish at 30 Hz
196/// }
197/// # }
198/// ```
199pub fn interval_at_hz(hz: f32) -> tokio::time::Interval {
200    let duration = std::time::Duration::from_secs_f32(1.0 / hz);
201    tokio::time::interval(duration)
202}
203
204/// Initialize logging with sensible defaults
205///
206/// This function is safe to call multiple times - it will silently succeed if
207/// a tracing subscriber is already set (e.g., when running bundled in the CLI).
208///
209/// The logging system will automatically include node context (node_id) in all
210/// log messages when using the Context's logging methods or the `in_node_context!` macro.
211///
212/// Under the `messaging` feature, this also attaches
213/// [`crate::log_publisher::RedisLogLayer`], the layer that ships captured
214/// log events to Redis on `/system/log` once `crate::log_publisher::attach`
215/// has been called with a live `Context` (done by `run_node`). It's
216/// attached here - unconditionally, before any node-specific state exists -
217/// rather than deferred, because `init_logging()` is what every node
218/// binary's `main.rs` calls *first* (before `run_node` ever runs): `tracing`
219/// only lets the first `try_init()` call in a process actually install a
220/// subscriber, so the redis layer has to ride along with whichever call
221/// wins that race, not assume it'll be `run_node`'s.
222pub fn init_logging() {
223    use tracing_subscriber::EnvFilter;
224
225    static SENTRY_GUARD: std::sync::OnceLock<sentry::ClientInitGuard> = std::sync::OnceLock::new();
226    if SENTRY_GUARD.get().is_none() && sentry::Hub::current().client().is_none() {
227        let sentry_dsn = std::env::var("SENTRY_DSN_NODES").unwrap_or_else(|_| {
228            "https://7ea967345cb382353a93ce31365e4790@o4506107376631808.ingest.us.sentry.io/4512334108229632"
229                .to_string()
230        });
231        let mut sentry_opts = sentry::ClientOptions::default();
232        sentry_opts.dsn = sentry_dsn.parse().ok();
233        sentry_opts.release = sentry::release_name!();
234        let _ = SENTRY_GUARD.set(sentry::init(sentry_opts));
235    }
236
237    let base = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
238    let curated = format!("{base},ort=warn,rtc_dtls=error,rtc_ice=error,rtc::peer_connection::handler=error");
239    let env_filter = EnvFilter::try_new(&curated).unwrap_or_else(|_| {
240        EnvFilter::new("info,ort=warn,rtc_dtls=error,rtc_ice=error,rtc::peer_connection::handler=error")
241    });
242
243    #[cfg(feature = "messaging")]
244    {
245        use tracing_subscriber::prelude::*;
246
247        let _ = tracing_subscriber::registry()
248            .with(env_filter)
249            .with(tracing_subscriber::fmt::layer())
250            .with(crate::log_publisher::RedisLogLayer::new())
251            .try_init();
252    }
253
254    #[cfg(not(feature = "messaging"))]
255    {
256        let _ = tracing_subscriber::fmt().with_env_filter(env_filter).try_init();
257    }
258}
259
260#[macro_export]
261macro_rules! in_node_context {
262    ($ctx:expr, $body:block) => {{
263        let _span = tracing::info_span!("node", node_id = $ctx.node_id()).entered();
264        $body
265    }};
266}
267
268pub use in_node_context;
269
270#[macro_export]
271macro_rules! node_binary_main {
272    ($node_crate:ident) => {
273        #[tokio::main]
274        async fn main() -> anyhow::Result<()> {
275            $crate::prelude::init_logging();
276
277            $node_crate::run().await.map_err(|e| anyhow::anyhow!(e))
278        }
279    };
280}
281
282pub use node_binary_main;
283
284/// Get the current time in milliseconds since UNIX epoch
285///
286/// This is useful when microsecond precision is excessive (logs, metrics, etc.).
287///
288/// # Example
289///
290/// ```rust
291/// use mecha10_core::prelude::*;
292///
293/// let timestamp_ms = now_millis();
294/// println!("Current time: {} ms", timestamp_ms);
295/// ```
296pub fn now_millis() -> u64 {
297    since_epoch().as_millis() as u64
298}
299
300/// Calculate time elapsed in microseconds since a given timestamp
301///
302/// # Arguments
303///
304/// * `timestamp_micros` - A timestamp in microseconds (from `now_micros()`)
305///
306/// # Returns
307///
308/// Elapsed time in microseconds
309///
310/// # Example
311///
312/// ```rust
313/// use mecha10_core::prelude::*;
314///
315/// let start = now_micros();
316/// // ... do work ...
317/// let elapsed = elapsed_micros(start);
318/// println!("Operation took {} μs", elapsed);
319/// ```
320pub fn elapsed_micros(timestamp_micros: u64) -> u64 {
321    now_micros().saturating_sub(timestamp_micros)
322}
323
324/// Convert a microsecond timestamp to seconds (as f64)
325///
326/// Useful for displaying timestamps in human-readable format.
327///
328/// # Example
329///
330/// ```rust
331/// use mecha10_core::prelude::*;
332///
333/// let timestamp = now_micros();
334/// let seconds = timestamp_as_secs(timestamp);
335/// println!("Time: {:.6} seconds", seconds);
336/// ```
337pub fn timestamp_as_secs(timestamp_micros: u64) -> f64 {
338    timestamp_micros as f64 / 1_000_000.0
339}
340
341/// Create a Duration from a frequency in Hz
342///
343/// # Example
344///
345/// ```rust
346/// use mecha10_core::prelude::*;
347///
348/// let duration = duration_from_hz(30.0);  // 33.33ms for 30Hz
349/// ```
350pub fn duration_from_hz(hz: f32) -> Duration {
351    Duration::from_secs_f32(1.0 / hz)
352}
353
354/// Normalize an angle in radians to the range [-π, π]
355///
356/// This is essential for angle calculations to avoid unwanted wraparound effects.
357///
358/// # Example
359///
360/// ```rust
361/// use mecha10_core::prelude::*;
362///
363/// let angle = normalize_angle(7.0);  // Returns ~0.717 (7.0 - 2π)
364/// let angle2 = normalize_angle(-4.0); // Returns ~2.283 (-4.0 + 2π)
365/// ```
366pub fn normalize_angle(angle: f32) -> f32 {
367    use std::f32::consts::PI;
368    let mut normalized = angle;
369    while normalized > PI {
370        normalized -= 2.0 * PI;
371    }
372    while normalized < -PI {
373        normalized += 2.0 * PI;
374    }
375    normalized
376}
377
378/// Normalize an angle in degrees to the range [-180, 180]
379///
380/// # Example
381///
382/// ```rust
383/// use mecha10_core::prelude::*;
384///
385/// let angle = normalize_angle_degrees(270.0);  // Returns -90.0
386/// ```
387pub fn normalize_angle_degrees(angle: f32) -> f32 {
388    let mut normalized = angle;
389    while normalized > 180.0 {
390        normalized -= 360.0;
391    }
392    while normalized < -180.0 {
393        normalized += 360.0;
394    }
395    normalized
396}
397
398/// Calculate the shortest angular difference between two angles in radians
399///
400/// Returns the angular distance in the range [-π, π].
401///
402/// # Example
403///
404/// ```rust
405/// use mecha10_core::prelude::*;
406///
407/// let diff = angle_difference(0.1, 6.2);  // Shortest path wrapping around
408/// ```
409pub fn angle_difference(from: f32, to: f32) -> f32 {
410    normalize_angle(to - from)
411}
412
413/// Convert degrees to radians
414///
415/// # Example
416///
417/// ```rust
418/// use mecha10_core::prelude::*;
419///
420/// let radians = degrees_to_radians(180.0);  // Returns π
421/// ```
422pub fn degrees_to_radians(degrees: f32) -> f32 {
423    degrees * std::f32::consts::PI / 180.0
424}
425
426/// Convert radians to degrees
427///
428/// # Example
429///
430/// ```rust
431/// use mecha10_core::prelude::*;
432///
433/// let degrees = radians_to_degrees(std::f32::consts::PI);  // Returns 180.0
434/// ```
435pub fn radians_to_degrees(radians: f32) -> f32 {
436    radians * 180.0 / std::f32::consts::PI
437}
438
439/// Safely divide two numbers, returning a default value if divisor is zero
440///
441/// # Example
442///
443/// ```rust
444/// use mecha10_core::prelude::*;
445///
446/// let result = safe_divide(10.0, 2.0, 0.0);   // Returns 5.0
447/// let result = safe_divide(10.0, 0.0, 99.0);  // Returns 99.0 (default)
448/// ```
449pub fn safe_divide(numerator: f32, denominator: f32, default: f32) -> f32 {
450    if denominator.abs() < f32::EPSILON {
451        default
452    } else {
453        numerator / denominator
454    }
455}
456
457/// Clamp a value between a minimum and maximum
458///
459/// This is available in std but this wrapper makes it more discoverable.
460///
461/// # Example
462///
463/// ```rust
464/// use mecha10_core::prelude::*;
465///
466/// let value = clamp(15.0, 0.0, 10.0);  // Returns 10.0
467/// ```
468pub fn clamp<T: PartialOrd>(value: T, min: T, max: T) -> T {
469    if value < min {
470        min
471    } else if value > max {
472        max
473    } else {
474        value
475    }
476}
477
478/// Linear interpolation between two values
479///
480/// # Arguments
481///
482/// * `a` - Start value
483/// * `b` - End value
484/// * `t` - Interpolation factor [0.0, 1.0]
485///
486/// # Example
487///
488/// ```rust
489/// use mecha10_core::prelude::*;
490///
491/// let mid = lerp(0.0, 10.0, 0.5);  // Returns 5.0
492/// ```
493pub fn lerp(a: f32, b: f32, t: f32) -> f32 {
494    a + (b - a) * t
495}
496
497pub use std::collections::{HashMap, HashSet};
498
499pub use std::sync::{Arc, Mutex};
500
501pub use std::path::{Path, PathBuf};
502
503pub type Shared<T> = Arc<T>;
504pub type SharedMut<T> = Arc<Mutex<T>>;
505
506#[cfg(test)]
507mod sentry_options_tests {
508    // Regression test for the E0639 compile error: `sentry::ClientOptions` is
509    // `#[non_exhaustive]`, so it cannot be built with a struct-literal +
510    // `..Default::default()`. Construction must go through
511    // `ClientOptions::default()` followed by field assignment, as done in
512    // `init_logging`. If a future edit reverts to the struct-literal form,
513    // this module (and `init_logging` itself) will fail to compile.
514    #[test]
515    fn client_options_builds_via_default_and_field_assignment() {
516        let dsn = "https://public@sentry.example.com/1";
517
518        let mut opts = sentry::ClientOptions::default();
519        opts.dsn = dsn.parse().ok();
520        opts.release = sentry::release_name!();
521
522        assert!(opts.dsn.is_some());
523    }
524
525    #[test]
526    fn invalid_dsn_parses_to_none_without_panicking() {
527        let mut opts = sentry::ClientOptions::default();
528        opts.dsn = "not-a-valid-dsn".parse().ok();
529
530        assert!(opts.dsn.is_none());
531    }
532}