Skip to content

Implement Startable Trait for Component Initialization #54

@thep2p

Description

@thep2p

Overview

The Go implementation has a Startable interface that defines how components are started. This trait ensures consistent initialization behavior across all components.

Background

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

The Startable interface provides:

  • A standard way to start components
  • Integration with ThrowableContext for error propagation
  • Guarantee of single initialization

Requirements

1. Define Startable Trait

use std::sync::Arc;
use async_trait::async_trait;

/// Trait for components that can be started
#[async_trait]
pub trait Startable: Send + Sync {
    /// Start the component.
    /// 
    /// # Arguments
    /// * `ctx` - ThrowableContext for error propagation and cancellation
    /// 
    /// # Panics
    /// Must panic if called more than once
    /// 
    /// # Errors
    /// Critical errors should be propagated via ctx.throw_irrecoverable()
    async fn start(&self, ctx: Arc<dyn ThrowableContext>);
}

2. Provide Helper for Single-Start Enforcement

use std::sync::atomic::{AtomicBool, Ordering};

/// Helper to ensure start is called only once
pub struct StartOnce {
    started: AtomicBool,
}

impl StartOnce {
    pub fn new() -> Self {
        Self {
            started: AtomicBool::new(false),
        }
    }
    
    /// Check if already started and mark as started
    /// Returns Ok(()) if this is the first call, Err if already started
    pub fn ensure_once(&self) -> Result<(), &'static str> {
        match self.started.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst) {
            Ok(_) => Ok(()),
            Err(_) => Err("Component already started"),
        }
    }
}

3. Implementation Example

pub struct MyComponent {
    start_once: StartOnce,
    lifecycle: LifecycleState,
    // other fields
}

impl MyComponent {
    pub fn new() -> Self {
        Self {
            start_once: StartOnce::new(),
            lifecycle: LifecycleState::new(),
        }
    }
}

#[async_trait]
impl Startable for MyComponent {
    async fn start(&self, ctx: Arc<dyn ThrowableContext>) {
        // Ensure single start
        if let Err(e) = self.start_once.ensure_once() {
            panic!("{}", e);
        }
        
        // Spawn initialization task
        let lifecycle = self.lifecycle.clone();
        let ctx_clone = ctx.clone();
        
        tokio::spawn(async move {
            // Initialize component
            match initialize_resources().await {
                Ok(_) => {
                    lifecycle.signal_ready();
                    
                    // Wait for cancellation
                    let mut cancelled = ctx_clone.cancelled();
                    cancelled.changed().await.ok();
                    
                    // Cleanup
                    cleanup_resources().await;
                    lifecycle.signal_done();
                }
                Err(e) => {
                    ctx_clone.throw_irrecoverable(Box::new(e));
                }
            }
        });
    }
}

Design Patterns

  1. Async Start: Use async trait to allow async initialization
  2. Non-blocking: Start should spawn tasks and return quickly
  3. Error Handling: Critical errors use throw_irrecoverable
  4. Single Start: Panic on multiple start attempts
  5. Context Integration: Use context for cancellation signals

Testing Requirements

  • Test single start enforcement
  • Test error propagation via context
  • Test async initialization
  • Test cancellation handling
  • Test with mock components

Dependencies

Priority

High - Required for Component trait

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