Skip to main content

Module events

Module events 

Source
Expand description

§Alien Events System

A Rust-based events system for user-facing events in Alien. This system is designed to handle events that can be consumed by dashboards, CLIs, and other monitoring tools to show users what’s happening during builds, deployments, and other operations.

§Why Not Use Traces?

This events system is not an alternative to traces - traces are for engineering observability and internal debugging, while this events module is about user-facing events that you show in UIs (either GUI or TUI). Key differences:

  • Reliable User Flow: Events are blocking (unlike non-blocking traces) because we want to guarantee users see critical progress updates and fail fast if we can’t communicate status
  • Rich UI Components: Events have strict schemas (see AlienEvent enum) enabling translation to custom React components, progress bars, and interactive UI elements that show meaningful progress
  • Flexible Granularity: Unlike traces (spans + events), we only have events, with infinite hierarchy and unique IDs. Users can choose their view: high-level “Building” → “Deploying” or drill down to see “Building image”, “Pushing image”, etc. Each event can be scoped for perfect user control over detail level
  • Live Progress Updates: Events can be updated with new information (e.g., “pushing image… layer 1/5, layer 2/5, layer 3/5”) enabling real-time progress indicators, which is impossible with immutable traces
  • Clear Success/Failure States: Each scoped event has explicit states (Started, Success, Failed) with detailed error information, giving users immediate feedback on what succeeded, what failed, and exactly why

§Key Features

  • Global Event Bus: Events can be emitted from anywhere in the code without passing around event bus instances, making it easy to add to large Rust workspaces with many crates.
  • Hierarchical Events: Events can have parent-child relationships for organizing complex operations.
  • State Management: Events can track their lifecycle (None, Started, Success, Failed).
  • Durable Execution Support: Designed to work with frameworks like Temporal, Inngest, and Restate where processes can restart and state needs to be preserved externally.
  • Change-based Architecture: Instead of storing events in memory, the system emits changes (Created, Updated, StateChanged) that handlers can persist or react to.
  • Macro Support: The #[alien_event] macro provides a convenient way to instrument functions with events, similar to tracing’s #[instrument] macro.

§Basic Usage

The easiest way to instrument functions with events is using the #[alien_event] macro:

use alien_core::{AlienEvent, EventBus, alien_event, Result};

#[alien_event(AlienEvent::BuildingStack { stack: "my-stack".to_string() })]
async fn build_stack() -> Result<()> {
    // All events emitted within this function will automatically be children
    // of the BuildingStack event. The event will be marked as successful
    // if the function returns Ok, or failed if it returns an error.
     
    AlienEvent::BuildingImage {
        image: "api:latest".to_string(),
    }
    .emit()
    .await?;
     
    Ok(())
}

let bus = EventBus::new();
bus.run(|| async {
    build_stack().await?;
    Ok(())
}).await?;

§Simple Event Emission

For more control, you can emit events manually:

use alien_core::{AlienEvent, EventBus};

let bus = EventBus::new();
bus.run(|| async {
    // Emit a simple event
    let handle = AlienEvent::BuildingStack {
        stack: "my-stack".to_string(),
    }
    .emit()
    .await?;
     
    println!("Emitted event with ID: {}", handle.id);
    Ok::<_, Box<dyn std::error::Error>>(())
}).await?;

§Event Updates

use alien_core::{AlienEvent, EventBus};

let bus = EventBus::new();
bus.run(|| async {
    // Emit an event and get a handle
    let handle = AlienEvent::BuildingImage {
        image: "api:latest".to_string(),
    }
    .emit()
    .await?;
     
    // Update the event with new information
    handle.update(AlienEvent::BuildingImage {
        image: "api:latest-v2".to_string(),
    }).await;
     
    // Mark as completed
    handle.complete().await;
    Ok::<_, Box<dyn std::error::Error>>(())
}).await?;

§Scoped Events with Automatic State Management

You can also use in_scope directly for more control over the event lifecycle:

use alien_core::{AlienEvent, EventBus, ErrorData, Result};
use alien_error::AlienError;

let bus = EventBus::new();
bus.run(|| async {
    // Use in_scope for automatic success/failure tracking
    // All events emitted within the scope automatically become children
    let result = AlienEvent::BuildingStack {
        stack: "my-stack".to_string(),
    }
    .in_scope(|_handle| async move {
        // This event will automatically be a child of BuildingStack
        AlienEvent::BuildingImage {
            image: "api:latest".to_string(),
        }
        .emit()
        .await?;
         
        // Do some work that might fail
        std::fs::create_dir_all("/tmp/build")
            .map_err(|e| AlienError::new(ErrorData::GenericError {
                message: e.to_string(),
            }))?;
         
        // Return success
        Ok::<_, AlienError<ErrorData>>(42)
    })
    .await?;
     
    println!("Operation completed with result: {}", result);
    Ok(())
}).await?;

§Event Hierarchy

use alien_core::{AlienEvent, EventBus, Result};

let bus = EventBus::new();
bus.run(|| async {
    let parent = AlienEvent::BuildingStack {
        stack: "my-stack".to_string(),
    }
    .emit()
    .await?;
     
    // Create a parent context for multiple child events
    parent.as_parent(|_handle| async {
        // All events emitted here will be children of the parent
        AlienEvent::BuildingImage {
            image: "api:latest".to_string(),
        }
        .emit()
        .await?;
         
        AlienEvent::PushingImage {
            image: "api:latest".to_string(),
            progress: None,
        }
        .emit()
        .await?;
         
        Ok(())
    }).await?;
     
    // Complete the parent when all children are done
    parent.complete().await;
    Ok(())
}).await?;

§Durable Execution Support

The events system is designed to work with durable execution frameworks where processes can restart and lose in-memory state. Instead of storing events in memory, the system emits changes that external handlers can persist.

§Manual State Management for Durable Workflows

use alien_core::{AlienEvent, EventBus, EventState, Result};

let bus = EventBus::new();
bus.run(|| async {
    // In a durable execution framework like Temporal:
     
    // Step 1: Start a long-running operation
    let parent_handle = AlienEvent::BuildingStack {
        stack: "my-stack".to_string(),
    }
    .emit_with_state(EventState::Started)
    .await?;
     
    // Step 2: Perform work across multiple durable steps
    parent_handle.as_parent(|_handle| async {
        // ctx.run(|| { ... }) - durable step 1
        AlienEvent::BuildingImage {
            image: "api:latest".to_string(),
        }
        .emit()
        .await?;
         
        // ctx.run(|| { ... }) - durable step 2
        AlienEvent::PushingImage {
            image: "api:latest".to_string(),
            progress: None,
        }
        .emit()
        .await?;
         
        Ok(())
    }).await?;
     
    // Step 3: Complete the operation
    parent_handle.complete().await;
    Ok(())
}).await?;

§The #[alien_event] Macro

The #[alien_event] macro provides a convenient way to instrument async functions with events, similar to how tracing’s #[instrument] macro works for logging. It automatically:

  • Creates an event when the function starts (with EventState::Started)
  • Establishes a parent context so all events emitted within the function become children
  • Marks the event as successful (EventState::Success) if the function returns Ok
  • Marks the event as failed (EventState::Failed) if the function returns an Err

§Basic Usage

use alien_core::{AlienEvent, alien_event, Result};

#[alien_event(AlienEvent::BuildingStack { stack: "my-stack".to_string() })]
async fn build_stack() -> Result<()> {
    // Function implementation
    Ok(())
}

§Dynamic Values

You can use function parameters and expressions in the event definition:

use alien_core::{AlienEvent, alien_event, Result};

#[alien_event(AlienEvent::BuildingStack { stack: format!("stack-{}", stack_id) })]
async fn build_dynamic_stack(stack_id: u32) -> Result<()> {
    // Function implementation
    Ok(())
}

§Comparison with Manual Event Management

The macro transforms this:

#[alien_event(AlienEvent::BuildingStack { stack: "my-stack".to_string() })]
async fn build_stack() -> Result<()> {
    // function body
    Ok(())
}

Into this:

async fn build_stack() -> Result<()> {
    AlienEvent::BuildingStack { stack: "my-stack".to_string() }
        .in_scope(|_event_handle| async move {
            // function body
            Ok(())
        })
        .await
}

§Limitations

  • The macro only works with async functions
  • For sync functions, use AlienEvent::emit() manually
  • The event expression is evaluated when the function is called, not when the macro is expanded

§Event Handlers

Event handlers receive changes and can persist them, update UIs, or trigger other actions:

use alien_core::{EventHandler, EventChange, EventBus, AlienEvent};
use async_trait::async_trait;

struct PostgresEventHandler {
    // database connection pool, etc.
}

#[async_trait]
impl EventHandler for PostgresEventHandler {
    async fn on_event_change(&self, change: EventChange) -> alien_core::Result<()> {
        match change {
            EventChange::Created { id, parent_id, created_at, event, state } => {
                // Insert new event record into database
                println!("Creating event {} with parent {:?}", id, parent_id);
            }
            EventChange::Updated { id, updated_at, event } => {
                // Update event data in database
                println!("Updating event {} at {}", id, updated_at);
            }
            EventChange::StateChanged { id, updated_at, new_state } => {
                // Update event state in database
                println!("Event {} state changed to {:?} at {}", id, new_state, updated_at);
            }
        }
        Ok(())
    }
}

let handler = std::sync::Arc::new(PostgresEventHandler {});
let bus = EventBus::with_handlers(vec![handler]);

bus.run(|| async {
    // Events will now be persisted to PostgreSQL
    AlienEvent::BuildingStack {
        stack: "my-stack".to_string(),
    }
    .emit()
    .await?;
    Ok::<_, Box<dyn std::error::Error>>(())
}).await?;

§Architecture Notes

§Change-Based Design

Unlike traditional event systems that store complete event state, this system emits incremental changes:

  • EventChange::Created: A new event was created with initial data and state
  • EventChange::Updated: An event’s data was updated
  • EventChange::StateChanged: An event’s state transitioned (None → Started → Success/Failed)

This design is crucial for durable execution where the event bus itself doesn’t persist state across process restarts.

§Task-Local Context

The event bus uses Tokio’s task-local storage to provide a global context without requiring explicit parameter passing. This makes it easy to add event emission to existing codebases. The #[alien_event] macro leverages this design to provide seamless instrumentation without requiring changes to function signatures.

§Error Handling

Event emission is designed to be non-blocking and fault-tolerant. If no event bus context is available, operations will return EventBusError::NoEventBusContext but won’t panic, allowing code to continue running even without event tracking.

Structs§

EventBus
The event bus for managing events within a task context
EventHandle
Handle to an emitted event that allows updating it
NoOpEventHandler
A no-op event handler for testing
PushProgress
Progress information for image push operations

Enums§

AlienEvent
Represents all possible events in the Alien system
EventChange
Represents a change to an event
EventState
Represents the state of an event

Traits§

EventHandler
Trait for handling events