Add durable namespace control, Iroh-backed source transfer, session-independent publication lifetime, and wire managed Myelin jobs and bindings through the reusable data-plane API.
14 KiB
Data-plane SPSC stream MVP
Status
Pending implementation.
This document is the authoritative plan for the remaining data-plane stream slice. Blob namespace and resolution work is defined separately in DATA_PLANE_VIRTUAL_BLOB_NAMESPACE_MVP.md.
Goal
Complete the existing public stream API for one producer and one consumer:
reader = await ctx.data.read_stream(path)
async with ctx.data.write_stream(path) as writer:
await writer.write(data)
Swactor must resolve the logical path, match exactly one source with exactly one sink, provision the local arena rings and remote transport, preserve byte order, apply lossless backpressure, propagate terminal state, and clean up every binding.
The final Myelin acceptance path replaces the temporary Unix-socket result bridge with this implementation.
Scope
The MVP supports only:
- one producer;
- one consumer;
- ordered opaque bytes;
- lossless delivery while both endpoints remain live;
- explicit EOF and fault termination;
- bounded buffering and backpressure;
- actor-owned protocol state;
- arena-backed SPSC byte rings;
- local or Iroh-backed transfer chosen internally.
The MVP does not support:
- fanout or SPMC streams;
- multiple writers;
- topics or best-effort delivery;
- replay or resume;
- reconnecting an existing stream incarnation;
- application records, tensor schemas, JSON, dtype, or shape semantics;
- application-visible actors, nodes, edges, rings, sockets, or arena offsets.
Public semantics
A stream path names one SPSC rendezvous.
DataPath
-> waiting source or sink
-> matched source/sink incarnation
-> active transfer
-> EOF or fault
The stream is opaque ordered bytes. Application framing remains application code.
Each match creates one incarnation. A failed or closed incarnation is never silently resumed or joined to a replacement endpoint.
Writer
async with ctx.data.write_stream(path) as writer:
await writer.write(data)
- Entering the context resolves and authorizes the path, matches the sink, provisions the transfer, and waits for readiness.
write()completes only after Swactor has accepted the complete input into its bounded ordered pipeline.- Backpressure suspends
write()rather than dropping bytes. - A clean context exit publishes EOF exactly once.
- Exceptional exit publishes a fault or abort terminal state.
- Writes after terminal state fail typed.
Reader
reader = await ctx.data.read_stream(path)
chunk = await reader.read()
- Opening resolves and authorizes the path, matches the source, provisions the transfer, and waits for readiness.
- Reads observe source byte order.
- EOF is distinguishable from a zero-length temporary read.
- Source, transport, ring, and session faults become typed stream failures.
- Cancellation releases any held ring view and permits cleanup.
The precise Python read method shape may follow existing binding conventions, but it must not return arena, ring, edge, actor, or transport identities.
Namespace rendezvous
The centralized namespace service from DATA_PLANE_VIRTUAL_BLOB_NAMESPACE_MVP.md gains stream entries without changing blob semantics.
Conceptually:
enum NamespaceEntry {
Blob(BlobBinding),
Stream(StreamBinding),
}
enum StreamBinding {
WaitingSink {
sink: ActorAddress,
},
WaitingSource {
source: ActorAddress,
},
Matched {
incarnation: u64,
source: ActorAddress,
sink: ActorAddress,
coordinator: ActorAddress,
},
}
The directory actor serializes registration and matching. A path cannot simultaneously name a blob and a stream.
For the first Myelin flow, orchestration may register the result sink before the job opens its writer. The actor protocol should not require application-managed edge IDs or socket paths.
Existing foundation to retain
crates/data-plane/src/byte_ring.rs already provides the correct stream storage foundation:
- arena-resident SPSC header and data region;
- release/acquire
commitandconsumecursors; - generation and producer/consumer role enforcement;
Data,Eof, andFaultrecord framing;- lossless bounded backpressure;
- ordering and wraparound handling;
- corruption containment.
Retain its valid guarantee tests.
Also retain:
- one inherited arena descriptor;
- automatically driven child runtime;
- persistent host and child data-plane session actors;
- one operation actor per public open;
- one host binding/coordinator actor per match;
- existing actor routing and Iroh transport;
- the rule that protocol state lives in actor FSM fields rather than mutex-protected pending maps.
Required byte-ring correction
The current consuming path returns owned bytes through recv_record() -> Vec<u8>. That is not zero-copy.
Add a pinned borrowed record view:
struct PinnedRecordView {
endpoint_guard: Arc<...>,
generation: u64,
kind: RecordKind,
first_span: ...,
second_span: Option<...>,
}
A record may wrap around the ring and therefore expose two ordered spans. The view pins the committed range. The consumer cursor advances only after the view is released or explicitly consumed.
Required behavior:
producer commits record
-> consumer receives pinned view
-> transport/Python consumes one or two spans
-> view release publishes consume cursor
-> producer capacity becomes available
Holding a view intentionally applies backpressure.
No hot-path stream operation should assemble a record into Vec<u8> merely to cross the host/child boundary or feed the transport.
Actor notification model
Actor messages replace eventfd wakeups and polling.
Messages indicate that protocol state may have advanced; ring cursors remain authoritative.
Conceptually:
producer commits data
-> ProgressAvailable notification
-> consumer checks committed cursor
consumer advances consume cursor
-> CapacityAvailable notification
-> producer retries pending reservation
Notifications may be coalesced or redundant. Correctness must not depend on wake counts.
Each operation actor owns its pending action in FSM fields. Do not add a shared request-ID map or protocol mutex.
Standard happy path
Sink registration
-> namespace records waiting sink
Python write_stream(path)
-> PyDataPlane delegates to general DataPlane
-> ChildDataPlaneSessionActor spawns WriteStreamOperationActor
-> host session authorizes canonical path
-> namespace matches source operation with waiting sink
-> StreamCoordinatorActor is created
Coordinator
-> resolves persistent endpoint actors to nodes
-> allocates and installs arena SPSC rings
-> establishes local or Iroh transfer
-> waits for source and sink readiness
-> sends Opened to both operation paths
Python writer.write(bytes)
-> reserves ring capacity
-> writes bytes into producer spans
-> release-commits Data record
-> actor notification drives downstream progress
-> transport consumes pinned source record
-> destination producer commits to consumer ring
-> reader consumes bytes in order
-> consume progress releases backpressure
Clean writer exit
-> commits EOF once
-> reader observes all preceding bytes, then EOF
-> coordinator quiesces pumps
-> rings are uninstalled and leases released
-> namespace incarnation terminates
Backpressure
Backpressure is end-to-end and lossless:
slow reader
-> destination ring fills
-> network receive stops advancing
-> source transport stops draining
-> source ring fills
-> writer.write awaits capacity
No layer may drop bytes, overwrite unread bytes, busy-spin, or create an unbounded overflow queue.
A held reader or transport view is part of the backpressure mechanism and must keep the relevant range pinned.
Ordering
For one stream incarnation:
- producer writes have one total order;
- the consumer observes exactly that order;
- EOF follows every successfully accepted data byte;
- a fault terminates the incarnation and no later data is delivered;
- transport chunk boundaries are not application-visible stream boundaries.
The existing Iroh QUIC byte stream already provides remote ordered delivery. Swactor must not implement another TCP-like acknowledgement, retransmission, congestion-control, or reorder protocol.
Terminal behavior
Each incarnation has exactly one terminal result:
EOF
Fault
Cancelled
SessionClosed
Terminal state is sticky. Duplicate or late terminal messages are ignored after the first accepted terminal transition.
A clean writer close produces EOF. Writer failure, transport failure, ring corruption, endpoint death, or exceptional writer exit produces a fault. Cancellation and session shutdown reclaim resources without presenting successful EOF.
Python binding boundary
General functionality belongs in crates/data-plane:
- stream reader and writer types;
- operation actors;
- pinned ring record views;
- ring lifetime guards;
- backpressure and terminal FSMs;
- namespace and transfer protocols.
Python owns only:
PyStreamReaderandPyStreamWriterwrappers;- Rust-future to awaitable conversion;
- buffer exposure where used;
- async context-manager behavior;
- typed exception conversion.
Python must not manually tick the runtime or poll shared cursors.
Myelin migration
The current result path uses a temporary Unix-domain socket exposed through legacy_output and SWACTOR_DATA_PLANE_OUTPUT.
After actor-driven streams satisfy the acceptance path:
- Register the Myelin result sink at
/runs/<run-id>/results/inference. - Let
/runs/self/results/inferenceresolve throughJobContext. - Route
ctx.data.write_stream(...)through the SPSC stream implementation. - Remove the Unix listener, socket path, legacy environment variable, connector, and compatibility code.
- Preserve application-owned JSON/result framing.
- Rerun the complete Tinygrad CUDA scenario.
Expected result:
CUDA -> [2.75, -8.75]
Failure behavior
| Failure | Required behavior |
|---|---|
| Path absent or unmatched under chosen open policy | Typed path/rendezvous failure or pending operation, never fallback socket |
| Unauthorized path | Fail before ring or edge provisioning |
| Duplicate source or sink | Typed namespace conflict |
| Arena exhaustion | Fail both sides and release partial provisioning |
| Ring generation mismatch or corruption | Fault incarnation and stop using the ring |
| Iroh stream fault | Fault both endpoint operations |
| Writer exception | Publish fault/abort, never clean EOF |
| Reader or writer cancellation | Release held views and terminate binding |
| Child or host session death | Stop bindings, quiesce pumps, reclaim leases |
| EOF | Deliver all prior bytes, then complete reader exactly once |
Behavioral invariants
- One stream incarnation has exactly one producer and one consumer.
- A live path cannot simultaneously name a blob and a stream.
- Neither endpoint receives
Openedbefore required rings and transport are ready. - Successfully accepted bytes are delivered exactly once and in order while both endpoints remain live.
- The producer never overwrites unread bytes.
- The consumer never observes uncommitted bytes.
- A pinned record prevents its range from being consumed or reused.
- Backpressure is bounded and lossless.
- EOF follows all accepted data and appears exactly once.
- A fault prevents subsequent data or EOF success.
- Every incarnation reaches exactly one terminal outcome.
- Every arena lease is released only after pumps and views are quiescent.
- Wake notifications are hints; ring state is authoritative.
- Python never observes actor, node, edge, ring, socket, or arena identities.
- Swactor never interprets application byte schemas.
Implementation sequence
- Extend the centralized namespace with SPSC stream source/sink registration and atomic matching.
- Define shared stream session, operation, coordinator, progress, and terminal messages.
- Implement pinned borrowed byte-ring record views with wraparound spans.
- Add actor-driven capacity and data-progress notifications.
- Implement general
StreamReaderandStreamWriteroperation actors incrates/data-plane. - Provision local arena rings and Iroh edges through one coordinator FSM.
- Add thin Python reader/writer wrappers and async context behavior.
- Add focused tests for ordering, backpressure, wraparound, held views, EOF, faults, cancellation, and lease cleanup.
- Substitute the SPSC path for Myelin's temporary Unix result bridge.
- Remove all legacy stream socket and environment plumbing.
- Run affected Rust and Python suites.
- Exercise the full Tinygrad CUDA scenario and verify the deterministic result.
Verification
Focused behavioral proof must cover:
- source-first or sink-first rendezvous behavior selected for the MVP;
- concurrent independent SPSC paths;
- exact byte ordering through wraparound;
- writer suspension and resumption under backpressure;
- no consume advancement while a view is pinned;
- EOF after all prior data;
- fault dominance over later progress;
- cancellation and session-failure reclamation;
- no stream payload assembly into
Vec<u8>on zero-copy host/child paths; - absence of the legacy Unix stream bridge after migration;
- the complete Myelin CUDA result path.
Affected suites include:
cargo test -p data-plane
cargo test -p myelin --lib
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 cargo check -p python
Python binding tests run after rebuilding the extension from crates/bindings/python.
Final boundary
The remaining stream MVP is intentionally narrow:
One logical path rendezvous-matches one writer with one reader. Swactor provisions ordered bounded SPSC byte movement, uses actor messages for progress, applies lossless backpressure, propagates EOF or faults, and removes the temporary Unix bridge without exposing transport mechanics to applications.