Skip to content

feat: add a retry predicate to the Node and Java SDKs#498

Merged
pratyush618 merged 4 commits into
ByteVeda:masterfrom
stromanni:feat/retry-predicate-node-java
Jul 22, 2026
Merged

feat: add a retry predicate to the Node and Java SDKs#498
pratyush618 merged 4 commits into
ByteVeda:masterfrom
stromanni:feat/retry-predicate-node-java

Conversation

@stromanni

@stromanni stromanni commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Closes #479.

Python can already decide per-error whether a failure is worth retrying
(retry_on / dont_retry_on). Node and Java could not — both dispatchers
hardcoded should_retry: true, so every failure burned the whole retry budget
and a permanently-broken payload took the full backoff curve before reaching the
DLQ.

API

One predicate per task, evaluated against the live error rather than a type name:

queue.task("parseData", parseData, {
  maxRetries: 5,
  retryOn: (error) => !(error instanceof SyntaxError),
});
Task<String> PARSE = Task.of("parse", String.class)
        .maxRetries(5)
        .retryOn(error -> !(error instanceof IllegalArgumentException));

Returning false dead-letters the job at once, whatever budget is left. Unset
retries everything, and so does a predicate that itself throws — a broken
classifier must not silently turn transient failures into dead letters. The
predicate is synchronous (the job cannot settle until it answers) and covers
both of Python's whitelist and blacklist forms.

No core change: scheduler/result_handler.rs already routes should_retry: false straight to the DLQ.

Scope

The predicate rules on every error raised while running the task, not only the
handler's — before/after middleware hooks and result serialization can fail
too (on the Java side also payload decoding and the middleware-disable read), so
a whitelist predicate dead-letters those as well. That is deliberate: a result
blob that will not serialize never becomes serializable on a later attempt, so
letting the predicate rule on it beats retrying it forever. Errors raised before
the task starts (Node's payload decode, an unregistered task) and timeouts sit
outside the predicate and always consume a retry, matching Python.

Notes on placement and plumbing

Java: the predicate lives on Task, not RetryPolicy. RetryPolicy is
wire-encoded into the per-task config the native scheduler reads
(Worker.encodeRetryPolicy), and a Java lambda cannot cross that boundary. It
rides on RegisteredTask to WorkerDispatchBridge instead, and is deliberately
kept out of Worker.Builder.hasPolicy() so a predicate-only task doesn't push
an empty config to the native side. failJob gains a retryable flag through
the JNI entry point and TaskOutcome::Failure.

Node needed a protocol change. The JS callback used to smuggle the canonical
error JSON out through a rejection (new Error(json) with name = "" so
String(err) was the bare JSON); napi hands Rust only that string, with no room
for a verdict. The callback now resolves a JsTaskOutcome
({ result?, error?, retryable? }), which also removes that hack. A rejection
still exists but now means only "the shell threw before it could shape an
outcome" — an unregistered task, an undecodable payload — and stays retryable,
exactly as before.

Verification

  • cargo check --workspace (default / postgres / redis), cargo test --workspace (also --features workflows), cargo clippy --workspace --all-targets — clean.
  • Java: ./gradlew test — 378 tests, 0 failures, including the new
    RetryPredicateTest (rejected exception dead-letters after one attempt and
    emits no RETRY; accepted exception still spends its budget).
  • Node: tsc, biome check, and vitest including the new retryOn.test.ts
    (rejected error, accepted error, throwing predicate). The 9 dashboard test
    files fail locally only because vite isn't installed in dashboard/.
  • pnpm --dir docs build — the Node and Java retry pages now render retryOn
    and the "no exception-type filter" note is gone.

The JS callback now resolves a JsTaskOutcome instead of rejecting, so a
failure can carry its retry verdict — a napi rejection is only a string.
Predicate lives on Task, not RetryPolicy: RetryPolicy is wire-encoded into
the scheduler's per-task config, which a lambda cannot cross.
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Node and Java tasks now support per-error retry predicates. Retryability is carried through SDK worker execution, structured task outcomes, JNI/N-API bridges, dispatchers, and queue backends, with tests and documentation covering accepted, rejected, and predicate-failure cases.

Changes

Retry Predicate Support

Layer / File(s) Summary
Java task retry configuration
sdks/java/src/main/java/org/byteveda/taskito/task/Task.java, sdks/java/src/main/java/org/byteveda/taskito/worker/*, docs/content/docs/java/..., docs/content/docs/shared/...
Java tasks expose retryOn(Predicate<Throwable>), preserve it across copied task descriptors, and pass it into registered handlers.
Java failure retry transport
sdks/java/src/main/java/org/byteveda/taskito/{internal,spi,worker}/..., crates/taskito-java/src/{dispatcher,worker}.rs, sdks/java/test-support/..., sdks/java/src/test/...
Failure calls carry retryability through Java control APIs, JNI, Rust outcomes, and the in-memory backend; tests cover immediate dead-lettering and budgeted retries.
Node structured task outcomes
sdks/node/src/{native,types,worker}.ts, crates/taskito-node/src/convert/..., sdks/node/test/worker/retryOn.test.ts, docs/content/docs/node/..., docs/content/docs/shared/...
Node callbacks return structured success or failure objects, and retryOn classifies failures while predicate exceptions remain retryable.
Node dispatcher retry mapping
crates/taskito-node/src/dispatcher.rs
The dispatcher converts structured callback outcomes into internal results and sets JobResult::Failure.should_retry from the callback metadata.

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

Sequence Diagram(s)

sequenceDiagram
  participant TaskHandler
  participant Worker
  participant Dispatcher
  participant QueueBackend
  TaskHandler->>Worker: Throw error
  Worker->>Worker: Evaluate retryOn
  Worker->>Dispatcher: Return error and retryable
  Dispatcher->>QueueBackend: Submit retry-aware failure
  QueueBackend->>QueueBackend: Retry or dead-letter
Loading

Possibly related PRs

  • ByteVeda/taskito#300: Adds Java worker dispatch plumbing used by the retryability propagation changes.
  • ByteVeda/taskito#413: Modifies the Node dispatcher failure path touched by this retryability update.

Suggested reviewers: pratyush618

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.32% 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
Linked Issues check ✅ Passed The PR implements the linked retry_on predicate for both Node and Java and adds supporting docs/tests.
Out of Scope Changes check ✅ Passed The docs and tests support the retry predicate feature and do not appear unrelated to the PR scope.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: adding a retry predicate to the Node and Java SDKs.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@kartikeya-27
kartikeya-27 requested a review from pratyush618 July 22, 2026 15:41

@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.

Caution

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

⚠️ Outside diff range comments (1)
sdks/node/src/worker.ts (1)

182-212: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

retryOn classifies more than "errors the handler throws."

The TaskOptions.retryOn doc says the predicate applies to errors the handler throws, but this try block also wraps mw.before/mw.after hooks and serializer.serialize(result). An error from any of those is routed through the same isRetryable(task, error) call as a genuine handler failure, so a predicate scoped to handler-specific error types could inadvertently dead-letter a middleware or serialization failure that has nothing to do with task-level retry semantics.

Either narrow the predicate's scope to the handler invocation specifically, or update the retryOn doc in sdks/node/src/types.ts to state it covers any error surfaced during this try block (middleware hooks and result serialization included), not just handler-thrown errors.

🤖 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/worker.ts` around lines 182 - 212, Update the retryOn
documentation in TaskOptions within types.ts to state that its predicate
receives any error surfaced during task execution, including middleware
before/after hooks and result serialization, rather than only errors thrown by
the handler. Keep the existing isRetryable behavior unchanged.
🤖 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.

Outside diff comments:
In `@sdks/node/src/worker.ts`:
- Around line 182-212: Update the retryOn documentation in TaskOptions within
types.ts to state that its predicate receives any error surfaced during task
execution, including middleware before/after hooks and result serialization,
rather than only errors thrown by the handler. Keep the existing isRetryable
behavior unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 29eda922-5a5b-4920-83d1-a37ad77bc567

📥 Commits

Reviewing files that changed from the base of the PR and between 2936976 and 78d4761.

📒 Files selected for processing (21)
  • crates/taskito-java/src/dispatcher.rs
  • crates/taskito-java/src/worker.rs
  • crates/taskito-node/src/convert/job.rs
  • crates/taskito-node/src/convert/mod.rs
  • crates/taskito-node/src/dispatcher.rs
  • docs/content/docs/java/api-reference/task.mdx
  • docs/content/docs/node/api-reference/task.mdx
  • docs/content/docs/shared/guides/reliability/retries.mdx
  • sdks/java/src/main/java/org/byteveda/taskito/internal/JniWorkerControl.java
  • sdks/java/src/main/java/org/byteveda/taskito/internal/NativeWorker.java
  • sdks/java/src/main/java/org/byteveda/taskito/spi/WorkerControl.java
  • sdks/java/src/main/java/org/byteveda/taskito/task/Task.java
  • sdks/java/src/main/java/org/byteveda/taskito/worker/RegisteredTask.java
  • sdks/java/src/main/java/org/byteveda/taskito/worker/Worker.java
  • sdks/java/src/main/java/org/byteveda/taskito/worker/WorkerDispatchBridge.java
  • sdks/java/src/test/java/org/byteveda/taskito/worker/RetryPredicateTest.java
  • sdks/java/test-support/src/main/java/org/byteveda/taskito/test/InMemoryQueueBackend.java
  • sdks/node/src/native.ts
  • sdks/node/src/types.ts
  • sdks/node/src/worker.ts
  • sdks/node/test/worker/retryOn.test.ts

The predicate also sees middleware, decode and serialization failures — a
whitelist dead-letters those too.
@stromanni

Copy link
Copy Markdown
Contributor Author

Valid — the try block also wraps the before/after middleware hooks and result serialization (and, on the Java side, payload decoding and the disable-list read), so a whitelist predicate dead-letters those too.

Fixed by widening the docs rather than narrowing the scope, in b9a2442. Narrowing would make a serialization failure permanently retryable, which is worse — a bad result blob never becomes serializable on the next attempt, so letting the predicate rule on it is the useful behavior. Updated TaskOptions.retryOn (node), Task.retryOn javadoc (java), and both <SdkOnly> blocks in the shared retries guide; each now names the exact boundary and notes that payload decode and timeouts still always retry.

@pratyush618
pratyush618 merged commit 4c75684 into ByteVeda:master Jul 22, 2026
34 checks passed
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 a retry predicate (retry_on) to the Node and Java SDKs

2 participants