memlink-runtime 0.2.0

Dynamic module loading framework with circuit breaker, caching, pooling, health checks, versioning, and auto-discovery
Documentation
//! Safety hardening features for the runtime.

use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc;
use std::thread;
use std::time::Duration;

use crate::error::{Error, Result};

pub const DEFAULT_CALL_TIMEOUT: Duration = Duration::from_secs(30);
pub const DEFAULT_MAX_STACK_DEPTH: usize = 100;
pub const DEFAULT_MEMORY_LIMIT: usize = 64 * 1024 * 1024;

#[derive(Debug, Clone)]
pub struct SafetyConfig {
    pub call_timeout: Duration,
    pub max_stack_depth: usize,
    pub memory_limit: usize,
    pub enforce_timeout: bool,
    pub check_stack_depth: bool,
    pub enforce_memory_limit: bool,
}

impl Default for SafetyConfig {
    fn default() -> Self {
        SafetyConfig {
            call_timeout: DEFAULT_CALL_TIMEOUT,
            max_stack_depth: DEFAULT_MAX_STACK_DEPTH,
            memory_limit: DEFAULT_MEMORY_LIMIT,
            enforce_timeout: true,
            check_stack_depth: false,
            enforce_memory_limit: true,
        }
    }
}

impl SafetyConfig {
    pub fn with_call_timeout(mut self, timeout: Duration) -> Self {
        self.call_timeout = timeout;
        self
    }

    pub fn with_max_stack_depth(mut self, depth: usize) -> Self {
        self.max_stack_depth = depth;
        self
    }

    pub fn with_memory_limit(mut self, limit: usize) -> Self {
        self.memory_limit = limit;
        self
    }

    pub fn with_timeout_enforcement(mut self, enabled: bool) -> Self {
        self.enforce_timeout = enabled;
        self
    }

    pub fn with_stack_depth_check(mut self, enabled: bool) -> Self {
        self.check_stack_depth = enabled;
        self
    }

    pub fn with_memory_limit_enforcement(mut self, enabled: bool) -> Self {
        self.enforce_memory_limit = enabled;
        self
    }
}

#[derive(Debug, Default)]
pub struct StackDepth {
    depth: AtomicUsize,
}

impl StackDepth {
    pub fn new() -> Self {
        StackDepth {
            depth: AtomicUsize::new(0),
        }
    }

    pub fn enter(&self, max_depth: usize) -> Result<()> {
        let new_depth = self.depth.fetch_add(1, Ordering::Relaxed) + 1;
        if new_depth > max_depth {
            self.depth.fetch_sub(1, Ordering::Relaxed);
            Err(Error::ModulePanicked(format!(
                "Stack depth {} exceeds maximum {}",
                new_depth, max_depth
            )))
        } else {
            Ok(())
        }
    }

    pub fn exit(&self) {
        self.depth.fetch_sub(1, Ordering::Relaxed);
    }

    pub fn current(&self) -> usize {
        self.depth.load(Ordering::Relaxed)
    }

    pub fn reset(&self) {
        self.depth.store(0, Ordering::Relaxed);
    }
}

#[derive(Debug)]
pub struct MemoryTracker {
    used: AtomicUsize,
    limit: usize,
}

impl MemoryTracker {
    pub fn new(limit: usize) -> Self {
        MemoryTracker {
            used: AtomicUsize::new(0),
            limit,
        }
    }

    pub fn allocate(&self, size: usize) -> Result<()> {
        let mut current = self.used.load(Ordering::Relaxed);

        loop {
            let new_used = current + size;
            if new_used > self.limit {
                return Err(Error::InvalidModuleFormat(format!(
                    "Memory allocation of {} bytes would exceed limit of {} bytes",
                    size, self.limit
                )));
            }

            match self.used.compare_exchange(
                current,
                new_used,
                Ordering::SeqCst,
                Ordering::Relaxed,
            ) {
                Ok(_) => return Ok(()),
                Err(x) => current = x,
            }
        }
    }

    pub fn free(&self, size: usize) {
        self.used.fetch_sub(size, Ordering::Relaxed);
    }

    pub fn used(&self) -> usize {
        self.used.load(Ordering::Relaxed)
    }

    pub fn limit(&self) -> usize {
        self.limit
    }

    pub fn remaining(&self) -> usize {
        self.limit - self.used.load(Ordering::Relaxed)
    }

    pub fn usage_ratio(&self) -> f32 {
        self.used.load(Ordering::Relaxed) as f32 / self.limit as f32
    }
}

pub fn with_timeout<F, R>(timeout: Duration, f: F) -> Result<R>
where
    F: FnOnce() -> R + Send + 'static,
    R: Send + 'static,
{
    let timeout_flag = Arc::new(AtomicBool::new(false));
    let timeout_flag_clone = Arc::clone(&timeout_flag);

    let handle = thread::spawn(move || {
        let result = f();
        timeout_flag_clone.store(true, Ordering::Relaxed);
        result
    });

    match handle.join_timeout(timeout) {
        Ok(result) => Ok(result),
        Err(_) => Err(Error::ModuleCallFailed(-3)),
    }
}

trait JoinHandleTimeout<T> {
    fn join_timeout(self, timeout: Duration) -> thread::Result<T>;
}

impl<T> JoinHandleTimeout<T> for thread::JoinHandle<T> {
    fn join_timeout(self, timeout: Duration) -> thread::Result<T> {
        let start = std::time::Instant::now();
        while start.elapsed() < timeout {
            if self.is_finished() {
                return self.join();
            }
            thread::sleep(Duration::from_millis(1));
        }
        Err(Box::new("Timeout expired"))
    }
}

pub fn validate_call_safety(
    config: &SafetyConfig,
    stack_depth: &StackDepth,
    memory_tracker: Option<&MemoryTracker>,
) -> Result<()> {
    if config.check_stack_depth {
        stack_depth.enter(config.max_stack_depth)?;
    }

    if let Some(tracker) = memory_tracker {
        if config.enforce_memory_limit && tracker.used() > tracker.limit() {
            return Err(Error::InvalidModuleFormat(
                "Memory limit exceeded".to_string()
            ));
        }
    }

    Ok(())
}