Skip to content

Ask pattern: awaiting the outcome of an event chain (send_and_await) #114

Description

@ddrcode

Motivation

First real-world use of maiko 0.3.1 (an orchestration tool dispatching containerised coding-agent runs; a runner and a reporter actor over topic routing) hit one place where the model pushed back: the application entry point needs the outcome of a run it requested — emit RunRequested, obtain RunCompleted/RunFailed in the caller.

Workarounds tried, and why they disappoint:

  • Smuggling a tokio::sync::mpsc sender into an actor's factory closure. Works, but it is hand-wired channel plumbing — the exact species maiko exists to eliminate — and forms a parallel subsystem next to the broker.
  • oneshot::Sender inside the event payload (the classic Actix/Erlang reply-to idiom). Blocked by maiko's own commitments — arguably correctly:
    • Event: Clone (needed for fan-out) vs oneshot::Sender: !Clone. Events are facts (cloneable, many readers); replies are capabilities (linear, exactly one consumer). Forcing a capability into a fact-type requires Arc<Mutex<Option<…>>> with subscribers racing to .take().
    • It poisons observability: with the serde/recorder features, events are recordable and replayable; a live channel in a payload is neither.

Proposal

Keep the flow unidirectional by reframing: a response is not a reply — it is a fact someone awaits. The runtime already tracks causality (parent IDs, send_child_event); the missing piece is an awaitable, causality-scoped query over the stream:

let envelope = sup
    .send_and_await(MyEvent::RunRequested { config }, |e| {
        matches!(e, MyEvent::RunCompleted { .. } | MyEvent::RunFailed { .. })
    })
    .await?;

Semantics: publish the event normally; resolve on the first event matching the predicate whose ancestry includes the sent event's id. Internally this is an ephemeral filtered subscriber — Akka's ask pattern via temporary actor, but keyed on the existing causality chain instead of an address. The outcome event remains an ordinary event: journaled, routed, observable by all other subscribers.

Design notes / open questions

  • Timeout: either a parameter or caller-side tokio::time::timeout as the documented pattern.
  • The mechanism depends on handlers linking chains via send_child_event; worth stating as a contract in its docs.
  • Composes with actor-death observability: if the handling actor stops, the await should fail fast rather than hang — an actor-terminated event participating in the chain would give this for free (candidate for a separate issue).
  • A Context-level variant (ctx.send_and_await) may be wanted eventually; the supervisor-level verb covers the entry-point and testing cases that motivated this.

Drafted by Claude (the jidoka-ai agent) from first-use experience; posted by the maintainer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions