Skip to content

feat(sdks): taskito executor subcommand for Python, Node and Java - #595

Merged
kartikeya-27 merged 46 commits into
ByteVeda:masterfrom
stromanni:feat/546-executor-attach-sdks
Aug 1, 2026
Merged

feat(sdks): taskito executor subcommand for Python, Node and Java#595
kartikeya-27 merged 46 commits into
ByteVeda:masterfrom
stromanni:feat/546-executor-attach-sdks

Conversation

@stromanni

@stromanni stromanni commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Implements the three SDK executor subcommands — the S5–S7 phases of #546.

Closes #551
Closes #552
Closes #553

One client in core, three thin CLIs

tasks/executor-attach.md says "only the concrete stream changes", and that turned out to be literally true, so it sets the shape:

taskito-server                     app container
RemoteDispatcher ←── socket ──→ ExecutorClient (taskito-core)
                                      ↓ Job   ↑ JobResult
                           PreforkPool │ NodeDispatcher │ JavaDispatcher

WorkerDispatcher was already the seam every SDK pool implements, and its signature — run(job_rx, result_tx) + shutdown() + notify_cancel() — is exactly what an executor needs. Worker::dispatcher() even documented the mirror case. So ExecutorClient is Worker::spawn with storage swapped for a socket, and each SDK's subcommand is a wrapper over the pool it already had.

Writing that client three times would have tripled the subtle parts — handshake ordering, exactly-once result accounting, drain sequencing, cancel routing — with nothing gained. The JSON-header wire is unchanged, so a third-party SDK can still write an executor with its standard library alone.

The executor opens no storage

An executor imports the user's app to find its handlers, and that import used to construct a Queue — which connected eagerly, putting the database credentials straight back into the app image that #546 exists to keep them out of.

TASKITO_DETACHED_EXECUTOR=1 is now set by each CLI before it imports the app, and the native queue is replaced by a stand-in. Python's prefork children inherit the variable, so they open nothing either; Java already took no QueueBackend. One rule decides every method:

  • Reads answer emptyNone/null is what a queue with no such row returns anyway, and callers already handle it. Not optional: Queue.__init__ reads settings (webhook subscriptions), so a stand-in that refused them could not build a Queue at all.
  • Writes throw — an enqueue that quietly vanished would be worse than one that failed.
  • Job-scoped conveniences degrade with one warning — a task calling set_progress must not fail because it happens to be running detached.

What that costs — progress, task logs, published partials, dashboard middleware toggles — is tracked in #589 with per-layer sub-issues; closing it needs new protocol frames.

Four things the tests caught that reading the docs did not

  • The Java ServiceLoader mechanism in Java: executor subcommand plus classpath handler discovery #553 cannot work. On the classpath a listed class must be a subtype of the service with a public no-arg constructor; the static provider() form only applies to modules on the module path. The first attempt generated provider() and died with ServiceConfigurationError: demo.GreeterTasks not a subtype. A new HandlerRegistryProvider interface is now the service, and the processor generates a nested <Class>Tasks$Provider implementing it. Owners with no accessible no-arg constructor are skipped with a compiler note rather than silently — only their own code knows how to build them.
  • startExecutor blocked the Node event loop for the whole handshake timeout. Now async. Found because the in-process test deadlocked against its own fake scheduler.
  • A local stop() hung forever. stop() cannot unpark the frame reader, which is parked on a read only the scheduler could satisfy, so the session never ended and any shell that stopped and then waited deadlocked. Fixed in core with a regression test.
  • Node cancels would have silently stopped working once storage was gone: the AbortSignal a cooperative task watches is driven by a storage poll. JsExecutor.isCancelRequested now exposes the state the cancel frames land in. The test asserts the result frame comes back cancelled, not failure — proving both that the handler aborted and that the native side reclassified the throw.

Verification

Rust cargo test --workspace clean, clippy --all-targets -D warnings clean, --features postgres and --features redis check
Python 1377 passed / 12 skipped, no regressions; 19 executor tests, 5 of them against the real taskito-server binary; ruff + mypy clean
Node 545 passed (baseline 524), zero lost; 4 gated tests against the real server; biome + tsc clean
Java 9 executor tests; plus a job enqueued through the CLI ran on an executor whose handlers came only from META-INF/services, with no user main

Each SDK has two test layers: a hermetic scheduler speaking the wire (so they run with no Rust build, in every CI job) and the same assertions against the real taskito-server behind TASKITO_SERVER_BIN. That pairing is what keeps the fakes honest.

Two environment notes, both reproduced on a clean tree and unrelated to this branch: Java's :compileTestJava is skipped here, so its tests were run through the JUnit platform launcher directly; and 10 test/dashboard/** Node files fail on a missing SPA build.

Reviewing

The first three commits (feat(server): authenticate the attach listener handshake and the two storage fixes) are the #584 review follow-ups this branch was cut from, not part of this work — they are unmerged and unreviewed, so say the word if you would rather they went up as their own PR and this one rebased onto master.

The remaining 25 split cleanly by layer: core (7), Python (5), Node (7), Java (6). S5/S6/S7 were meant to be separate PRs, so this can be split per SDK if you prefer that shape.

Summary by CodeRabbit

  • New Features

    • Added detached scheduler executors for Java, Node.js, and Python with configurable tasks, concurrency, authentication, identity, heartbeats, and shutdown handling.
    • Added executor lifecycle controls, status and connection details, cancellation, retries, and graceful draining.
    • Added executor commands to the Java, Node.js, and Python CLIs.
    • Added detached-mode queue behavior for storage-independent task execution.
    • Added automatic Java handler discovery and registration support.
  • Bug Fixes

    • Improved handling of unavailable storage, cancelled jobs, connection failures, and malformed scheduler responses.

@coderabbitai

coderabbitai Bot commented Aug 1, 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: 50 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: 58928d11-76f8-4f73-8fe9-e5b99a5a3e67

📥 Commits

Reviewing files that changed from the base of the PR and between fab6f68 and 42be90b.

📒 Files selected for processing (7)
  • crates/taskito-core/src/worker/protocol.rs
  • crates/taskito-python/src/executor.rs
  • sdks/java/processor/src/main/java/org/byteveda/taskito/processor/TaskHandlerProcessor.java
  • sdks/java/src/main/java/org/byteveda/taskito/cli/Cli.java
  • sdks/node/src/detached.ts
  • sdks/python/taskito/detached.py
  • sdks/python/tests/worker/test_executor_attach_server.py
📝 Walkthrough

Walkthrough

The PR adds a shared scheduler-attachment executor to the Rust core and exposes it through Node.js, Python, and Java SDKs. It adds detached storage and cancellation handling, lifecycle controls, CLIs, Java handler discovery, protocol conversion, and end-to-end tests.

Changes

Core executor attachment

Layer / File(s) Summary
Attachment protocol and runtime
crates/taskito-core/src/worker/*, crates/taskito-core/tests/rust/*
Adds address parsing, cancellation signals, handshake and job-result conversion, executor lifecycle controls, dispatch, heartbeats, draining, and integration coverage.

Node.js executor

Layer / File(s) Summary
Detached execution and task dispatch
sdks/node/src/*, crates/taskito-node/src/*
Adds detached queue adapters, executor startup and shutdown, cancellation-aware task callbacks, resource handling, and public exports.
Node CLI and integration tests
sdks/node/src/cli/*, sdks/node/test/worker/*
Adds the executor command, shared app loading, environment validation, fake-scheduler tests, and real-server tests.

Python executor

Layer / File(s) Summary
Detached execution and Python API
sdks/python/taskito/*
Adds detached queue behavior, the Python Executor binding, lifecycle methods, CLI configuration, and signal-driven shutdown.
Python executor tests
sdks/python/tests/worker/*
Adds fake-scheduler, subprocess, cancellation, retry, storage-independence, and real-server integration tests.

Java executor

Layer / File(s) Summary
Executor API and JNI control
sdks/java/src/main/java/org/byteveda/taskito/{worker,internal}/*, crates/taskito-java/src/*
Adds the Java executor builder, native attachment surface, dispatcher cancellation support, detached metadata behavior, and lifecycle controls.
CLI discovery and validation
sdks/java/processor/src/main/java/*, sdks/java/src/main/java/org/byteveda/taskito/cli/Cli.java
Adds generated ServiceLoader providers, handler registry discovery, executor CLI options, slot validation, and bounded shutdown.
Java integration tests
sdks/java/src/test/java/*
Adds socket-level coverage for handshake metadata, execution, failure results, refusal handling, and session shutdown.

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

Possibly related issues

  • Issue 546: Directly covers the executor-attachment architecture implemented across Rust, Python, Node.js, and Java.

Possibly related PRs

Suggested reviewers: kartikeya-27

🚥 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 executor subcommand added for Python, Node, and Java.
Linked Issues check ✅ Passed The changes implement the Python, Node, and Java executor requirements, including attachment, discovery, lifecycle, cancellation, retries, and integration coverage [#551] [#552] [#553].
Out of Scope Changes check ✅ Passed The changes remain within executor attachment, detached execution, handler discovery, shared transport support, and related tests for #551#553.
Docstring Coverage ✅ Passed Docstring coverage is 95.52% which is sufficient. The required threshold is 80.00%.

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.

@pratyush618 pratyush618 added deployment Container and cluster deployment topology enhancement New feature or request java Java SDK node Node SDK labels Aug 1, 2026
@stromanni

Copy link
Copy Markdown
Contributor Author

Stacked on #596 — the first three commits here are that PR's, not this one's. Once #596 merges this diff drops to its own 25 commits.

@stromanni
stromanni force-pushed the feat/546-executor-attach-sdks branch 2 times, most recently from 9c5d725 to 7cdfec9 Compare August 1, 2026 04:16
The attach protocol only had scheduler-side conversions. An executor needs
the other direction: a Job rebuilt from a dispatch frame, and a result frame
built from a JobResult.
ExecutorClient is Worker::spawn with storage swapped for a socket: it dials
a scheduler, then feeds the same WorkerDispatcher every SDK already
implements. Needs no Storage, so an executor image carries app code and no
database credentials.

Draining announces zero capacity by heartbeat before disconnecting, which
the scheduler already honours, so in-flight work reports instead of waiting
for a reap.
Every SDK executor needs to turn TASKITO_ATTACH into a connection, and the
grammar has to match what the listener binds. One parser here rather than
three in three languages, where unix: support would drift.
A worker reads the storage cancel flag; an attached executor has no storage
and learns from the scheduler's cancel frame instead. Both answer one
question, so both ask it in one place.
Slots are prefork children, so the timeout watchdog, least-loaded dispatch
and restart-on-crash all come from shipped code and child.py is untouched.

start/wait/stop rather than one blocking run: a Python signal handler only
runs when the main thread holds the GIL, so a blocking call would make the
process deaf to SIGTERM.
Reads TASKITO_ATTACH and TASKITO_SLOTS, the contract shared with the other
SDKs. The token is env-only: in argv it would show up in ps and shell
history.
A socket speaking the frame protocol stands in for the scheduler so these
run without a Rust build; the same assertions run against the real
taskito-server when TASKITO_SERVER_BIN points at one.
ExecutorHandle::wait consumes the handle, which a shell that must also hold
it to shut down cannot do. A cloneable session view lets an async runtime
watch for the scheduler ending the session.
The executor runs the same task bodies as the worker, so the middleware,
codec and resource-scope handling is shared rather than copied.
run and executor both resolve a Queue from a module path.
Attaching is async: dialling and the handshake block, so on the JS thread
they would freeze the event loop until the scheduler answers.

The dispatcher now takes its cancels from the shared source, so a detached
executor can learn of one without reading storage.
The inverse of runWorker: the scheduler holds the database connection and
dispatches over a socket. Same middleware, codecs and resources — only the
transport differs.
Reads TASKITO_ATTACH and TASKITO_SLOTS, the contract shared with the other
SDKs. The token is env-only: in argv it would show up in ps.
A socket speaking the frame protocol stands in for the scheduler so these
run without a Rust build; the same assertions run against the real
taskito-server when TASKITO_SERVER_BIN points at one.
stop() cannot unpark the frame reader, which is blocked on a read only the
scheduler could satisfy. A shell that stopped and then waited would hang
instead of draining.
The processor now generates a ServiceLoader provider per annotated class and
lists it in META-INF/services, so an executor finds handlers with no user
main.

ServiceLoader cannot load HandlerRegistry itself: on the classpath a listed
class must be a subtype with a public no-arg constructor, and the static
provider() form only applies to modules. Hence the new interface.

Classes with no accessible no-arg constructor are skipped with a note —
only their own code knows how to build them.

@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: 14

🧹 Nitpick comments (14)
sdks/node/test/worker/executorAttach.test.ts (1)

467-469: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove the dead environment assignment.

process.env.TASKITO_ATTACH = undefined stores the string "undefined", because Node coerces environment values to strings. Line 469 deletes the key immediately, so the assignment has no effect and misstates the intent.

🧹 Proposed fix
   const previous = process.env.TASKITO_ATTACH;
-  process.env.TASKITO_ATTACH = undefined;
   delete process.env.TASKITO_ATTACH;
🤖 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/node/test/worker/executorAttach.test.ts` around lines 467 - 469, Remove
the redundant process.env.TASKITO_ATTACH = undefined assignment in the test
setup, leaving delete process.env.TASKITO_ATTACH as the sole operation that
clears the environment variable while preserving the previous value capture.
sdks/node/src/queue.ts (1)

1665-1670: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Consider caching the disable lookup instead of reading it per job.

middlewareFor runs once per dispatched job, and disables.getFor(taskName) performs a settings read each time. On a storage-backed queue handle that is a synchronous storage hit on the job path. A small per-task cache, or a snapshot taken at attach time, removes it.

Note that the executor path normally runs detached, where the read is a no-op, so this only matters for a storage-backed handle.

🤖 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/node/src/queue.ts` around lines 1665 - 1670, Update the middlewareFor
callback to cache the result of disables.getFor(taskName) per task name,
avoiding repeated settings reads for dispatched jobs while preserving the
existing filtering behavior for disabled and enabled task middleware.
sdks/node/src/executor.ts (1)

176-187: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Validate run.slots the same way as the environment value.

envInt rejects a non-positive or non-integer TASKITO_SLOTS. A programmatic slots value bypasses that check and crosses to native unvalidated. Reject it here so both paths report the same error.

🤖 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/node/src/executor.ts` around lines 176 - 187, Validate the programmatic
run.slots value with the same positive-integer rules implemented by envInt
before passing it to native code. Reject non-integer or values below 1 with the
same error behavior and message format, while preserving handling for omitted
slots.
sdks/node/test/worker/executorAttachServer.test.ts (1)

38-48: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Reduce two flakiness sources in the harness.

freePort closes the probe socket before the server binds, so another process can take the port in that window. The failure then looks like a server startup failure.

await sleep(1000) also encodes a fixed startup delay. A slow machine can enqueue before the scheduler is ready. waitFor already exists; poll for the attached executor instead, for example through queue.listWorkers().

Also applies to: 114-114

🤖 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/node/test/worker/executorAttachServer.test.ts` around lines 38 - 48,
Update freePort and the executor startup test to remove both race conditions:
reserve/select a port without closing the probe before the server binds, and
replace the fixed await sleep(1000) delay with waitFor polling that uses
queue.listWorkers() to confirm the attached executor is ready before enqueueing
work.
sdks/node/src/detached.ts (1)

57-62: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Align the warning label with the method name.

updateProgress warns as "setProgress" and writeTaskLog warns as "publish". The public API names are helpful, but writeTaskLog also backs job.log(...), not only publish. A user who calls job.log(...) sees a warning about publish.

Consider a label that covers both, for example "log/publish".

🤖 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/node/src/detached.ts` around lines 57 - 62, Update the warning labels in
updateProgress and writeTaskLog to reflect their public API methods; use the
method name for updateProgress and a shared label covering both log and publish
for writeTaskLog, such as “log/publish”.
sdks/python/tests/worker/executor_apps/attach_app.py (1)

22-28: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Fixed default database path in the fixture app is written by the test process. attach_app.py falls back to a predictable /tmp file when TASKITO_EXECUTOR_TEST_DB is absent, and the test interpreter imports the same module without that variable set, so the suite creates a shared database outside tmp_path.

  • sdks/python/tests/worker/executor_apps/attach_app.py#L22-L28: replace the /tmp/taskito-executor.db default with a tempfile.gettempdir()-based per-run name, and derive db_url from the backend value rather than from the presence of TASKITO_EXECUTOR_TEST_BACKEND.
  • sdks/python/tests/worker/test_executor_attach.py#L241-L257: set TASKITO_EXECUTOR_TEST_DB to a tmp_path value in the test process before attach_app is imported by payload_for and the _app_importable fixture.
🤖 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/executor_apps/attach_app.py` around lines 22 - 28,
Update sdks/python/tests/worker/executor_apps/attach_app.py lines 22-28 to use a
tempfile.gettempdir()-based per-run database filename instead of the fixed /tmp
fallback, and derive db_url from the resolved backend value. Update
sdks/python/tests/worker/test_executor_attach.py lines 241-257 to set
TASKITO_EXECUTOR_TEST_DB to a tmp_path-based value in the test process before
payload_for and _app_importable import attach_app.

Source: Linters/SAST tools

crates/taskito-python/src/executor.rs (1)

142-148: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

wait holds the handle lock while it blocks.

with_handle keeps the Mutex guard for the whole closure, so the guard is held across the blocking wait_timeout. A concurrent stop(), is_running(), or shutdown() call from another thread blocks until the timeout expires. The CLI polls with 200 ms, so the delay is bounded, and the signal handler runs on the main thread after wait returns. Consider cloning the needed handle state (or wrapping it in an Arc) so lifecycle calls never wait on a network 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-python/src/executor.rs` around lines 142 - 148, Update wait so
with_handle does not retain the mutex guard while blocking in
handle.wait_timeout. Clone or otherwise share the required handle state before
entering the timeout wait, then release the lock so stop, is_running, and
shutdown can proceed immediately; preserve the existing timeout result and
unwrap_or(true) behavior.
sdks/python/taskito/cli.py (1)

342-406: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Install the signal handlers before work can arrive.

The scheduler can dispatch jobs as soon as _Executor(...) completes the handshake. The SIGTERM and SIGINT handlers are installed several statements later. In that window a SIGTERM terminates the process with the default action, so in-flight jobs are stranded for the reaper instead of drained. Install request_stop before the attach, or move the print after the handler installation and register the handlers first.

Also consider wrapping the poll loop in try/finally so executor.shutdown() still runs if the loop raises.

🤖 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/taskito/cli.py` around lines 342 - 406, Register SIGTERM and
SIGINT handlers in run_executor before constructing _Executor, with request_stop
safely stopping the attached executor once available; this must cover the
handshake window where jobs may arrive. Keep the existing signal behavior and
move the attachment status print after handler registration. Wrap the polling
and shutdown flow in try/finally so executor.shutdown() always runs when the
loop or wait raises.
sdks/python/tests/worker/test_executor_attach.py (1)

129-136: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

The drain heartbeat assertion also matches a normal busy heartbeat.

next_heartbeat(free_slots=0) accepts any heartbeat that reports zero free slots. With slots=1 and one running job, the executor already reports free_slots == 0 before the SIGTERM. The test can therefore pass on a pre-signal heartbeat and prove nothing about the drain announcement. Assert on a drain-specific field if the frame carries one, or read heartbeats until one arrives after the signal was sent and assert the field the drain sets.

Also applies to: 369-373

🤖 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.py` around lines 129 - 136, The
next_heartbeat helper currently accepts a normal busy heartbeat when free_slots
is zero, so the drain assertion can occur before SIGTERM takes effect. Update
the drain test flow around next_heartbeat and the SIGTERM send to consume or
distinguish pre-signal heartbeats, then assert the drain-specific heartbeat
field or otherwise require a heartbeat emitted after the signal.
sdks/python/tests/worker/test_executor_attach_server.py (1)

135-137: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Replace the fixed time.sleep(1.0) attach waits with a readiness poll.

Each test waits one second for the executor to attach before it enqueues. Under a loaded CI runner the attach can take longer, and the enqueued job is then dispatched late or the assertion times out. Poll for a deterministic signal instead, for example the executor's stdout attach line, or the presence of a registered worker in storage.

♻️ Sketch of a readiness helper
def wait_attached(process: subprocess.Popen[str], timeout: float = SETTLE) -> None:
    """Block until the executor prints its attach line."""
    assert process.stdout is not None
    deadline = time.monotonic() + timeout
    while time.monotonic() < deadline:
        line = process.stdout.readline()
        if "attached to" in line:
            return
        if process.poll() is not None:
            raise AssertionError(f"the executor exited early: {read_stderr(process)}")
    raise AssertionError("the executor never attached")

Also applies to: 148-150, 175-177

🤖 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 135 -
137, Replace the fixed time.sleep(1.0) waits in the affected executor attach
tests with a shared readiness helper, such as wait_attached, that polls the
executor process output for its attach confirmation until the existing timeout
and reports early exit or timeout failures. Call this helper before enqueueing
jobs in each affected test, preserving the current test flow while eliminating
timing-dependent waits.
sdks/java/src/test/java/org/byteveda/taskito/worker/ExecutorAttachTest.java (2)

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

results is never written, and readFrame can block past SETTLE_MS.

Two test-hygiene points in nextResult:

  • Nothing ever adds to the results deque, so the !results.isEmpty() branch at lines 190-192 is dead. Remove the field and the branch.
  • readFrame() blocks in stream.read() with no socket timeout, so the deadline loop cannot enforce SETTLE_MS. When a result never arrives, the @Timeout(60) fires instead of the "no result frame arrived" assertion, which gives a less useful failure message. Set a read timeout on the accepted socket.
♻️ Proposed fix
             try {
                 Socket client = server.accept();
+                // Bounds every readFrame, so nextResult's deadline is real.
+                client.setSoTimeout((int) SETTLE_MS);
                 socket.set(client);
-        private final Deque<JsonNode> results = new ArrayDeque<>();
         JsonNode nextResult() throws IOException {
             long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(SETTLE_MS);
             while (System.nanoTime() < deadline) {
-                if (!results.isEmpty()) {
-                    return results.removeFirst();
-                }
                 JsonNode frame = readFrame();

Drop the now-unused ArrayDeque and Deque imports at lines 19-20.

Also applies to: 186-202

🤖 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/java/src/test/java/org/byteveda/taskito/worker/ExecutorAttachTest.java`
at line 65, Remove the unused results deque field and its ArrayDeque/Deque
imports, then delete the dead results branch from nextResult. Configure a read
timeout on the accepted socket used by readFrame so its deadline loop can
enforce SETTLE_MS and produce the intended no-result assertion.

229-247: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for discover() and for a cancel frame.

The suite covers explicit register(...) only. Two gaps remain against the PR objectives:

  • Classpath discovery through META-INF/services is the headline Java objective, and no test exercises Executor.Builder.discover(ClassLoader) or the generated Provider. A test can pass a ClassLoader that serves a synthetic META-INF/services/org.byteveda.taskito.worker.HandlerRegistryProvider resource, so it needs no annotation-processor run.
  • No test sends a cancel frame. JavaDispatcher::notify_cancel and the failure-to-cancelled reclassification at crates/taskito-java/src/dispatcher.rs line 162 are therefore untested from Java.

I can draft both tests. Do you want me to open an issue to track them?

Also applies to: 349-356

🤖 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/java/src/test/java/org/byteveda/taskito/worker/ExecutorAttachTest.java`
around lines 229 - 247, Add tests in ExecutorAttachTest covering
Executor.Builder.discover(ClassLoader) with a synthetic
META-INF/services/org.byteveda.taskito.worker.HandlerRegistryProvider resource
and verifying the discovered handler is advertised. Also add a cancel-frame test
exercising JavaDispatcher cancellation, including reclassification of a failed
task as cancelled; reuse existing FakeScheduler and attachment helpers where
applicable.
sdks/java/src/main/java/org/byteveda/taskito/worker/Executor.java (1)

55-55: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

closed is not volatile, so the documented idempotence does not hold across threads.

close() reads and writes closed without synchronization. Two threads can both pass the check at line 115 and both run the drain and control.close(). JniExecutorControl.close() is itself guarded, so a double native free cannot happen, but handlerPool.shutdown(), resources.teardownWorker(), and the WORKER_STOPPED event all run twice.

A shutdown hook that calls close() while the main thread is inside try-with-resources reaches this. Make the field volatile and set it with a compare-and-set, or synchronize the method.

♻️ Proposed fix
-    private boolean closed;
+    private final java.util.concurrent.atomic.AtomicBoolean closed =
+            new java.util.concurrent.atomic.AtomicBoolean();
     `@Override`
     public void close() {
-        if (closed) {
+        if (!closed.compareAndSet(false, true)) {
             return;
         }
-        closed = true;
         control.stop();

Also applies to: 113-120

🤖 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/java/src/main/java/org/byteveda/taskito/worker/Executor.java` at line
55, Update the closed-state handling in Executor.close() and its closed field so
shutdown is atomic across threads: make the field volatile and use a
compare-and-set guard, or synchronize close(). Ensure only one caller performs
handlerPool.shutdown(), resources.teardownWorker(), and the WORKER_STOPPED event
while preserving idempotent subsequent close() calls.
sdks/java/processor/src/main/java/org/byteveda/taskito/processor/TaskHandlerProcessor.java (1)

40-41: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Declare AGGREGATING for the service-file processor.

writeServiceFile() collects providers in processors and writes exactly that set to META-INF/services/<SERVICE> during processingOver(). Since the processor is not declared incremental, Gradle currently forces full annotation processing; add @IncrementalAnnotationProcessor(AGGREGATING) so the accumulating service-output contract stays explicit and later isolating incremental builds do not lose providers.

🤖 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/java/processor/src/main/java/org/byteveda/taskito/processor/TaskHandlerProcessor.java`
around lines 40 - 41, Annotate TaskHandlerProcessor with
`@IncrementalAnnotationProcessor`(AGGREGATING) alongside its existing processor
annotations. Keep writeServiceFile() and the processors accumulation behavior
unchanged so the service output continues to include all providers during
processingOver().
🤖 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/dial.rs`:
- Around line 91-103: Update the Tcp branch of connect to iterate through every
address returned by to_socket_addrs() in order, attempting
TcpStream::connect_timeout and constructing the TcpTransport for each candidate.
Return immediately on the first successful connection, retain the last
connection or transport error, and return it after all candidates fail; preserve
the existing invalid-host and no-address errors.

In `@crates/taskito-core/src/worker/executor.rs`:
- Around line 384-412: Bound Executor::teardown so it does not unconditionally
join threads after the shutdown-drain deadline: join them only when
results_flushed is set, otherwise detach the remaining threads. In
crates/taskito-core/src/worker/executor.rs lines 384-412, preserve the existing
drain warning and shutdown behavior. In
crates/taskito-core/src/worker/executor_tests.rs lines 908-930, replace
Behaviour::Block with a behavior that parks indefinitely and assert that
shutdown() returns within a multiple of shutdown_drain.
- Around line 468-489: Update ExecutorHandle::begin_drain so local shutdown
state is applied before the potentially blocking Heartbeat send: drop the job
sender and set session_over before calling self.send. Preserve the existing
idempotence guard and heartbeat announcement, while ensuring stop’s documented
non-waiting behavior is maintained even when the scheduler is no longer reading.

In `@crates/taskito-python/src/lib.rs`:
- Line 5: Gate the prefork module declaration in lib.rs and the PreforkPool
construction in executor::PyExecutor with the same supported-target or feature
condition. Ensure unsupported targets such as Windows avoid both the module and
instantiation while preserving the existing prefork behavior on supported
targets.

In
`@sdks/java/processor/src/main/java/org/byteveda/taskito/processor/TaskHandlerProcessor.java`:
- Around line 93-96: Update the Javadoc above the generated companion listing to
reference HandlerRegistryProvider instead of HandlerRegistry, matching the
service type loaded by Executor.Builder.discover(ClassLoader).
- Around line 128-132: Update isServiceLoadable() to reject every nested
TypeElement, including static nested classes, before ServiceLoader generation;
retain the existing abstract-class rejection. Ensure generate() only creates
providers for package-level owners so Provider.registry() cannot reference an
invalid simple nested type.

In `@sdks/java/src/main/java/org/byteveda/taskito/cli/Cli.java`:
- Around line 328-343: Update Cli.resolveSlots and its call path to reject
parsed TASKITO_SLOTS values less than one, and throw ParameterException using
the injected CommandSpec’s commandLine() rather than a new CommandLine instance.
Resolve the slot count once in call(), then reuse that value for both the
Executor.Builder configuration and the banner so they remain consistent.
- Around line 309-323: Update the shutdown-hook setup in Cli.main to use a
shared flag indicating whether the session is still active, and have the hook
return without calling executor.stop() or joining main once the normal path has
completed. Clear the flag and remove the registered hook before leaving the
try-with-resources block, while preserving stop-and-join behavior for
signal-triggered shutdowns.

In
`@sdks/java/src/main/java/org/byteveda/taskito/internal/JniExecutorControl.java`:
- Around line 88-100: Update awaitSession to acquire the read lock only long
enough to validate that the handle is open and capture the handle, then release
it before calling NativeExecutor.awaitSession. Do not wrap the blocking native
wait in withOpenHandle; preserve closed-state checking while allowing stop() and
close() to acquire their respective locks.

In `@sdks/java/src/main/java/org/byteveda/taskito/worker/Executor.java`:
- Around line 302-315: Extend the try/catch in the Executor setup flow to
include JniExecutorControl creation, bridge.bind, emitter.emit,
resources.acquireWorker, and Executor construction after NativeExecutor.attach
succeeds. If any post-attach operation throws, call NativeExecutor.close(handle)
before rethrowing, while preserving pool shutdown and successful setup behavior.

In `@sdks/node/src/cli/commands/executor.ts`:
- Around line 52-57: Update the signal handler around executor.stop() to catch
rejected promises, report the stop failure, and exit with an intentional
non-zero status; preserve the successful path’s clean exit(0) behavior for both
SIGINT and SIGTERM handlers.

In `@sdks/node/src/executor.ts`:
- Around line 114-120: Update the post-attach setup in the start flow around
resources.acquireWorker() and emitter.emit("worker.started", ...) so any thrown
error triggers cleanup of the successfully attached native executor before
propagating the error. Ensure the attached session is released or stopped and
the original rejection is preserved, while keeping normal Executor creation
unchanged.

In `@sdks/node/src/queue.ts`:
- Around line 1659-1675: Update Queue.runExecutor and Queue.shutdown so
executors created by runExecutor are tracked alongside liveWorkers and stopped
during shutdown, ensuring queue shutdown closes attached scheduler sessions;
otherwise explicitly document in both methods that callers own the executor
lifecycle.

In `@sdks/node/test/worker/executorAttachServer.test.ts`:
- Line 71: Update the server spawn setup in the executor attach test so its
stdout and stderr are either discarded or continuously consumed by the test
logger. Replace the undrained stdio pipes in the spawn call while preserving the
existing SERVER_BIN, env, and process lifecycle behavior.

---

Nitpick comments:
In `@crates/taskito-python/src/executor.rs`:
- Around line 142-148: Update wait so with_handle does not retain the mutex
guard while blocking in handle.wait_timeout. Clone or otherwise share the
required handle state before entering the timeout wait, then release the lock so
stop, is_running, and shutdown can proceed immediately; preserve the existing
timeout result and unwrap_or(true) behavior.

In
`@sdks/java/processor/src/main/java/org/byteveda/taskito/processor/TaskHandlerProcessor.java`:
- Around line 40-41: Annotate TaskHandlerProcessor with
`@IncrementalAnnotationProcessor`(AGGREGATING) alongside its existing processor
annotations. Keep writeServiceFile() and the processors accumulation behavior
unchanged so the service output continues to include all providers during
processingOver().

In `@sdks/java/src/main/java/org/byteveda/taskito/worker/Executor.java`:
- Line 55: Update the closed-state handling in Executor.close() and its closed
field so shutdown is atomic across threads: make the field volatile and use a
compare-and-set guard, or synchronize close(). Ensure only one caller performs
handlerPool.shutdown(), resources.teardownWorker(), and the WORKER_STOPPED event
while preserving idempotent subsequent close() calls.

In `@sdks/java/src/test/java/org/byteveda/taskito/worker/ExecutorAttachTest.java`:
- Line 65: Remove the unused results deque field and its ArrayDeque/Deque
imports, then delete the dead results branch from nextResult. Configure a read
timeout on the accepted socket used by readFrame so its deadline loop can
enforce SETTLE_MS and produce the intended no-result assertion.
- Around line 229-247: Add tests in ExecutorAttachTest covering
Executor.Builder.discover(ClassLoader) with a synthetic
META-INF/services/org.byteveda.taskito.worker.HandlerRegistryProvider resource
and verifying the discovered handler is advertised. Also add a cancel-frame test
exercising JavaDispatcher cancellation, including reclassification of a failed
task as cancelled; reuse existing FakeScheduler and attachment helpers where
applicable.

In `@sdks/node/src/detached.ts`:
- Around line 57-62: Update the warning labels in updateProgress and
writeTaskLog to reflect their public API methods; use the method name for
updateProgress and a shared label covering both log and publish for
writeTaskLog, such as “log/publish”.

In `@sdks/node/src/executor.ts`:
- Around line 176-187: Validate the programmatic run.slots value with the same
positive-integer rules implemented by envInt before passing it to native code.
Reject non-integer or values below 1 with the same error behavior and message
format, while preserving handling for omitted slots.

In `@sdks/node/src/queue.ts`:
- Around line 1665-1670: Update the middlewareFor callback to cache the result
of disables.getFor(taskName) per task name, avoiding repeated settings reads for
dispatched jobs while preserving the existing filtering behavior for disabled
and enabled task middleware.

In `@sdks/node/test/worker/executorAttach.test.ts`:
- Around line 467-469: Remove the redundant process.env.TASKITO_ATTACH =
undefined assignment in the test setup, leaving delete
process.env.TASKITO_ATTACH as the sole operation that clears the environment
variable while preserving the previous value capture.

In `@sdks/node/test/worker/executorAttachServer.test.ts`:
- Around line 38-48: Update freePort and the executor startup test to remove
both race conditions: reserve/select a port without closing the probe before the
server binds, and replace the fixed await sleep(1000) delay with waitFor polling
that uses queue.listWorkers() to confirm the attached executor is ready before
enqueueing work.

In `@sdks/python/taskito/cli.py`:
- Around line 342-406: Register SIGTERM and SIGINT handlers in run_executor
before constructing _Executor, with request_stop safely stopping the attached
executor once available; this must cover the handshake window where jobs may
arrive. Keep the existing signal behavior and move the attachment status print
after handler registration. Wrap the polling and shutdown flow in try/finally so
executor.shutdown() always runs when the loop or wait raises.

In `@sdks/python/tests/worker/executor_apps/attach_app.py`:
- Around line 22-28: Update sdks/python/tests/worker/executor_apps/attach_app.py
lines 22-28 to use a tempfile.gettempdir()-based per-run database filename
instead of the fixed /tmp fallback, and derive db_url from the resolved backend
value. Update sdks/python/tests/worker/test_executor_attach.py lines 241-257 to
set TASKITO_EXECUTOR_TEST_DB to a tmp_path-based value in the test process
before payload_for and _app_importable import attach_app.

In `@sdks/python/tests/worker/test_executor_attach_server.py`:
- Around line 135-137: Replace the fixed time.sleep(1.0) waits in the affected
executor attach tests with a shared readiness helper, such as wait_attached,
that polls the executor process output for its attach confirmation until the
existing timeout and reports early exit or timeout failures. Call this helper
before enqueueing jobs in each affected test, preserving the current test flow
while eliminating timing-dependent waits.

In `@sdks/python/tests/worker/test_executor_attach.py`:
- Around line 129-136: The next_heartbeat helper currently accepts a normal busy
heartbeat when free_slots is zero, so the drain assertion can occur before
SIGTERM takes effect. Update the drain test flow around next_heartbeat and the
SIGTERM send to consume or distinguish pre-signal heartbeats, then assert the
drain-specific heartbeat field or otherwise require a heartbeat emitted after
the signal.
🪄 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: ad8de900-f2ef-4bc8-b7f5-bffad9e29091

📥 Commits

Reviewing files that changed from the base of the PR and between 6f142d2 and 39126be.

📒 Files selected for processing (46)
  • crates/taskito-core/src/lib.rs
  • crates/taskito-core/src/worker/cancel.rs
  • crates/taskito-core/src/worker/dial.rs
  • crates/taskito-core/src/worker/executor.rs
  • crates/taskito-core/src/worker/executor_tests.rs
  • crates/taskito-core/src/worker/mod.rs
  • crates/taskito-core/src/worker/protocol.rs
  • crates/taskito-java/src/dispatcher.rs
  • crates/taskito-java/src/executor.rs
  • crates/taskito-java/src/lib.rs
  • crates/taskito-node/src/dispatcher.rs
  • crates/taskito-node/src/executor.rs
  • crates/taskito-node/src/lib.rs
  • crates/taskito-python/src/executor.rs
  • crates/taskito-python/src/lib.rs
  • sdks/java/processor/src/main/java/org/byteveda/taskito/processor/TaskHandlerProcessor.java
  • sdks/java/src/main/java/org/byteveda/taskito/cli/Cli.java
  • sdks/java/src/main/java/org/byteveda/taskito/internal/JniExecutorControl.java
  • sdks/java/src/main/java/org/byteveda/taskito/internal/MiddlewareDisables.java
  • sdks/java/src/main/java/org/byteveda/taskito/internal/NativeExecutor.java
  • sdks/java/src/main/java/org/byteveda/taskito/worker/Executor.java
  • sdks/java/src/main/java/org/byteveda/taskito/worker/HandlerRegistryProvider.java
  • sdks/java/src/main/java/org/byteveda/taskito/worker/WorkerDispatchBridge.java
  • sdks/java/src/test/java/org/byteveda/taskito/worker/ExecutorAttachTest.java
  • sdks/node/src/cli/commands/executor.ts
  • sdks/node/src/cli/commands/index.ts
  • sdks/node/src/cli/commands/run.ts
  • sdks/node/src/cli/index.ts
  • sdks/node/src/cli/load-app.ts
  • sdks/node/src/detached.ts
  • sdks/node/src/executor.ts
  • sdks/node/src/index.ts
  • sdks/node/src/native.ts
  • sdks/node/src/queue.ts
  • sdks/node/src/task-callback.ts
  • sdks/node/src/worker.ts
  • sdks/node/test/worker/executorAttach.test.ts
  • sdks/node/test/worker/executorAttachServer.test.ts
  • sdks/python/taskito/_taskito.pyi
  • sdks/python/taskito/app.py
  • sdks/python/taskito/cli.py
  • sdks/python/taskito/detached.py
  • sdks/python/tests/worker/executor_apps/__init__.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 crates/taskito-core/src/worker/dial.rs
Comment thread crates/taskito-core/src/worker/executor.rs
Comment thread crates/taskito-core/src/worker/executor.rs
Comment thread crates/taskito-python/src/lib.rs
Comment thread sdks/java/src/main/java/org/byteveda/taskito/worker/Executor.java
Comment thread sdks/node/src/cli/commands/executor.ts Outdated
Comment thread sdks/node/src/executor.ts Outdated
Comment thread sdks/node/src/queue.ts Outdated
Comment thread sdks/node/test/worker/executorAttachServer.test.ts Outdated
stop() is documented as safe on a signal-handling path, but begin_drain wrote the zero-capacity heartbeat first — a scheduler that stopped reading blocked that write for the whole write timeout.
teardown joined unconditionally after the drain budget, so a task that ignores its cancel kept the pool's run alive and the process never exited. The test could not catch it: its blocking behaviour released itself.
The companion is written at package level and names the handler by simple name, so an Outer.Inner provider would not compile. Also corrects the service type in the Javadoc.
System.exit runs hooks while main is still inside Runtime.exit, so a registered hook waited out the whole drain budget for a join that could not complete, then called stop() on an already-closed control.
A throwaway CommandLine routed a bad TASKITO_SLOTS through the execution-exception handler, so the user saw a stack trace rather than a usage message. Zero and negative counts also passed, and the builder clamped them behind the banner.
awaitSession held the read lock for the whole park. A queued close() then blocked a concurrent stop() — the only call that could have ended the session. Waiters are counted instead, and close drains them before freeing.
Nothing awaits a signal listener's return value, so a rejected stop() surfaced as an unhandled rejection and aborted the process mid-drain.
runExecutor tracked nothing, so queue.shutdown() left an attached executor running and its scheduler session open.
Nothing read the pipes, so a scheduler that logs per job fills the buffer and blocks on its next write.
@kartikeya-27

Copy link
Copy Markdown
Contributor

@CodeRabbit full review

@coderabbitai

coderabbitai Bot commented Aug 1, 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: 5

🧹 Nitpick comments (3)
crates/taskito-core/tests/rust/executor_tests.rs (1)

59-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the pool observed shutdown().

TestPool::shutdown records into self.shutdown, but no test reads that field. ExecutorHandle::stop calls self.dispatcher.shutdown(), and spawn_reader calls it again when the session ends. Neither call is covered.

Add an accessor and assert it in the drain test, so a regression that stops signalling the pool is caught.

💚 Proposed test addition
     fn cancelled(&self) -> Vec<String> {
         self.cancels.lock().expect("cancels").clone()
     }
+
+    /// Whether the executor asked the pool to stop accepting work.
+    fn was_shut_down(&self) -> bool {
+        self.shutdown.load(Ordering::SeqCst)
+    }

Then in a_drain_announces_zero_capacity_before_anything_else, or in stop_finishes_in_flight_work_before_disconnecting after the result arrives:

     attached.handle.stop();
+    wait_until(
+        || attached.pool.was_shut_down(),
+        "stop() must tell the pool to stop accepting work",
+    );

Also applies to: 161-163

🤖 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/tests/rust/executor_tests.rs` around lines 59 - 60, Add a
`TestPool` accessor for its recorded `shutdown` flag, then update the
drain-related test (`a_drain_announces_zero_capacity_before_anything_else` or
`stop_finishes_in_flight_work_before_disconnecting`) to assert the accessor is
true after shutdown completes, covering the
`ExecutorHandle::stop`/`spawn_reader` signaling paths.
sdks/java/src/main/java/org/byteveda/taskito/worker/Executor.java (1)

113-138: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Make close() idempotent across threads.

closed is a plain field and close() is not synchronized. Executor is public, so a caller can close it from a shutdown hook while the main thread also closes it. Both threads can then pass the guard, call resources.teardownWorker() twice, and emit WORKER_STOPPED twice. synchronized on close() plus a volatile read keeps the documented idempotence.

♻️ Proposed refactor
-    private boolean closed;
+    private volatile boolean closed;
     `@Override`
-    public void close() {
+    public synchronized void close() {
         if (closed) {
             return;
         }
🤖 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/java/src/main/java/org/byteveda/taskito/worker/Executor.java` around
lines 113 - 138, Make Executor.close() thread-safe and idempotent by
synchronizing the close() method and declaring the closed field volatile.
Preserve the existing early-return guard so concurrent callers allow only one
shutdown sequence, including resources.teardownWorker() and WORKER_STOPPED
emission.
sdks/java/src/test/java/org/byteveda/taskito/worker/ExecutorAttachTest.java (1)

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

Remove the unused results deque.

Nothing adds to results. readFrame() returns each frame directly, so the !results.isEmpty() branch at lines 190-192 is unreachable. Deleting the field and the branch makes the polling loop easier to read.

♻️ Proposed cleanup
-        private final Deque<JsonNode> results = new ArrayDeque<>();
         JsonNode nextResult() throws IOException {
             long deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(SETTLE_MS);
             while (System.nanoTime() < deadline) {
-                if (!results.isEmpty()) {
-                    return results.removeFirst();
-                }
                 JsonNode frame = readFrame();

Drop the now-unused ArrayDeque and Deque imports.

Also applies to: 186-202

🤖 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/java/src/test/java/org/byteveda/taskito/worker/ExecutorAttachTest.java`
at line 65, Remove the unused results field from ExecutorAttachTest, delete the
unreachable results check in the polling loop within readFrame-related logic,
and remove the now-unused ArrayDeque and Deque imports while preserving direct
frame handling.
🤖 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/protocol.rs`:
- Around line 244-293: Update the SchedulerMessage::Job dispatch representation
and SchedulerMessage::into_job to carry and reconstruct created_at,
scheduled_at, priority, metadata, unique_key, and notes instead of defaulting
them. Populate these fields when converting a Job into a dispatch message, and
ensure the Node and Python converters expose the reconstructed values
consistently for detached and attached workers.

In `@crates/taskito-python/src/executor.rs`:
- Around line 142-154: Update Executor::stop and its contract to acknowledge
that concurrent calls may block behind wait’s with_handle mutex for up to
timeout_ms, or otherwise ensure stop uses a separately shared primitive that
does not depend on the polling handle lock. Keep the existing wait behavior and
CLI signal-handling flow unchanged.

In
`@sdks/java/processor/src/main/java/org/byteveda/taskito/processor/TaskHandlerProcessor.java`:
- Around line 35-38: Update the class Javadoc near the generated companion
description to state that an accessible no-arg constructor causes generation of
a nested Provider class implementing HandlerRegistryProvider, which is
registered through META-INF/services; remove the inaccurate provider() method
wording and preserve the explanation of classpath discovery.

In `@sdks/java/src/main/java/org/byteveda/taskito/cli/Cli.java`:
- Around line 294-296: Update the token assignment in Cli.Executor to pass
TASKITO_ATTACH_TOKEN through a blankToNull helper before calling token, treating
null and blank values as absent while preserving nonblank tokens. Add the helper
as a private static nullable method alongside the existing Executor helpers,
matching the address normalization behavior.

In `@sdks/python/tests/worker/test_executor_attach_server.py`:
- Around line 134-138: Replace the fixed one-second attach waits with a shared
polling readiness helper that confirms the executor is attached before
enqueueing. Update sdks/python/tests/worker/test_executor_attach_server.py lines
134-138, 146-150, and 175-178 to call the helper before enqueueing ECHO, BOOM,
and SLOW respectively; preserve each test’s existing enqueue and status
assertions.

---

Nitpick comments:
In `@crates/taskito-core/tests/rust/executor_tests.rs`:
- Around line 59-60: Add a `TestPool` accessor for its recorded `shutdown` flag,
then update the drain-related test
(`a_drain_announces_zero_capacity_before_anything_else` or
`stop_finishes_in_flight_work_before_disconnecting`) to assert the accessor is
true after shutdown completes, covering the
`ExecutorHandle::stop`/`spawn_reader` signaling paths.

In `@sdks/java/src/main/java/org/byteveda/taskito/worker/Executor.java`:
- Around line 113-138: Make Executor.close() thread-safe and idempotent by
synchronizing the close() method and declaring the closed field volatile.
Preserve the existing early-return guard so concurrent callers allow only one
shutdown sequence, including resources.teardownWorker() and WORKER_STOPPED
emission.

In `@sdks/java/src/test/java/org/byteveda/taskito/worker/ExecutorAttachTest.java`:
- Line 65: Remove the unused results field from ExecutorAttachTest, delete the
unreachable results check in the polling loop within readFrame-related logic,
and remove the now-unused ArrayDeque and Deque imports while preserving direct
frame handling.
🪄 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: e32abdfc-cc4f-4a80-bcfe-8c0a07073426

📥 Commits

Reviewing files that changed from the base of the PR and between 6f142d2 and eb452aa.

📒 Files selected for processing (49)
  • crates/taskito-core/src/lib.rs
  • crates/taskito-core/src/worker/cancel.rs
  • crates/taskito-core/src/worker/dial.rs
  • crates/taskito-core/src/worker/executor.rs
  • crates/taskito-core/src/worker/mod.rs
  • crates/taskito-core/src/worker/protocol.rs
  • crates/taskito-core/tests/rust.rs
  • crates/taskito-core/tests/rust/executor_tests.rs
  • crates/taskito-core/tests/rust/remote_tests.rs
  • crates/taskito-core/tests/rust/worker_tests.rs
  • crates/taskito-java/src/dispatcher.rs
  • crates/taskito-java/src/executor.rs
  • crates/taskito-java/src/lib.rs
  • crates/taskito-node/src/dispatcher.rs
  • crates/taskito-node/src/executor.rs
  • crates/taskito-node/src/lib.rs
  • crates/taskito-python/src/executor.rs
  • crates/taskito-python/src/lib.rs
  • sdks/java/processor/src/main/java/org/byteveda/taskito/processor/TaskHandlerProcessor.java
  • sdks/java/src/main/java/org/byteveda/taskito/cli/Cli.java
  • sdks/java/src/main/java/org/byteveda/taskito/internal/JniExecutorControl.java
  • sdks/java/src/main/java/org/byteveda/taskito/internal/MiddlewareDisables.java
  • sdks/java/src/main/java/org/byteveda/taskito/internal/NativeExecutor.java
  • sdks/java/src/main/java/org/byteveda/taskito/worker/Executor.java
  • sdks/java/src/main/java/org/byteveda/taskito/worker/HandlerRegistryProvider.java
  • sdks/java/src/main/java/org/byteveda/taskito/worker/WorkerDispatchBridge.java
  • sdks/java/src/test/java/org/byteveda/taskito/worker/ExecutorAttachTest.java
  • sdks/node/src/cli/commands/executor.ts
  • sdks/node/src/cli/commands/index.ts
  • sdks/node/src/cli/commands/run.ts
  • sdks/node/src/cli/index.ts
  • sdks/node/src/cli/load-app.ts
  • sdks/node/src/detached.ts
  • sdks/node/src/executor.ts
  • sdks/node/src/index.ts
  • sdks/node/src/native.ts
  • sdks/node/src/queue.ts
  • sdks/node/src/task-callback.ts
  • sdks/node/src/worker.ts
  • sdks/node/test/worker/executorAttach.test.ts
  • sdks/node/test/worker/executorAttachServer.test.ts
  • sdks/python/taskito/_taskito.pyi
  • sdks/python/taskito/app.py
  • sdks/python/taskito/cli.py
  • sdks/python/taskito/detached.py
  • sdks/python/tests/worker/executor_apps/__init__.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 crates/taskito-core/src/worker/protocol.rs
Comment thread crates/taskito-python/src/executor.rs
Comment thread sdks/java/src/main/java/org/byteveda/taskito/cli/Cli.java Outdated
Comment thread sdks/python/tests/worker/test_executor_attach_server.py
@kartikeya-27
kartikeya-27 merged commit 90be320 into ByteVeda:master Aug 1, 2026
31 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

deployment Container and cluster deployment topology enhancement New feature or request java Java SDK node Node SDK python rust tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Java: executor subcommand plus classpath handler discovery Node: taskito executor subcommand Python: taskito executor subcommand

3 participants