Skip to content

Implement Component Trait Combining Startable and ReadyDoneAware #55

@thep2p

Description

@thep2p

Overview

The Go implementation has a Component interface that combines Startable and ReadyDoneAware traits. This provides a complete lifecycle management interface for all components in the system.

Background

Reference implementation: skipgraph-go/modules/component.go

A Component is any module that:

  • Can be started (Startable)
  • Has ready and done states (ReadyDoneAware)

Requirements

1. Define Component Trait

use async_trait::async_trait;

/// A component that can be started and has lifecycle awareness
#[async_trait]
pub trait Component: Startable + ReadyDoneAware + Send + Sync {
    // No additional methods needed - this is a marker trait
    // that combines Startable and ReadyDoneAware
}

// Blanket implementation for any type that implements both traits
impl<T> Component for T 
where 
    T: Startable + ReadyDoneAware + Send + Sync
{
}

2. Provide Base Component Implementation

use std::sync::Arc;
use tokio::sync::watch;

/// Base implementation that components can wrap or extend
pub struct BaseComponent {
    start_once: StartOnce,
    lifecycle: LifecycleState,
}

impl BaseComponent {
    pub fn new() -> Self {
        Self {
            start_once: StartOnce::new(),
            lifecycle: LifecycleState::new(),
        }
    }
    
    /// Helper to check and mark as started
    pub fn ensure_start_once(&self) -> Result<(), &'static str> {
        self.start_once.ensure_once()
    }
    
    /// Signal that component is ready
    pub fn signal_ready(&self) {
        self.lifecycle.signal_ready()
    }
    
    /// Signal that component is done
    pub fn signal_done(&self) {
        self.lifecycle.signal_done()
    }
}

impl ReadyDoneAware for BaseComponent {
    fn ready(&self) -> watch::Receiver<bool> {
        self.lifecycle.ready()
    }
    
    fn done(&self) -> watch::Receiver<bool> {
        self.lifecycle.done()
    }
}

3. Example Component Implementation

pub struct NetworkComponent {
    base: BaseComponent,
    port: u16,
    // other fields
}

impl NetworkComponent {
    pub fn new(port: u16) -> Self {
        Self {
            base: BaseComponent::new(),
            port,
        }
    }
}

#[async_trait]
impl Startable for NetworkComponent {
    async fn start(&self, ctx: Arc<dyn ThrowableContext>) {
        if let Err(e) = self.base.ensure_start_once() {
            panic!("{}", e);
        }
        
        let base = self.base.clone();
        let port = self.port;
        
        tokio::spawn(async move {
            // Start network listener
            match start_listener(port).await {
                Ok(listener) => {
                    base.signal_ready();
                    
                    // Run until cancelled
                    let mut cancelled = ctx.cancelled();
                    tokio::select! {
                        _ = serve_connections(listener) => {},
                        _ = cancelled.changed() => {},
                    }
                    
                    base.signal_done();
                }
                Err(e) => {
                    ctx.throw_irrecoverable(Box::new(e));
                }
            }
        });
    }
}

impl ReadyDoneAware for NetworkComponent {
    fn ready(&self) -> watch::Receiver<bool> {
        self.base.ready()
    }
    
    fn done(&self) -> watch::Receiver<bool> {
        self.base.done()
    }
}

// NetworkComponent automatically implements Component
// due to the blanket implementation

4. Usage Pattern

async fn start_application() {
    let ctx = Arc::new(Context::new());
    
    let network = Arc::new(NetworkComponent::new(8080));
    let storage = Arc::new(StorageComponent::new());
    
    // Start components
    network.start(ctx.clone()).await;
    storage.start(ctx.clone()).await;
    
    // Wait for components to be ready
    wait_for_ready(&network).await;
    wait_for_ready(&storage).await;
    
    println!("Application ready");
}

async fn wait_for_ready(component: &dyn Component) {
    let mut ready = component.ready();
    while !*ready.borrow() {
        ready.changed().await.ok();
    }
}

Benefits

  • Unified interface for all components
  • Consistent lifecycle management
  • Easy to compose components
  • Type-safe component handling

Testing

  • Test blanket implementation
  • Test with various component types
  • Test lifecycle transitions
  • Test error scenarios

Dependencies

Priority

High - This is the core trait for the component system

Related Issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions