Skip to content

feat(executor): cooperative single-thread lanes + generic config-driven lane routing#2

Merged
rlch merged 2 commits into
masterfrom
fix/cooperative-loro-lane
Jun 25, 2026
Merged

feat(executor): cooperative single-thread lanes + generic config-driven lane routing#2
rlch merged 2 commits into
masterfrom
fix/cooperative-loro-lane

Conversation

@rlch

@rlch rlch commented Jun 23, 2026

Copy link
Copy Markdown
Owner

Two related changes to the #[frb(thread = ...)] executor-routing feature.

1. Cooperative single-thread lanes (fix)

ThreadExecutor::execute_async drove each async task with futures::executor::block_on on the lane's single worker, so any .await inside a #[frb(thread = ...)] handler parked that worker until the future resolved — every other call on the same lane queued behind it. This is a native-only regression vs SimpleExecutor (spawns onto a multi-thread runtime) and the wasm lane (spawn_locals, yielding).

  • SingleThreadAsyncLane (native thread_pool/io.rs): one dedicated thread running a current_thread runtime + LocalSet, jobs run inside the LocalSet.
  • Native spawn_local_compat now tokio::task::spawn_locals the task future instead of block_on-ing it, so .await yields the lane while keeping single-thread FIFO ownership — matching wasm.

Regression test single_worker_lane_pinning_tests: two async tasks on one single-worker lane with a cross-task dependency — deadlocks under block_on, passes under cooperative scheduling.

2. Generic, config-driven lane routing (feat)

The feature baked a consumer's domain into FRB: Thread hardcoded Loro/Export, the attribute parser allowlisted those names, and validate_frb_threads hardcoded session_id/drive_id/… param names + a "sync or session-handle ⇒ must annotate" rule. FRB should know none of it.

  • Thread is now { Main, Worker(&'static str) } — the lane key is opaque; the embedder's ThreadRouter resolves it.
  • Attribute parser accepts any ident; codegen emits Thread::Main / Thread::Worker("<ident>").
  • New optional lane_routing config (flutter_rust_bridge.yaml): lanes (valid keys) + require_annotation_when { sync, param_named }. validate_frb_threads enforces it against config; absent config ⇒ no-op, so FRB ships with zero domain knowledge.
lane_routing:
  lanes: [Loro, Export]
  require_annotation_when:
    sync: true
    param_named: [session_id, drive_id, browser_id, wizard_id]

Tests

frb_rust 37/37; frb_codegen attribute + config-golden tests green. WASM unchanged. (Pre-existing oxidized MIR-golden drift that predates this branch is left untouched — unrelated to this change.)

🤖 Generated with Claude Code

rlch and others added 2 commits June 23, 2026 15:30
…lock_on

`ThreadExecutor::execute_async` drove each async task with
`futures::executor::block_on` on the lane's single worker thread, so any
`.await` inside a `#[frb(thread = ...)]` handler parked that worker until the
future resolved — every other call routed to the same lane queued behind it.
This is a native-only regression versus `SimpleExecutor` (which spawns onto a
multi-thread runtime) and versus the wasm lane (which `spawn_local`s onto the
JS event loop, yielding on `.await`).

Drive the lane cooperatively instead:

- Add `SingleThreadAsyncLane` (native `thread_pool/io.rs`): one dedicated
  thread running a `current_thread` runtime + `LocalSet`, fed jobs over a
  channel and running each inside the LocalSet context.
- Native `spawn_local_compat` now `tokio::task::spawn_local`s the task future
  instead of `block_on`-ing it, so `.await` yields the lane to other queued
  jobs while preserving single-thread FIFO ownership — matching the wasm lane.

WASM is unchanged. Adds `single_worker_lane_pinning_tests`, a regression test
that deadlocks under `block_on` and passes under cooperative scheduling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…omain lanes)

`#[frb(thread = ...)]` routing baked modality's domain into FRB: the `Thread`
enum hardcoded `Loro`/`Export` variants, the attribute parser allowlisted those
names, and `validate_frb_threads` hardcoded `session_id`/`drive_id`/... param
names + a "sync or session-handle ⇒ must annotate" rule. FRB should know none of
that — lane identity and policy belong to the embedder.

Make lanes opaque and the policy config-driven:

- `Thread` is now `{ Main, Worker(&'static str) }`. The lane key is an opaque
  string the embedder's `ThreadRouter` resolves; FRB assigns it no meaning.
- The attribute parser accepts any ident (no allowlist); codegen emits
  `Thread::Main` for `Main`/unset and `Thread::Worker("<ident>")` otherwise.
- New optional `lane_routing` config (`flutter_rust_bridge.yaml`): `lanes` (valid
  keys) + `require_annotation_when { sync, param_named }`. `validate_frb_threads`
  enforces it against config — erroring on an unknown lane or a function that
  matches the predicate but isn't annotated. Absent config ⇒ no-op (any key
  accepted, nothing required), so FRB ships with zero domain knowledge.

Runtime/doc comments de-Loro'd. WASM unchanged. Config golden fixtures updated
for the new `lane_routing` field (the unrelated `oxidized` MIR-golden drift that
predates this branch is left untouched).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rlch rlch changed the title fix(executor): drive native lane async tasks cooperatively, not via block_on feat(executor): cooperative single-thread lanes + generic config-driven lane routing Jun 23, 2026
@rlch
rlch merged commit db6361f into master Jun 25, 2026
12 of 115 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant