-
Notifications
You must be signed in to change notification settings - Fork 1
Open
Description
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
- Async Start: Use async trait to allow async initialization
- Non-blocking: Start should spawn tasks and return quickly
- Error Handling: Critical errors use throw_irrecoverable
- Single Start: Panic on multiple start attempts
- 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
- async-trait (for async trait methods)
- tokio (for async runtime)
- Depends on: Implement ThrowableContext for Error Propagation #52 (ThrowableContext), Implement ReadyDoneAware Trait for Lifecycle State Management #53 (ReadyDoneAware)
Priority
High - Required for Component trait
Related Issues
- Depends on: Implement ThrowableContext for Error Propagation #52 (ThrowableContext)
- Blocks: Component trait implementation
Metadata
Metadata
Assignees
Labels
No labels