Skip to content

Latest commit

 

History

History
476 lines (370 loc) · 12.7 KB

File metadata and controls

476 lines (370 loc) · 12.7 KB

BYOB Refactoring Pattern - Trait-Based Composition

FOSSIL (S175): This refactoring was executed. byob_impl.rs was split into byob_impl/ directory module with deployment_lifecycle.rs, network_manager.rs, health_monitor.rs, and test submodules. File path and line counts below are historical. Retained as pattern reference for future refactoring.

File: crates/core/toadstool/src/byob/byob_impl/ (was byob_impl.rs)
Original Size: 927 lines
Status: ✅ Executed


🎯 Refactoring Objective

Goal: Apply trait-based composition to separate concerns and improve maintainability

Not a Goal: Blindly split into smaller files (we want architectural improvements)


📊 Current Structure Analysis

File Breakdown

Concern Lines Functions Complexity
Service Execution ~250 3 High
Network Management ~220 3 Medium
Health Monitoring ~150 2 Medium
Resource Management ~100 2 Low
Lifecycle (Coordinator) ~150 5 Medium
Tests ~57 5 Low

Total: 927 lines

Assessment

Current State: ✅ Good (not bad code!)

  • Clear separation of concerns in function names
  • Well-documented
  • Proper error handling
  • Tests included

Why Refactor:

  • Easier testing (mock individual concerns)
  • Better reusability (traits can be implemented by other types)
  • Clearer responsibilities (single responsibility per trait)
  • Reduced cognitive load (smaller files, focused logic)

🏗️ Proposed Architecture

Trait Abstraction Pattern

// BEFORE: All in one impl block
impl ByobComputeExecutor {
    fn create_service_execution_request(...) { /* ... */ }
    async fn execute_services(...) { /* ... */ }
    fn create_deployment_network(...) { /* ... */ }
    fn allocate_external_ip(...) { /* ... */ }
    async fn monitor_deployment_health(...) { /* ... */ }
    fn perform_health_check(...) { /* ... */ }
    async fn update_resource_usage(...) { /* ... */ }
    async fn stop_service_execution(...) { /* ... */ }
    // + 5 lifecycle methods
}

// AFTER: Trait-based composition
trait ServiceExecutor {
    fn create_service_execution_request(...) -> Result<ExecutionRequest>;
    async fn execute_services(...) -> Result<()>;
    async fn stop_service_execution(...) -> Result<()>;
}

trait NetworkManager {
    fn create_deployment_network(...) -> NetworkInfo;
    fn allocate_external_ip(...) -> Option<String>;
}

trait HealthMonitor {
    async fn monitor_deployment_health(...) -> Result<()>;
    fn perform_health_check(...) -> Result<bool>;
}

trait ResourceManager {
    async fn update_resource_usage(...) -> Result<()>;
    async fn get_resource_usage(...) -> Result<ResourceUsage>;
}

// Coordinator stays in main impl (lifecycle only)
impl ByobComputeExecutor {
    // Coordination methods use traits
    async fn deploy_biome(&self, ...) -> Result<Response> {
        // Uses: ServiceExecutor, NetworkManager, ResourceManager
    }
    
    async fn stop_deployment(&self, ...) -> Result<()> {
        // Uses: ServiceExecutor
    }
}

// Trait implementations in separate files
impl ServiceExecutor for ByobComputeExecutor { /* ... */ }
impl NetworkManager for ByobComputeExecutor { /* ... */ }
impl HealthMonitor for ByobComputeExecutor { /* ... */ }
impl ResourceManager for ByobComputeExecutor { /* ... */ }

📁 Proposed File Structure

New Files

crates/core/toadstool/src/byob/
├── byob_impl.rs              (~400 lines) - Coordinator + lifecycle
├── service_executor.rs       (~250 lines) - Service execution trait + impl
├── network_manager.rs        (~220 lines) - Network management trait + impl
├── health_monitor.rs         (~150 lines) - Health monitoring trait + impl
├── resource_manager.rs       (~100 lines) - Resource tracking trait + impl
├── byob_types.rs             (existing)
├── config.rs                 (existing)
├── deployment.rs             (existing)
├── validation.rs             (existing)
└── mod.rs                    (updated exports)

Line Count Reduction

File Before After Change
byob_impl.rs 927 ~400 -57%
New: service_executor.rs 0 ~250 +250
New: network_manager.rs 0 ~220 +220
New: health_monitor.rs 0 ~150 +150
New: resource_manager.rs 0 ~100 +100
Total 927 1120 +193

Note: Line count increases (good thing!) - we gain:

  • 4 trait definitions (~40 lines total)
  • Better documentation per trait (~80 lines)
  • More modular structure

🔬 Detailed Design

1. ServiceExecutor Trait

Purpose: Handle service execution lifecycle

Methods:

#[async_trait]
pub trait ServiceExecutor: Send + Sync {
    /// Create execution request for a service
    fn create_service_execution_request(
        &self,
        service: &ServiceSpec,
        deployment_id: Uuid,
    ) -> ToadStoolResult<ExecutionRequest>;
    
    /// Execute all services in a deployment
    async fn execute_services(
        &self,
        deployment: &mut ActiveDeployment,
    ) -> ToadStoolResult<()>;
    
    /// Stop a specific service execution
    async fn stop_service_execution(
        &self,
        service_name: String,
        execution_id: Uuid,
    ) -> ToadStoolResult<()>;
}

Benefits:

  • Can mock for testing (inject fake ServiceExecutor)
  • Can reuse in other executor types
  • Clear contract for service management

2. NetworkManager Trait

Purpose: Handle networking and IP allocation

Methods:

pub trait NetworkManager: Send + Sync {
    /// Create network configuration for deployment
    fn create_deployment_network(
        &self,
        deployment: &ByobDeploymentRequest,
    ) -> NetworkInfo;
    
    /// Allocate external IP for a service
    fn allocate_external_ip(
        &self,
        service_spec: &ServiceSpec,
        team_id: &str,
    ) -> Option<String>;
}

Benefits:

  • Can swap implementations (different network backends)
  • Easy to test networking logic in isolation
  • Clear boundary for network-related code

3. HealthMonitor Trait

Purpose: Monitor deployment and service health

Methods:

#[async_trait]
pub trait HealthMonitor: Send + Sync {
    /// Monitor overall deployment health
    async fn monitor_deployment_health(
        &self,
        deployment_id: Uuid,
    ) -> ToadStoolResult<()>;
    
    /// Perform health check for a specific service
    fn perform_health_check(
        &self,
        service_name: &str,
        health_check: &HealthCheck,
    ) -> ToadStoolResult<bool>;
}

Benefits:

  • Can mock health checks for testing
  • Can implement different health strategies
  • Clear separation of monitoring logic

4. ResourceManager Trait

Purpose: Track and report resource usage

Methods:

#[async_trait]
pub trait ResourceManager: Send + Sync {
    /// Update resource usage for a deployment
    async fn update_resource_usage(
        &self,
        deployment_id: Uuid,
    ) -> ToadStoolResult<()>;
    
    /// Get current resource usage
    async fn get_resource_usage(
        &self,
        deployment_id: Uuid,
    ) -> ToadStoolResult<ResourceUsage>;
}

Benefits:

  • Can implement different tracking strategies
  • Easy to add caching layer
  • Clear boundary for resource-related code

🎯 Migration Strategy

Phase 1: Extract Traits (Low Risk)

Step 1: Create trait definitions

# Create new files with trait definitions only
touch crates/core/toadstool/src/byob/service_executor.rs
touch crates/core/toadstool/src/byob/network_manager.rs
touch crates/core/toadstool/src/byob/health_monitor.rs
touch crates/core/toadstool/src/byob/resource_manager.rs

Step 2: Add traits to mod.rs

pub mod service_executor;
pub mod network_manager;
pub mod health_monitor;
pub mod resource_manager;

Step 3: Implement traits in byob_impl.rs

impl ServiceExecutor for ByobComputeExecutor {
    // Copy existing methods here
}
// ... same for other traits

Step 4: Test (should compile with zero behavior changes)

cargo test --package toadstool

Phase 2: Move Implementations (Medium Risk)

Step 1: Move trait impls to dedicated files

  • Cut impl blocks from byob_impl.rs
  • Paste into respective trait files

Step 2: Update imports in byob_impl.rs

use super::service_executor::ServiceExecutor;
use super::network_manager::NetworkManager;
// ...

Step 3: Test again

cargo test --package toadstool

Phase 3: Optimize (Low Risk)

Step 1: Remove #[allow(dead_code)] as methods are used Step 2: Add more tests for individual traits Step 3: Document each trait thoroughly


🧪 Testing Strategy

Before Refactoring

#[tokio::test]
async fn test_deploy_biome() {
    let executor = ByobComputeExecutor::new(...);
    let response = executor.deploy_biome(request).await?;
    assert!(response.is_successful());
}

After Refactoring

// Test individual concerns in isolation

#[test]
fn test_network_manager_ip_allocation() {
    let executor = ByobComputeExecutor::new(...);
    let ip = executor.allocate_external_ip(&service, "team-1");
    assert_eq!(ip, Some("203.0.113.42".to_string()));
}

#[tokio::test]
async fn test_service_executor_executes_services() {
    let executor = ByobComputeExecutor::new(...);
    let mut deployment = ActiveDeployment::new(...);
    executor.execute_services(&mut deployment).await?;
    assert_eq!(deployment.status, DeploymentStatus::Running);
}

// Integration test still works
#[tokio::test]
async fn test_deploy_biome_integration() {
    let executor = ByobComputeExecutor::new(...);
    let response = executor.deploy_biome(request).await?;
    assert!(response.is_successful());
}

Benefits:

  • Faster unit tests (no full deployment needed)
  • Better error isolation (know exactly which component failed)
  • Easier mocking (mock just what you need)

📈 Expected Improvements

Metrics

Metric Before After Change
Lines per file 927 ~400 -57%
Cognitive complexity High Medium
Test granularity Coarse Fine
Reusability Low High
Mock-ability Hard Easy

Maintainability

Before:

  • "Where's the network code?" → Search 927 lines
  • "How do I test IP allocation?" → Full deployment needed
  • "Can I reuse health checks?" → Tightly coupled

After:

  • "Where's the network code?" → network_manager.rs
  • "How do I test IP allocation?" → Unit test NetworkManager
  • "Can I reuse health checks?" → impl HealthMonitor for MyType

🚨 Risks & Mitigation

Risk 1: Breaking Changes

Risk: Refactoring breaks existing code
Mitigation:

  • Keep public API identical (ByobExecutor trait unchanged)
  • Run full test suite after each phase
  • Use compiler to catch issues (Rust's type system helps!)

Risk 2: Over-Abstraction

Risk: Too many traits, too complex
Mitigation:

  • Only 4 traits (manageable)
  • Each trait has clear, focused purpose
  • All traits are actively used by coordinator

Risk 3: Performance Regression

Risk: Trait dispatch adds overhead
Mitigation:

  • Traits are monomorphized (zero runtime cost in most cases)
  • Benchmark before/after (expect < 1% difference)
  • Profile if needed

✅ Success Criteria

Refactoring is successful if:

  1. ✅ All tests pass (zero behavior changes)
  2. ✅ byob_impl.rs < 500 lines (coordinator only)
  3. ✅ Each trait file < 300 lines (focused concerns)
  4. ✅ Zero clippy warnings
  5. ✅ Improved test granularity (can test traits in isolation)
  6. ✅ Better documentation (per-trait docs)

🎓 Lessons for Other Files

This pattern applies to:

  1. crates/runtime/universal/src/backends/podman.rs (1038 lines)
  2. crates/barracuda/src/device/wgpu_device.rs (891 lines)
  3. Any file > 800 lines with multiple concerns

Pattern:

  1. Identify distinct concerns (2-5 concerns)
  2. Create traits for each concern
  3. Keep coordinator in main file
  4. Move implementations to trait files
  5. Test each trait in isolation
  6. Document benefits

📚 References


Document: docs/architecture/REFACTORING_PATTERN_BYOB.md
Status: ✅ Analysis Complete | 🔄 Ready to Implement
Est. Time: 4-6 hours
Risk Level: Low (can be done incrementally)

Let's refactor smart, not just split files! 🚀