Skip to content

feat(python): report executor progress, logs and toggles over the side-channel - #600

Merged
kartikeya-27 merged 11 commits into
ByteVeda:masterfrom
stromanni:feat/589-side-channel-python
Aug 1, 2026
Merged

feat(python): report executor progress, logs and toggles over the side-channel#600
kartikeya-27 merged 11 commits into
ByteVeda:masterfrom
stromanni:feat/589-side-channel-python

Conversation

@stromanni

@stromanni stromanni commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Part 2 of #589, on top of #599. The core protocol and the scheduler's half landed there; this wires the Python SDK to it, so a task running on an attached executor gets its progress bar, its task logs, its published partials and its dashboard middleware toggles back. Node and Java follow in their own PRs.

What changes

taskito/detached.py — where an executor's storage-shaped writes go

DetachedNative previously warned once and dropped update_progress and write_task_log. It now forwards both to an installed ExecutorSink, a Protocol with exactly those two methods. Both are fire-and-forget: a task reporting progress must not be able to fail, or block, because of what is happening at the far end. With no sink installed — an app imported outside taskito executor, or a scheduler that advertised no side_channel capability — the old warn-once degradation is unchanged.

The module also holds the toggle list the scheduler attached to the current dispatch (set_disabled_middleware / disabled_middleware). A prefork child runs one job at a time, so a single value is the whole story.

taskito/prefork/child.py — the child's half

A _ParentSink frames progress and task logs to the parent over the existing child protocol, and installs itself once the child is running under an executor.

crates/taskito-python/src/prefork/ — the parent's half

PreforkPool gains a set_side_channel, called once the attach completes — the earliest the handle exists. Reader threads start before that, so the slot is an Arc<Mutex<Option<_>>> rather than owned state. Frames from a child are relayed to the scheduler; until the slot is filled, and always for an in-process worker whose children hold real storage and write for themselves, they are dropped.

dispatch_job reads the toggle list off the side channel and carries it on to the child via write_job_with, since the task body — and so the middleware chain — runs there.

taskito/mixins/decorators.py — honouring the toggles

The middleware chain resolver takes a different route when detached: it has no settings store to read, so it uses the list the dispatch carried. That is per job rather than per task name, so it also bypasses the _MW_CHAIN_TTL cache — there is nothing to save when the list arrived with the work. The disable-filtering logic moved to a shared _chain_without so both routes key on middleware_key identically.

taskito/worker_protocol.py

declared_payload_len understands task_log's extra_len, so a published partial's blob is read off the wire rather than desyncing the stream.

One core fix, a6c8ba0

fix(core): send queued side-channel work before a result. The executor's result loop drained results to empty before considering anything else. The scheduler drops a side-channel frame naming a job it no longer holds, so a result that overtook the queues silently lost the task's final progress and its last log lines — the two it is most likely to care about. Results now flush the side-channel queues first, through a shared flush_side_channel that the teardown tail reuses. Bounded work: both queues are capped and each entry is one small frame on an already-open socket.

Covered by a_result_never_overtakes_what_the_task_already_reported.

Tests

  • test_progress_and_logs_reach_a_scheduler_that_advertised_the_side_channel
  • test_a_middleware_disabled_on_the_dispatch_does_not_run
  • test_progress_and_logs_reach_storage_through_a_real_scheduler
  • test_a_dashboard_toggle_reaches_an_attached_executor

The last two run against a real standalone scheduler and assert on the rows that land in storage, not on the frames.

cargo test --workspace, cargo clippy --workspace --all-targets, cargo fmt --check, cargo check under --features postgres and --features redis, ruff check, mypy (194 files) and the Python suite (1379 passed, 14 skipped) are all clean.

Refs #589.

Summary by CodeRabbit

  • New Features

    • Progress updates and task logs from attached and detached workers are now forwarded to the scheduler.
    • Detached jobs can honor scheduler-configured middleware settings.
    • Task-log messages are supported alongside progress updates.
  • Bug Fixes

    • Progress and task-log frames are delivered before the corresponding job result, ensuring complete side-channel data is available when jobs finish.
    • Improved compatibility with schedulers that do not support side-channel communication.

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 24 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: 07fd9db8-05aa-4dc6-a838-fecc2ee0f637

📥 Commits

Reviewing files that changed from the base of the PR and between 7790bb6 and d413054.

📒 Files selected for processing (6)
  • crates/taskito-core/src/worker/remote.rs
  • crates/taskito-core/tests/rust/remote_tests.rs
  • crates/taskito-python/src/prefork/mod.rs
  • sdks/python/taskito/prefork/child.py
  • sdks/python/tests/worker/test_executor_attach.py
  • sdks/python/tests/worker/test_executor_attach_server.py
📝 Walkthrough

Walkthrough

The executor now forwards progress and task-log frames before job results. Prefork workers relay these frames through a scheduler side channel, propagate disabled middleware settings, and preserve fallback behavior when side-channel support is unavailable.

Changes

Executor result ordering

Layer / File(s) Summary
Ordered side-channel delivery
crates/taskito-core/src/worker/executor.rs, crates/taskito-core/tests/rust/executor_tests.rs
The executor flushes progress and task logs before each result and during session cleanup. A regression test verifies the ordering.

Detached worker forwarding

Layer / File(s) Summary
Detached sink and middleware state
sdks/python/taskito/detached.py, sdks/python/taskito/mixins/decorators.py
Detached tasks can forward progress and logs through an installed sink. Detached middleware chains apply job-level disabled middleware settings.
Child-process relay
sdks/python/taskito/prefork/child.py, sdks/python/taskito/worker_protocol.py
Prefork children serialize frame writes, relay side-channel messages to the parent, apply disabled middleware, and decode task-log payload lengths.

Prefork executor wiring

Layer / File(s) Summary
Pool side-channel integration
crates/taskito-python/src/executor.rs, crates/taskito-python/src/prefork/mod.rs
The executor installs its side-channel handle on the concrete prefork pool. The pool passes it to child startup, job dispatch, and reader threads, which relay progress and task-log frames.

Protocol and integration validation

Layer / File(s) Summary
Attach and scheduler coverage
sdks/python/tests/worker/executor_apps/attach_app.py, sdks/python/tests/worker/test_executor_attach.py, sdks/python/tests/worker/test_executor_attach_server.py
Tests cover capability negotiation, side-channel delivery, result decoding, middleware suppression, compatibility without side-channel support, and persistence through the real scheduler.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Task
  participant PreforkChild
  participant PreforkPool
  participant Scheduler
  Task->>PreforkChild: report progress or task log
  PreforkChild->>PreforkPool: send side-channel frame
  PreforkPool->>Scheduler: relay progress or task log
  PreforkChild->>PreforkPool: send job result
  PreforkPool->>Scheduler: relay job result
Loading

Possibly related issues

Possibly related PRs

Suggested labels: enhancement

Suggested reviewers: kartikeya-27

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 75.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main Python SDK change: reporting executor progress, logs, and middleware toggles through the side-channel.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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 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: 1

🧹 Nitpick comments (2)
crates/taskito-python/src/prefork/mod.rs (1)

309-317: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Clone the handle out of the slot before calling into it.

dispatch_job holds the side_channel mutex while it calls relay.disabled_middleware(&job.id), which takes the core executor's own shared lock. relay_side_channel at Line 461 does the opposite: it clones the handle, releases the slot guard, then calls the relay. Aligning dispatch_job with that pattern shortens the critical section and keeps one lock-ordering rule in this module.

♻️ Proposed refactor
-    let disabled = side_channel
-        .lock()
-        .unwrap_or_else(|poisoned| poisoned.into_inner())
-        .as_ref()
-        .map(|relay| relay.disabled_middleware(&job.id))
-        .unwrap_or_default();
+    let relay = side_channel
+        .lock()
+        .unwrap_or_else(|poisoned| poisoned.into_inner())
+        .clone();
+    let disabled = relay
+        .map(|relay| relay.disabled_middleware(&job.id))
+        .unwrap_or_default();
🤖 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/mod.rs` around lines 309 - 317, Update
dispatch_job’s disabled-middleware lookup to clone the relay handle from the
side_channel slot while holding the mutex, release the guard, and only then call
disabled_middleware(&job.id). Match the existing relay_side_channel pattern at
its visible implementation, preserving the default behavior when no relay is
configured.
sdks/python/tests/worker/test_executor_attach_server.py (1)

239-251: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Bound the inner waits by the remaining outer budget.

The retry loop uses SETTLE as its own deadline, and each iteration calls wait_for_status(..., timeout=SETTLE) and result(timeout=SETTLE). A single slow iteration can therefore consume the whole outer budget on its own. Two consequences follow. The test can overrun its intended SETTLE bound by a multiple. The loop can also get only one attempt, which defeats the purpose of retrying until the scheduler's dispatch cache expires.

Passing the remaining time into the inner waits keeps the total bounded and guarantees several attempts.

♻️ Proposed refactor
         deadline = time.monotonic() + SETTLE
         while time.monotonic() < deadline:
+            remaining = max(deadline - time.monotonic(), 0.1)
             job_id = enqueue(db_path, MIDDLEWARED)
-            wait_for_status(db_path, job_id, "complete")
+            wait_for_status(db_path, job_id, "complete", timeout=remaining)
             toggled = queue.get_job(job_id)
             assert toggled is not None
-            if toggled.result(timeout=SETTLE) == "":
+            if toggled.result(timeout=remaining) == "":
                 return
             time.sleep(0.5)
🤖 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 `@sdks/python/tests/worker/test_executor_attach_server.py` around lines 239 -
251, Update the retry loop after disable_middleware_for_task so each wait uses
the remaining outer deadline rather than the full SETTLE value: compute the
remaining budget before enqueue/status polling and before toggled.result, pass
it to wait_for_status and result, and stop or fail when no budget remains.
Preserve the retry behavior and ensure the total loop duration stays within
SETTLE.
🤖 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 `@sdks/python/taskito/prefork/child.py`:
- Around line 119-124: Update the exception tuple in the static _send method to
catch OSError instead of BrokenPipeError, while preserving the existing
EOFError, ValueError, and ProtocolError handling and debug logging behavior.

---

Nitpick comments:
In `@crates/taskito-python/src/prefork/mod.rs`:
- Around line 309-317: Update dispatch_job’s disabled-middleware lookup to clone
the relay handle from the side_channel slot while holding the mutex, release the
guard, and only then call disabled_middleware(&job.id). Match the existing
relay_side_channel pattern at its visible implementation, preserving the default
behavior when no relay is configured.

In `@sdks/python/tests/worker/test_executor_attach_server.py`:
- Around line 239-251: Update the retry loop after disable_middleware_for_task
so each wait uses the remaining outer deadline rather than the full SETTLE
value: compute the remaining budget before enqueue/status polling and before
toggled.result, pass it to wait_for_status and result, and stop or fail when no
budget remains. Preserve the retry behavior and ensure the total loop duration
stays within SETTLE.
🪄 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: 66543270-228e-494e-9a11-b6c0c5d011bc

📥 Commits

Reviewing files that changed from the base of the PR and between e071028 and 7790bb6.

📒 Files selected for processing (11)
  • crates/taskito-core/src/worker/executor.rs
  • crates/taskito-core/tests/rust/executor_tests.rs
  • crates/taskito-python/src/executor.rs
  • crates/taskito-python/src/prefork/mod.rs
  • sdks/python/taskito/detached.py
  • sdks/python/taskito/mixins/decorators.py
  • sdks/python/taskito/prefork/child.py
  • sdks/python/taskito/worker_protocol.py
  • sdks/python/tests/worker/executor_apps/attach_app.py
  • sdks/python/tests/worker/test_executor_attach.py
  • sdks/python/tests/worker/test_executor_attach_server.py
🔗 Linked repositories identified

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

  • ByteVeda/taskito (manual)

Comment thread sdks/python/taskito/prefork/child.py
@stromanni

Copy link
Copy Markdown
Contributor Author

All three findings were valid and all three are fixed, plus the CI failure and a real bug the review prompted me to go looking for.

Item Commit
_send caught BrokenPipeError, not OSError b608915
dispatch_job held the slot mutex across a relay call 1ad245c
Toggle retry loop could spend its whole budget on one attempt 779be46
CI: unused type: ignore d413054
A job's final progress could be lost entirely d316bb5

_send. Widened to OSError, which keeps BrokenPipeError covered as a subclass. The comment names the two cases that motivated it: EBADF on a closed stream, and EPIPE surfaced during flush.

dispatch_job. Clones the relay out of the slot and releases the guard before calling into it, matching relay_side_channel. The relay takes locks of its own, so this module now holds exactly one at a time.

Toggle retry. Each inner wait gets what is left of the outer deadline rather than a fresh SETTLE, so the loop stays inside its bound and actually gets the several attempts it exists to make.

CI. mypy flagged an unused type: ignore — the same from attach_app import queue appears twice in the file, and only the first reports. Worth noting the repo's documented command is mypy taskito/, while CI runs mypy taskito/ tests/; that gap is why it passed locally.

The progress bug

Running test_executor_attach_server.py with TASKITO_SERVER_BIN set surfaced a genuine failure — test_progress_and_logs_reach_storage_through_a_real_scheduler failed with saw 50, where the task's last call is update_progress(100). Flaky: one run passed in 2s, the next failed after polling 60s.

Storage::complete moves a job's row from jobs into archived_jobs in one transaction, and update_progress writes only jobs. So progress applied after a result is not late — it is lost, permanently. The a6c8ba0 fix on this branch ordered the executor's queues ahead of its result; the scheduler's pump is a separate drain thread, and it was racing the archive.

RemoteDispatcher now settles a job's pending progress before emitting its result. That is one small write, on the reader thread, once per job — the task has already returned and its slot is already free, so nothing waits on it. Progress writes are serialized under applying_progress so a settle cannot conclude there is nothing to do while the drain is mid-write on the same value.

Covered by a_jobs_final_progress_is_applied_before_its_result, which delays the sink's write so the drain deterministically loses the race. It fails with the settle removed. The server-backed suite now passes 3/3 in ~21s each, against a 62s failure before.

Verification

cargo test --workspace, cargo clippy --all-targets --all-features -- -D warnings, cargo fmt --check, ruff check taskito/ tests/, ruff format --check, mypy taskito/ tests/ (315 files) and the Python suite (1379 passed, 14 skipped) are clean — the exact commands CI runs. The seven TASKITO_SERVER_BIN-gated tests, which CI skips, pass too.

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.

2 participants