Skip to content

19 of the 29 remaining tokio edges are one problem: the decline path, and turnloop's Poster is the missing half #10395

Description

@proggeramlug

19 of the 29 remaining tokio edges in PR #10354 share one root cause, and it is not nineteen problems. Every one of them is the decline path: a thread that cannot get a turnloop loop of its own keeps the tokio implementation as a live fallback.

$ python3 scripts/tokio_inventory.py            # groups whose blocker names the decline
  B 8   the four database ext crates
  A 4   perry-ext-http / perry-ext-net
  D 2   http2 on a declining thread
  F 2   fastify's 'upgrade' case
  C 1   agent.rs
  E 1   perry-ext-ws
  G 1   stdlib fetch

Since turnloop P9 landed per-agent loops, the declining thread is no longer "a worker". The inventory's own wording: "a second thread acting for an agent another thread already owns (a host pump thread; Android's UI thread for perry-native)".

Why this is tractable, and mostly already built

crates/perry-runtime/src/event_pump/agent_loop.rs keeps a ROUTES registry, one Route per agent:

struct Route {
    agent: AgentId,
    owner: ThreadId,          // only this thread may own the agent's loop
    loop_id: u64,
    in_turn: Arc<AtomicBool>, // readable by a producer under the registry lock
    notifier: Option<Notifier>, // the loop's wake endpoint
}

A non-owner thread already finds another agent's route and wakes itwake_parked_agents_slow() walks ROUTES and calls notifier.notify() on every loop currently inside a turn. So the cross-thread path exists; it carries a wake and not work.

turnloop ships the missing half and Perry does not use it: Loop::poster() -> Poster, with

pub fn post(&self, token: Token, payload: Payload) -> Result<(), PostError>

whose contract is documented as "Returns ownership on full/closed. On Ok the payload is owned by this loop. A wake error after enqueue returns payload: None: the post was accepted and must not be retried." Poster is Send.

Why posting is safe here, which is the crux

A pump thread acting for agent X and the thread that owns agent X's loop are both serving agent X's heap. So a completion delivered on the owner is delivered where the JS values live — which is the invariant the whole design rests on. This is not the "route completions back across agents" idea that was rightly rejected for a shared loop; it is two threads serving one agent, which is what a pump thread already is.

What this does and does not solve

Centrally, adding poster: Option<Poster> to Route alongside notifier (set at the same place, agent_loop.rs:397) and turning claim_route's LoopState::Declined into "may post, may not own" is a contained change.

Per binding, it is not. The declining code paths do not submit one operation — they construct whole tokio-backed objects (a perry-ext-net socket task, a database connection, a hyper accept loop). Each binding has to route its operations through the owner's loop rather than build a tokio object. That is why every lane so far has correctly declined to touch it: the central enabler is small, the conversions are not.

Suggested order

  1. poster on Route, and a post_to_owning_agent(...) helper that returns a clear error when there is no route rather than silently declining.
  2. One binding converted end to end as the proof — perry-ext-net's socket task is the smallest and unblocks group A, which the others sit behind.
  3. Then B (8 edges) as the largest single win.

Acceptance for step 1

A test where thread A owns agent X's loop and thread B, acting for the same agent, posts an operation that completes on A — with the payload arriving exactly once, and a clean error (not a silent decline) when no route exists. Plus the existing wake_parked_agents behaviour unchanged, since it walks the same registry.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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