Skip to content

feat(core): shared worker protocol and RemoteDispatcher - #583

Merged
pratyush618 merged 15 commits into
masterfrom
feat/executor-attach-s2-protocol
Jul 31, 2026
Merged

feat(core): shared worker protocol and RemoteDispatcher#583
pratyush618 merged 15 commits into
masterfrom
feat/executor-attach-s2-protocol

Conversation

@kartikeya-27

@kartikeya-27 kartikeya-27 commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

First two phases of Executor Attach (#546): the shared wire format, and the dispatcher that speaks it over a socket. Pure Rust in taskito-core plus the prefork migration — no server binary, no listener, no SDK executors, no dashboard.

Closes #547. Closes #548.

#547 — shared worker protocol

crates/taskito-core/src/worker/protocol.rs now owns the frames; crates/taskito-python/src/prefork/protocol.rs is gone and prefork runs on the shared module.

A frame is a JSON header line followed by exactly the raw payload bytes it declares, replacing base64-in-JSON. Over a socket that base64 was a permanent 33% size tax plus encode/decode on every payload and result; raw bytes also mean the blob on the wire is the CBOR wire-envelope bytes the contract describes. base64 drops out of crates/taskito-python/Cargo.toml entirely.

Prefork's one-way ready signal is replaced by the same hello/hello_ack an attached executor uses, so there is one handshake rather than two. protocol_version rides on both frames — the issue's table put it only on the ack, but one-sided validation lets a mismatched peer stay attached until it happens to notice. The ack is sent even when rejecting, so both ends can log both versions. This is not hypothetical: TASKITO_PYTHON lets the prefork child run from a different interpreter, so a version-mismatched install is reachable.

Python side gains sdks/python/taskito/worker_protocol.py — transport-agnostic, so the socket executor reuses it — reading the version from _taskito.WORKER_PROTOCOL_VERSION rather than mirroring a literal.

#548 — RemoteDispatcher + Transport

transport.rs (UDS, TCP, in-memory) and remote.rs (RemoteDispatcher + executor registry). Binding and accepting stay with the caller — attach() takes an already-connected transport — so the listener and its auth defaults land in a later phase without this one guessing at them.

Jobs go only to executors that advertised the task name in hello. Free-slot counts are the only limiter, with capacity() exposed so the server phase can size SchedulerConfig::max_in_flight from it instead of running a parallel limiter. A job nobody advertises fails retryably after placement_timeout — it reschedules under the normal retry policy and surfaces the misconfiguration rather than stalling silently.

An executor that drops mid-job gets no synthesised result: it may have run, so recovery stays with the scheduler's reaper, matching prefork's existing behaviour.

Worker::dispatcher(pool_type, …) is the one change outside the new modules — it makes the reaper path testable end to end, and the server phase needs it regardless. The registry reports the supplied pool_type so it never claims native for a pool that isn't.

Two bugs the design invites

Both were found by noticing every remote test took exactly 10.00s — the default handshake_timeout. Both are fixed, and each regression test was verified to fail without its fix.

  1. The handshake read timeout leaked onto the attached connection, so every executor would have been dropped once it idled past it. Transport::split now returns a Connection control that outlives the split; attach bounds the hello read, then clears it.
  2. Shutdown joined reader threads parked on a blocking read, so an executor that stopped responding hung the worker forever — bug 1's timeout had been masking this. Connection::close() after a bounded shutdown_drain now bounds it.

Separately, place() pins and enables the Notify::notified() future before checking capacity: notify_waiters() only wakes already-registered waiters, so subscribing lazily loses a slot freed in the check-then-await window.

Other SDKs

None needed. Node and Java never use taskito_core::Worker, have no prefork or out-of-process pool, and reference nothing in the new modules. Their executor subcommands are separate phases and will build against the contract section added here.

Verification

  • cargo test --workspace green; every commit compiles standalone
  • cargo clippy --workspace --all-targets clean; --features postgres|redis|native-async all check
  • pyo3 tripwire clean on core/workflows/mesh; scripts/version.mjs --check passes
  • Python: full suite 1328 passed / 7 skipped; ruff + mypy clean
  • Prefork behaviour unchanged, plus two tests for paths the suite missed — a binary payload containing newlines (the real regression test for raw framing) and a None-returning task

Summary by CodeRabbit

  • New Features

    • Added support for remote worker executors over TCP, Unix sockets, and in-memory connections.
    • Added executor registration, capacity reporting, job dispatch, cancellation, retries, and graceful shutdown handling.
    • Added configurable worker dispatchers and Python worker-protocol utilities with version negotiation and binary payload support.
    • Exposed the worker protocol version for Python integrations.
  • Improvements

    • Prefork execution now supports binary results, including non-UTF-8 data and empty results, without base64 encoding.
  • Documentation

    • Documented worker framing, compatibility rules, size limits, and prefork behavior.

The module became a directory; both references still pointed at the old file.
One JSON header line then the payload's raw bytes, so the same format serves a pipe and a socket and the blob stays the wire-envelope bytes. Closes #547.
Replaces the base64-in-JSON frames and the one-way ready signal with raw payload bytes and a versioned hello/hello_ack, so a mismatched TASKITO_PYTHON interpreter fails closed instead of silently.
The registry reports the supplied pool_type so it never claims 'native' for a pool that isn't.
split() hands back the read/write halves plus a Connection control, because the read timeout and the close both have to outlive the split.
Dispatches only task names an executor advertised in hello; a dropped executor leaves its in-flight jobs to the scheduler's reaper. Closes #548.
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@pratyush618, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 13 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 134187c3-b588-498d-9eaf-d96870e27afc

📥 Commits

Reviewing files that changed from the base of the PR and between 4e1882c and 6d405b6.

📒 Files selected for processing (8)
  • crates/taskito-core/src/lib.rs
  • crates/taskito-core/src/worker/protocol.rs
  • crates/taskito-core/src/worker/remote.rs
  • crates/taskito-core/src/worker/remote_tests.rs
  • crates/taskito-core/src/worker/transport.rs
  • crates/taskito-python/src/prefork/child.rs
  • sdks/python/taskito/worker_protocol.py
  • sdks/python/tests/worker/test_worker_protocol.py
📝 Walkthrough

Walkthrough

The PR adds a shared worker frame protocol with binary payload support, transport abstractions, remote executor dispatch, configurable worker dispatchers, and Python prefork integration. It adds protocol, transport, remote-dispatch, and binary-payload tests.

Changes

Worker execution infrastructure

Layer / File(s) Summary
Shared protocol contract
crates/taskito-core/src/worker/protocol.rs, crates/taskito-core/BINDING_CONTRACT.md, crates/taskito-core/src/lib.rs, crates/taskito-core/src/worker/mod.rs
Defines versioned JSON-header framing, raw payload limits, scheduler and executor messages, result conversion, handshake rules, and validation tests.
Remote transport and dispatch
crates/taskito-core/src/worker/transport.rs, crates/taskito-core/src/worker/remote.rs, crates/taskito-core/src/worker/remote_tests.rs
Adds Unix, TCP, and in-memory transports. RemoteDispatcher attaches executors, tracks capacity, dispatches jobs, forwards cancellation, handles disconnects, and drains during shutdown.
Configurable worker dispatcher
crates/taskito-core/src/worker/runner.rs
Allows workers to use a custom WorkerDispatcher and reports its configured pool type.
Python prefork protocol migration
crates/taskito-python/src/prefork/*, crates/taskito-python/src/lib.rs, sdks/python/taskito/prefork/child.py, sdks/python/taskito/worker_protocol.py, sdks/python/taskito/_taskito.pyi, sdks/python/tests/worker/*, docs/content/docs/python/guides/advanced-execution/prefork.mdx
Replaces JSONL and base64 IPC with shared framed messages, binary payloads, versioned handshakes, and result-length semantics. Adds binary round-trip and None-result tests.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

Suggested reviewers: pratyush618

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the shared worker protocol and RemoteDispatcher, which are the primary changes.
Linked Issues check ✅ Passed The changes satisfy the shared protocol and prefork migration requirements [#547] and the transport, registry, routing, capacity, and recovery requirements [#548].
Out of Scope Changes check ✅ Passed The reviewed changes support the linked protocol, transport, dispatcher, prefork, compatibility, and test objectives without unrelated code changes.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown
✅ Action performed

Full review finished.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 7

🧹 Nitpick comments (5)
crates/taskito-core/src/worker/runner.rs (1)

115-131: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Warn at spawn when handlers are registered alongside a custom dispatcher.

The doc comment states that registered handlers are unused with a custom dispatcher. spawn then drops registry silently, because only the unwrap_or_else branch consumes it. A caller who uses both .register(...) and .dispatcher(...) gets tasks that never run and no diagnostic. A log line at spawn makes the misconfiguration visible.

If TaskRegistry exposes a length or emptiness accessor, add this in spawn after the dispatcher is resolved:

if pool_type != "native" && !registry.is_empty() {
    log::warn!(
        "[taskito] {} handler(s) are registered but a custom '{pool_type}' dispatcher is \
         configured; the registered handlers will not run",
        registry.len(),
    );
}

Note that registry is moved into NativeDispatcher::new in the default branch, so the check must read it before the unwrap_or_else runs, or the branch must be restructured to an explicit match.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/taskito-core/src/worker/runner.rs` around lines 115 - 131, Update
spawn’s dispatcher-resolution logic to warn when a non-native custom dispatcher
is configured alongside registered handlers. Check registry emptiness and length
before registry is moved into NativeDispatcher::new, using an explicit match if
needed, and emit the specified warning with the pool type and handler count.
crates/taskito-core/src/worker/transport.rs (2)

174-199: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document that close on a memory transport is one-directional.

Connection::close on a socket transport calls shutdown(Both), so both peers observe the teardown. On MemoryTransport it calls close_reader on incoming only. The peer end keeps its reader blocked and never sees EOF. For an embedded in-process executor this means it will not learn that the scheduler closed the connection. The current dispatcher use in drain_and_close only needs the local reader to return, so behavior is correct today. Consider closing outgoing as well, or state the limitation in the doc comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/taskito-core/src/worker/transport.rs` around lines 174 - 199, Document
in the `MemoryTransport` implementation that `Connection::close` only closes the
local `incoming` reader, unlike socket shutdown, so the peer may remain blocked
without observing EOF. Keep the current `drain_and_close` behavior unchanged
unless the implementation is explicitly updated to close `outgoing` as well.

314-379: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a test for Connection::close unblocking a parked reader.

The tests cover byte transfer, EOF on writer drop, and read timeouts. They do not cover Connection::close. That path is the mechanism RemoteDispatcher::drain_and_close depends on to bound shutdown. A direct unit test here would catch a regression in Channel::close_reader without relying on the dispatcher-level test.

💚 Suggested test
#[test]
fn closing_the_connection_unblocks_a_parked_reader() {
    let (a, b) = MemoryTransport::pair();
    let (_a_read, _a_write, _a_conn) = Box::new(a).split().expect("split a");
    let (mut b_read, _b_write, b_conn) = Box::new(b).split().expect("split b");

    let reader = std::thread::spawn(move || {
        let mut buf = [0u8; 1];
        b_read.read(&mut buf)
    });
    std::thread::sleep(Duration::from_millis(20));
    b_conn.close();
    assert_eq!(reader.join().expect("reader thread").expect("read"), 0);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/taskito-core/src/worker/transport.rs` around lines 314 - 379, Add a
unit test alongside the existing MemoryTransport tests that splits a pair, parks
a reader thread on the read half, calls the corresponding Connection::close
method, and asserts the reader unblocks with a zero-byte read. Use the split
connection handle and a short synchronization delay, preserving the existing
test style.
crates/taskito-core/src/worker/remote.rs (1)

515-589: 🩺 Stability & Availability | 🔵 Trivial

idle_ms is recorded but never enforced.

last_seen_ms is updated on every frame and surfaced as AttachedExecutor::idle_ms, but nothing in the dispatcher acts on it. A half-open connection (peer host lost, no FIN) keeps the executor registered with its slots advertised. place will keep selecting it, and each write_job succeeds into the kernel buffer until TCP eventually gives up. Jobs dispatched in that window are only recovered by the scheduler's stale-job sweep.

A follow-up liveness reaper — deregister and connection.close() any executor whose idle_ms exceeds a configured multiple of the heartbeat interval — would close this gap. TCP keepalive on TcpTransport would also shorten detection.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/taskito-core/src/worker/remote.rs` around lines 515 - 589, Add
liveness enforcement for executors tracked by the dispatcher: periodically
inspect each executor’s last-seen time and, when its idle duration exceeds the
configured heartbeat-based threshold, deregister it and close its connection.
Integrate this reaper with the existing executor lifecycle and ensure stale
executors no longer remain eligible for job placement.
crates/taskito-core/BINDING_CONTRACT.md (1)

99-138: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a language to the frame-example fence.

The frame illustration in the fenced code block has no language identifier, unlike the equivalent example in worker/protocol.rs's module doc, which uses ```text. This triggers a markdownlint MD040 warning.

📝 Proposed fix
-```
+```text
 {"type":"job","id":"018f…","task_name":"resize","payload_len":7,…}\n
 <7 raw bytes>
</details>

Aside from this, the frame table and rules match the `SchedulerMessage`/`ExecutorMessage` definitions and limits in `worker/protocol.rs` field-for-field.

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @crates/taskito-core/BINDING_CONTRACT.md around lines 99 - 138, Update the
frame illustration fenced code block in the Worker frame protocol section of
BINDING_CONTRACT.md to include the text language identifier, matching the
equivalent worker/protocol.rs example; leave the example contents unchanged.


</details>

<!-- cr-comment:v1:42475594243691fca336cf16 -->

_Source: Linters/SAST tools_

</blockquote></details>

</blockquote></details>

<details>
<summary>🤖 Prompt for all review comments with AI agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @crates/taskito-core/src/lib.rs:

  • Around line 49-51: Re-export Capacity from the crate root alongside
    AttachedExecutor, RemoteConfig, and RemoteDispatcher in the public export list,
    so consumers can name the return type of RemoteDispatcher::capacity() without
    accessing the worker module directly.

In @crates/taskito-core/src/worker/protocol.rs:

  • Around line 369-381: Update read_header_line to distinguish an unterminated
    header caused by EOF from one that consumed MAX_HEADER_BYTES: return
    ProtocolError::HeaderTooLarge only when the byte count reaches the cap, and map
    a shorter partial header to ProtocolError::Io. Add a regression test alongside
    truncated_payload_is_an_error_not_a_clean_eof covering a source closed after a
    partial header without a newline and asserting the Io variant.

In @crates/taskito-core/src/worker/remote.rs:

  • Around line 452-479: Update dispatch_to so the blocking writer.write_job call
    cannot indefinitely occupy a Tokio runtime worker: either configure and apply a
    bounded socket write timeout through Connection, move each executor’s writes
    behind a bounded per-executor writer queue/thread, or execute the write via
    tokio::task::spawn_blocking with the existing failure cleanup preserved. Ensure
    dispatch and the cancel router remain responsive when an executor stops reading.
  • Around line 366-376: Update attach to reject new connections once shutdown has
    started by adding a shutdown-related AttachError variant and checking
    self.shutdown while holding the executors lock immediately before inserting the
    executor. Preserve normal attachment behavior before shutdown, and ensure the
    check is inside the same critical section as the registry insert to prevent
    attachments racing with drain_and_close.
  • Around line 696-699: Make drain_and_close asynchronous and replace the
    blocking std::thread::sleep polling with an awaited async delay while retaining
    the deadline and Executor::is_busy conditions. Update its call site in the async
    run method to await drain_and_close, leaving the bounded handle.join calls
    unchanged.
  • Around line 310-312: Update the reader attachment flow around spawn_reader and
    self.readers so it removes finished JoinHandles before pushing the newly spawned
    handle. Retain active handles, then notify capacity changes as before, ensuring
    repeated reconnects do not grow self.readers indefinitely.

In @crates/taskito-python/src/prefork/child.rs:

  • Around line 90-121: Update the handshake failure paths in spawn_child to
    explicitly terminate and reap the Child process before returning Err, including
    read/decode failures, non-Hello messages, acknowledgment write failures, and
    protocol-version mismatches. Ensure cleanup occurs for every error branch while
    preserving the existing error messages and successful return path.

Nitpick comments:
In @crates/taskito-core/BINDING_CONTRACT.md:

  • Around line 99-138: Update the frame illustration fenced code block in the
    Worker frame protocol section of BINDING_CONTRACT.md to include the text
    language identifier, matching the equivalent worker/protocol.rs example; leave
    the example contents unchanged.

In @crates/taskito-core/src/worker/remote.rs:

  • Around line 515-589: Add liveness enforcement for executors tracked by the
    dispatcher: periodically inspect each executor’s last-seen time and, when its
    idle duration exceeds the configured heartbeat-based threshold, deregister it
    and close its connection. Integrate this reaper with the existing executor
    lifecycle and ensure stale executors no longer remain eligible for job
    placement.

In @crates/taskito-core/src/worker/runner.rs:

  • Around line 115-131: Update spawn’s dispatcher-resolution logic to warn when a
    non-native custom dispatcher is configured alongside registered handlers. Check
    registry emptiness and length before registry is moved into
    NativeDispatcher::new, using an explicit match if needed, and emit the specified
    warning with the pool type and handler count.

In @crates/taskito-core/src/worker/transport.rs:

  • Around line 174-199: Document in the MemoryTransport implementation that
    Connection::close only closes the local incoming reader, unlike socket
    shutdown, so the peer may remain blocked without observing EOF. Keep the current
    drain_and_close behavior unchanged unless the implementation is explicitly
    updated to close outgoing as well.
  • Around line 314-379: Add a unit test alongside the existing MemoryTransport
    tests that splits a pair, parks a reader thread on the read half, calls the
    corresponding Connection::close method, and asserts the reader unblocks with a
    zero-byte read. Use the split connection handle and a short synchronization
    delay, preserving the existing test style.

</details>

<details>
<summary>🪄 Autofix (Beta)</summary>

Fix all unresolved CodeRabbit comments on this PR:

- [ ] <!-- {"checkboxId": "4b0d0e0a-96d7-4f10-b296-3a18ea78f0b9"} --> Push a commit to this branch (recommended)
- [ ] <!-- {"checkboxId": "ff5b1114-7d8c-49e6-8ac1-43f82af23a33"} --> Create a new PR with the fixes

</details>

---

<details>
<summary>ℹ️ Review info</summary>

<details>
<summary>⚙️ Run configuration</summary>

**Configuration used**: Organization UI

**Review profile**: CHILL

**Plan**: Pro Plus

**Run ID**: `eaad64c7-6191-48a2-8020-6f11c3fb0fbb`

</details>

<details>
<summary>📥 Commits</summary>

Reviewing files that changed from the base of the PR and between 5508015902c6ab4ae31d6a90bff8c58ed2c3f274 and 4e1882c5fb42d02be8a8c9a510ef1f47233355e7.

</details>

<details>
<summary>⛔ Files ignored due to path filters (1)</summary>

* `Cargo.lock` is excluded by `!**/*.lock`

</details>

<details>
<summary>📒 Files selected for processing (19)</summary>

* `crates/taskito-core/BINDING_CONTRACT.md`
* `crates/taskito-core/src/lib.rs`
* `crates/taskito-core/src/worker/mod.rs`
* `crates/taskito-core/src/worker/protocol.rs`
* `crates/taskito-core/src/worker/remote.rs`
* `crates/taskito-core/src/worker/remote_tests.rs`
* `crates/taskito-core/src/worker/runner.rs`
* `crates/taskito-core/src/worker/transport.rs`
* `crates/taskito-python/Cargo.toml`
* `crates/taskito-python/src/lib.rs`
* `crates/taskito-python/src/prefork/child.rs`
* `crates/taskito-python/src/prefork/mod.rs`
* `crates/taskito-python/src/prefork/protocol.rs`
* `docs/content/docs/python/guides/advanced-execution/prefork.mdx`
* `sdks/python/taskito/_taskito.pyi`
* `sdks/python/taskito/prefork/child.py`
* `sdks/python/taskito/worker_protocol.py`
* `sdks/python/tests/worker/prefork_apps/timeout_app.py`
* `sdks/python/tests/worker/test_prefork.py`

</details>

<details>
<summary>🔗 Linked repositories identified</summary>

CodeRabbit considers these linked repositories for cross-repo context during reviews:

- `ByteVeda/taskito` _(manual)_

</details>

<details>
<summary>💤 Files with no reviewable changes (2)</summary>

* crates/taskito-python/src/prefork/protocol.rs
* crates/taskito-python/Cargo.toml

</details>

</details>

<!-- This is an auto-generated comment by CodeRabbit for review status -->

Comment thread crates/taskito-core/src/lib.rs Outdated
Comment thread crates/taskito-core/src/worker/protocol.rs
Comment thread crates/taskito-core/src/worker/remote.rs
Comment thread crates/taskito-core/src/worker/remote.rs
Comment thread crates/taskito-core/src/worker/remote.rs
Comment thread crates/taskito-core/src/worker/remote.rs
Comment thread crates/taskito-python/src/prefork/child.rs Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/taskito-core/src/worker/runner.rs (1)

167-189: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wire remote cancel notifications through the configured dispatcher.

Worker::spawn() stores the user-provided dispatcher but WorkerHandle::shutdown() is the only code path that calls dispatcher.shutdown(). Jobs running under RemoteDispatcher need notify_cancel(job_id) delivered through that same dispatcher; otherwise only the storage cancel flag can terminate a blocked/long-running remote worker instead of using the executor-side cancel channel.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/taskito-core/src/worker/runner.rs` around lines 167 - 189, Update
Worker::spawn and WorkerHandle::shutdown so the configured WorkerDispatcher’s
notify_cancel(job_id) is invoked for remote cancellation, using the same
dispatcher instance stored during spawn rather than creating or using a separate
dispatcher. Preserve the existing storage cancellation behavior and dispatcher
shutdown flow.
♻️ Duplicate comments (1)
crates/taskito-python/src/prefork/child.rs (1)

90-121: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Kill the child process on every handshake failure path.

Every Err return in spawn_child (read failure, non-hello frame, ack write failure, version mismatch) drops process without calling kill/wait. Child::drop does not terminate the process, so the child becomes an orphan (a zombie on Unix). On the version-mismatch branch, is_alive() never reports the process as dead, so the restart logic in mod.rs never replaces this slot.

Call ChildProcess::kill_and_reap-equivalent cleanup (process.kill() + process.wait()) on each error branch before returning Err.

This was previously flagged as unresolved in a review comment on this exact code segment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/taskito-python/src/prefork/child.rs` around lines 90 - 121, Update the
handshake error paths in spawn_child to terminate and reap the child process
before returning Err. Apply process.kill() followed by process.wait() for read
failures, non-hello frames, acknowledgement write failures, and protocol-version
mismatches, while preserving the existing error messages and successful return
path.
🧹 Nitpick comments (4)
crates/taskito-core/BINDING_CONTRACT.md (1)

108-111: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Add a language to the fenced block.

markdownlint reports MD040 for this fence. The module doc in crates/taskito-core/src/worker/protocol.rs uses ```text for the same example. Use the same tag here.

♻️ Proposed fix
-```
+```text
 {"type":"job","id":"018f…","task_name":"resize","payload_len":7,…}\n
 <7 raw bytes>
</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @crates/taskito-core/BINDING_CONTRACT.md around lines 108 - 111, Update the
fenced example in BINDING_CONTRACT.md to use the text language tag, matching the
existing protocol documentation convention and satisfying markdownlint MD040.


</details>

<!-- cr-comment:v1:9efc93069e8d48693f03fc07 -->

_Source: Linters/SAST tools_

</blockquote></details>
<details>
<summary>crates/taskito-core/src/worker/remote_tests.rs (2)</summary><blockquote>

`165-166`: _📐 Maintainability & Code Quality_ | _🔵 Trivial_ | _💤 Low value_

**Correct the `with_running` doc comment.**

The comment says "current-thread runtime", but the helper builds a multi-thread runtime with two worker threads. The tests depend on that, because the body calls `blocking_send` and blocking frame reads while `run` progresses.

<details>
<summary>♻️ Proposed fix</summary>

```diff
-/// Run `body` with the dispatcher's `run` loop live on a current-thread runtime.
+/// Run `body` with the dispatcher's `run` loop live on a multi-thread runtime.
+/// The body blocks on channel sends and frame reads, so `run` needs its own
+/// worker thread to make progress.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/taskito-core/src/worker/remote_tests.rs` around lines 165 - 166,
Update the doc comment for with_running to state that it runs the dispatcher
loop on a multi-thread runtime with two worker threads, preserving the
documented behavior needed for blocking sends and frame reads.

74-108: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

A regression makes these tests hang instead of fail.

FakeExecutor::read blocks with no timeout, because attach_with_version discards the executor end's Connection at line 52. expect_job, expect_hello_ack, and expect_shutdown therefore block forever if the dispatcher stops writing the expected frame. The suite then hits the CI job timeout with no failing assertion. Keep the Connection and set a read timeout bounded by SETTLE, so a missing frame surfaces as a failed expectation.

♻️ Proposed refactor
 struct FakeExecutor {
     reader: FrameReader<ReadHalf>,
     writer: FrameWriter<WriteHalf>,
+    connection: Connection,
 }
-        let (read, write, _timeout) = Box::new(executor_end).split().expect("split executor end");
+        let (read, write, connection) = Box::new(executor_end).split().expect("split executor end");
+        connection
+            .set_read_timeout(Some(SETTLE))
+            .expect("bound executor reads");
         let mut executor = Self {
             reader: FrameReader::new(read),
             writer: FrameWriter::new(write),
+            connection,
         };

Import Connection alongside the other transport items on line 13.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/taskito-core/src/worker/remote_tests.rs` around lines 74 - 108, Update
attach_with_version and FakeExecutor to retain the executor-side Connection
returned during setup, import Connection with the existing transport types, and
configure its read timeout to the SETTLE duration. Ensure FakeExecutor::read
uses this timed connection so expect_job, expect_hello_ack, and expect_shutdown
fail promptly when frames are missing rather than blocking indefinitely.
crates/taskito-core/src/worker/transport.rs (1)

252-288: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

The read timeout is sampled once per read call, so a cleared timeout does not wake a parked reader.

read copies read_timeout at line 257 and then parks with that value. If Connection::set_read_timeout(None) runs while a reader is parked with Some(limit), that reader still returns WouldBlock at limit. The current attach sequence in crates/taskito-core/src/worker/remote.rs clears the timeout only after the handshake read returns, so this is not reachable today. Re-reading the timeout inside the loop would make the transport robust against a future caller that clears it concurrently.

♻️ Proposed refactor
-        let timeout = *self.channel.read_timeout.lock().unwrap_or_else(recover);
         let mut state = self.channel.state.lock().unwrap_or_else(recover);
 
         while state.buffer.is_empty() {
             // Before the peer splits, `writer_open` is false but no data can
             // have been lost — treating that as EOF would race the handshake.
             if state.writer_ever_opened && !state.writer_open {
                 return Ok(0);
             }
+            // Re-read each pass so a cleared timeout takes effect on a parked
+            // reader instead of expiring it.
+            let timeout = *self.channel.read_timeout.lock().unwrap_or_else(recover);
             state = match timeout {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/taskito-core/src/worker/transport.rs` around lines 252 - 288, Update
ChannelReader::read to re-read self.channel.read_timeout on each loop iteration
before waiting, rather than caching it once per read call. This must let a
concurrently cleared timeout switch the parked reader to an unbounded wait,
while preserving the existing timed WouldBlock behavior when a timeout remains
configured.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/taskito-core/src/worker/remote.rs`:
- Around line 386-410: Update place to check self.shutdown within each
placement-loop iteration and fail the job retryably when shutdown is active,
allowing the reaper and retry policy to handle it instead of waiting for
placement_timeout. Also ensure shutdown() wakes the capacity_changed waiter so a
task parked in changed.await observes the flag immediately.

In `@sdks/python/taskito/worker_protocol.py`:
- Around line 40-48: Update declared_payload_len to validate payload_len and
result_len as non-negative integers before returning them, rejecting invalid
header values rather than passing them to read operations. Ensure read_frame and
write_frame both use this validation path before reading or writing payload
data, while preserving zero for headers without a declared length.

---

Outside diff comments:
In `@crates/taskito-core/src/worker/runner.rs`:
- Around line 167-189: Update Worker::spawn and WorkerHandle::shutdown so the
configured WorkerDispatcher’s notify_cancel(job_id) is invoked for remote
cancellation, using the same dispatcher instance stored during spawn rather than
creating or using a separate dispatcher. Preserve the existing storage
cancellation behavior and dispatcher shutdown flow.

---

Duplicate comments:
In `@crates/taskito-python/src/prefork/child.rs`:
- Around line 90-121: Update the handshake error paths in spawn_child to
terminate and reap the child process before returning Err. Apply process.kill()
followed by process.wait() for read failures, non-hello frames, acknowledgement
write failures, and protocol-version mismatches, while preserving the existing
error messages and successful return path.

---

Nitpick comments:
In `@crates/taskito-core/BINDING_CONTRACT.md`:
- Around line 108-111: Update the fenced example in BINDING_CONTRACT.md to use
the text language tag, matching the existing protocol documentation convention
and satisfying markdownlint MD040.

In `@crates/taskito-core/src/worker/remote_tests.rs`:
- Around line 165-166: Update the doc comment for with_running to state that it
runs the dispatcher loop on a multi-thread runtime with two worker threads,
preserving the documented behavior needed for blocking sends and frame reads.
- Around line 74-108: Update attach_with_version and FakeExecutor to retain the
executor-side Connection returned during setup, import Connection with the
existing transport types, and configure its read timeout to the SETTLE duration.
Ensure FakeExecutor::read uses this timed connection so expect_job,
expect_hello_ack, and expect_shutdown fail promptly when frames are missing
rather than blocking indefinitely.

In `@crates/taskito-core/src/worker/transport.rs`:
- Around line 252-288: Update ChannelReader::read to re-read
self.channel.read_timeout on each loop iteration before waiting, rather than
caching it once per read call. This must let a concurrently cleared timeout
switch the parked reader to an unbounded wait, while preserving the existing
timed WouldBlock behavior when a timeout remains configured.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: c090e563-84ca-4b6a-9b27-9ee3d96223c0

📥 Commits

Reviewing files that changed from the base of the PR and between 5508015 and 4e1882c.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • crates/taskito-core/BINDING_CONTRACT.md
  • crates/taskito-core/src/lib.rs
  • crates/taskito-core/src/worker/mod.rs
  • crates/taskito-core/src/worker/protocol.rs
  • crates/taskito-core/src/worker/remote.rs
  • crates/taskito-core/src/worker/remote_tests.rs
  • crates/taskito-core/src/worker/runner.rs
  • crates/taskito-core/src/worker/transport.rs
  • crates/taskito-python/Cargo.toml
  • crates/taskito-python/src/lib.rs
  • crates/taskito-python/src/prefork/child.rs
  • crates/taskito-python/src/prefork/mod.rs
  • crates/taskito-python/src/prefork/protocol.rs
  • docs/content/docs/python/guides/advanced-execution/prefork.mdx
  • sdks/python/taskito/_taskito.pyi
  • sdks/python/taskito/prefork/child.py
  • sdks/python/taskito/worker_protocol.py
  • sdks/python/tests/worker/prefork_apps/timeout_app.py
  • sdks/python/tests/worker/test_prefork.py
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • ByteVeda/taskito (manual)
💤 Files with no reviewable changes (2)
  • crates/taskito-python/src/prefork/protocol.rs
  • crates/taskito-python/Cargo.toml

Comment thread crates/taskito-core/src/worker/remote.rs
Comment thread sdks/python/taskito/worker_protocol.py
RemoteDispatcher::capacity() returns it, so a root-level consumer could not name its own return type.
A peer closing mid-header reported HeaderTooLarge, contradicting the Eof contract and mislabelling a plain disconnect as an oversized frame.
Child::drop does not terminate the process, so a version-mismatched child survived as an orphan the restart path never notices.
A negative length reached stream.read(), which drains the connection to EOF rather than reading a payload.
The vector only shrank at shutdown, so a reconnecting executor grew it for the life of the process.
An executor attaching after the registry drained was never sent a shutdown and never joined, leaking its reader thread.
A job parked on placement ignored the flag, so shutdown blocked for a full placement_timeout before the drain even began.
run shares a runtime with the scheduler task, so a blocking sleep starved it for up to the whole drain budget.
An executor that stops reading fills the send buffer; the write had no timeout and could park a runtime thread indefinitely.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add RemoteDispatcher and a Transport abstraction to taskito-core Promote the prefork frames to a shared worker protocol module

2 participants