Skip to content

Cross-subsystem bridging via build_bridge #102

Description

@ddrcode

Status

This is currently a feature proposal open for discussion. The concept (cross-supervisor communication) is needed, but it can be implemented multiple way. This is a proposal of one of them.

Summary

Support multiple in-process subsystems with distinct event/topic types, connected via bridge actors. Each subsystem runs its own Supervisor<T>, and a bridge actor forwards events from one subsystem to another.

Motivation

Some applications have naturally distinct event domains - e.g., a USB subsystem and a UI subsystem - that need to exchange information without sharing a single event type. Today the only option is nesting one supervisor inside an actor of another, which creates an artificial hierarchy and awkward lifecycle coupling.

The goal is to support equal, peer subsystems with a minimal API surface, reusing existing concepts (actors, contexts, subscriptions, backpressure) rather than introducing new runtime primitives.

Proposed Design

API

// Two independent supervisors with their own event/topic types
let mut sup_a = Supervisor::<TopicA>::default();
let mut sup_b = Supervisor::<TopicB>::default();

// Bridge: an actor in A that can produce into B
sup_a.build_bridge("Bridge", &sup_b, |ctx, ext_ctx| ABBridge::new(ctx, ext_ctx))
    .subscribe(&[TopicA::SomeTopic])
    .build()?;

Bridge actor

The bridge is a regular actor in supervisor A. The user controls event conversion - the framework doesn't impose any mapping:

struct ABBridge {
    ctx: Context<EventA>,
    ext_ctx: ProducerContext<EventB>,
}

impl Actor for ABBridge {
    type Event = EventA;

    async fn handle_event(&mut self, envelope: &Envelope<Self::Event>) -> Result {
        let event_b: EventB = envelope.event().into(); // user-defined conversion
        self.ext_ctx.send(event_b).await
    }
}

ProducerContext<E>

A new narrow type - essentially a wrapped Sender<Arc<Envelope<E>>> plus an ActorId for stamping envelopes. It exposes only:

  • send(event) - send an event into the external subsystem
  • send_child_event(event, parent_id) - send with causality tracking
  • is_sender_full() - check backpressure on the external channel

Notably excluded: stop() (no actor to stop in B), stop_runtime() (too dangerous to expose implicitly - users who need cross-subsystem shutdown can hold a supervisor handle directly).

Key properties

  • Direction is explicit. A→B and B→A are separate bridge actors with separate subscriptions. Asymmetric bridging is natural.
  • Backpressure propagates. The bridge participates in B's backpressure as a producer. If B is overloaded, the bridge slows down, which slows A's processing of bridged topics.
  • Identity is visible on both sides. The bridge actor name is validated for uniqueness in both supervisors. Events arriving in B carry the bridge's ActorId, making them identifiable in monitoring and test harnesses.
  • No new runtime machinery. The bridge is a regular actor in A. It gets lifecycle hooks, monitoring, subscriptions, and graceful shutdown for free.
  • Event conversion is user-controlled. The framework doesn't require From<EventA> for EventB - users implement whatever mapping makes sense.

Alternatives considered

  • Shared event bus: Loses type safety, forces a single event type across subsystems.
  • Supervisor hierarchy (nesting): Creates artificial parent-child relationship, complicates lifecycle.
  • Standalone Bridge type (not owned by either supervisor): Loses actor lifecycle benefits - no subscriptions, no backpressure, no monitoring, no graceful shutdown.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions