Skip to content

feat(java): incremental workflow caching#337

Merged
pratyush618 merged 4 commits into
masterfrom
feat/java-workflows-incremental
Jun 30, 2026
Merged

feat(java): incremental workflow caching#337
pratyush618 merged 4 commits into
masterfrom
feat/java-workflows-incremental

Conversation

@pratyush618

@pratyush618 pratyush618 commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

Adds opt-in result caching for workflow steps: a step marked cache(ttl) whose
result is still fresh from a prior run is marked a cache hit and skipped instead
of re-running its task.

API

  • Step.cache(Duration ttl) — make a step cacheable for ttl.

Mechanics

  • A cacheable step is a deferred node. On promotion the tracker checks an
    in-memory resultCache keyed by workflow:node:sha256(payload); within TTL it
    calls the new setWorkflowNodeCacheHit JNI fn (marks the node CACHE_HIT,
    terminal) and advances successors without enqueuing the task. On a miss it
    enqueues normally and caches the result on completion.
  • New JNI fn setWorkflowNodeCacheHit + cache field on StepSpec/PlanNodeView.

Scope (documented limitation)

  • The cache is per worker process / non-durable — it accelerates re-runs
    within a live worker, not across restarts (same posture as the rest of the
    in-process tracker). A cacheable node also needs a predecessor (a deferred
    root isn't promoted).

Tests

WorkflowCacheTest covers a cache hit on re-run skipping the task and a TTL
expiry forcing re-execution.

Stacked after #335 (saga). Rebased onto master.

Summary by CodeRabbit

  • New Features

    • Added workflow step caching with a configurable TTL, so repeated runs can reuse recent step results.
    • Workflow plans now include cache information for eligible steps.
    • Added support for marking a workflow node as a cache hit and skipping execution.
  • Bug Fixes

    • Cached steps can now advance the workflow immediately on reruns when the cached result is still valid.
  • Tests

    • Added coverage for rerunning a workflow and verifying cached steps are not executed again.

Step.cache(ttl) reuses a step's result across runs of the same workflow:
when its task + payload are unchanged and within the TTL, the tracker
marks it a cache hit and skips re-running it. Adds a setWorkflowNodeCacheHit
JNI fn + cache metadata; cache state is per worker process.
A re-run of a cacheable step is a cache hit, not re-executed.
@coderabbitai

coderabbitai Bot commented Jun 30, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 23 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: c1b5b1f5-5706-477c-8991-fd7c6fd7e847

📥 Commits

Reviewing files that changed from the base of the PR and between 1ac5c7e and b28eae0.

📒 Files selected for processing (4)
  • sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java
  • sdks/java/src/main/java/org/byteveda/taskito/workflows/Step.java
  • sdks/java/src/main/java/org/byteveda/taskito/workflows/WorkflowTracker.java
  • sdks/java/src/test/java/org/byteveda/taskito/WorkflowCacheTest.java
📝 Walkthrough

Walkthrough

Adds TTL-based result caching for workflow steps. A new Step.Builder.cache(Duration) API propagates cache config through submission serialization into stored Rust metadata and the workflow plan JSON. WorkflowTracker maintains an in-memory TTL map and promotes cache hits via a new setWorkflowNodeCacheHit backend method (wired through QueueBackend, JniQueueBackend, NativeWorkflows, and a Rust JNI function) instead of dispatching deferred jobs.

Changes

Workflow Step Caching

Layer / File(s) Summary
Step.Builder cache API and PlanNode cache field
sdks/java/src/main/java/org/byteveda/taskito/workflows/Step.java, sdks/java/src/main/java/org/byteveda/taskito/workflows/PlanNode.java
Step gains cacheTtlMs field; Builder.cache(Duration) validates and stores TTL as milliseconds. PlanNode gains a matching cache field deserialized from plan JSON.
Cache config serialization on submission
sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java, crates/taskito-java/src/workflows/mod.rs
deferredNodes treats cacheTtlMs steps as deferred; stepSpec emits a cache:{ttlMs} object. Rust StepSpec and StepMetadata accept and store the cache field.
Plan propagation of cache metadata
crates/taskito-java/src/workflows/mod.rs
PlanNodeView gains a cache field populated from stored step metadata so the Java tracker receives cache config per node.
WorkflowTracker TTL cache and promotion logic
sdks/java/src/main/java/org/byteveda/taskito/workflows/WorkflowTracker.java
Adds resultCache map, records TTL expiry on node completion, checks validity during successor promotion, and short-circuits to setWorkflowNodeCacheHit on hit. Adds SHA-256 key helpers and CacheMeta JSON class.
QueueBackend interface and JNI wiring
sdks/java/src/main/java/org/byteveda/taskito/spi/QueueBackend.java, sdks/java/src/main/java/org/byteveda/taskito/internal/NativeWorkflows.java, sdks/java/src/main/java/org/byteveda/taskito/internal/JniQueueBackend.java, crates/taskito-java/src/workflows/mod.rs
QueueBackend adds default setWorkflowNodeCacheHit; JniQueueBackend overrides it; NativeWorkflows declares the native method; Rust JNI function sets node status to CacheHit.
End-to-end cache test
sdks/java/src/test/java/org/byteveda/taskito/WorkflowCacheTest.java
Runs a three-step workflow twice, asserting compute is CACHE_HIT on rerun and executed exactly once.

Sequence Diagram(s)

sequenceDiagram
  participant DefaultTaskito
  participant WorkflowTracker
  participant resultCache
  participant JniQueueBackend
  participant RustJNI

  DefaultTaskito->>RustJNI: submitWorkflowDef(stepSpec with cache:{ttlMs})
  RustJNI-->>DefaultTaskito: stored in StepMetadata.cache

  WorkflowTracker->>RustJNI: getWorkflowPlan()
  RustJNI-->>WorkflowTracker: PlanNode with cache field

  Note over WorkflowTracker: On node COMPLETED
  WorkflowTracker->>resultCache: store cacheKey → expiry

  Note over WorkflowTracker: On successor promotion
  WorkflowTracker->>resultCache: reuseFromCache(cacheKey)?
  alt Cache hit
    WorkflowTracker->>JniQueueBackend: setWorkflowNodeCacheHit(runId, nodeName)
    JniQueueBackend->>RustJNI: NativeWorkflows.setWorkflowNodeCacheHit(handle, runId, nodeName)
    RustJNI-->>WorkflowTracker: node → CacheHit
    WorkflowTracker->>WorkflowTracker: promoteSuccessors immediately
  else Cache miss
    WorkflowTracker->>JniQueueBackend: createDeferredJob(...)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • ByteVeda/taskito#306: Extends the same StepSpec/StepMetadataPlanNodeWorkflowTracker pipeline that this PR builds on for the cache attribute and setWorkflowNodeCacheHit integration.

Poem

A bunny hops twice down the workflow lane,
The second time 'round, no need to compute again!
A SHA-256 key and a TTL clock,
Cache hit! No re-run — just skip and unlock. 🐇✨
The tracker winks: "I've seen this before!"

🚥 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 is concise and accurately summarizes the main change: adding incremental workflow caching for Java workflows.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/java-workflows-incremental

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

@github-actions github-actions Bot added the rust label Jun 30, 2026

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

Caution

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

⚠️ Outside diff range comments (2)
crates/taskito-java/src/workflows/mod.rs (1)

221-239: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Definition reuse now ignores cache-policy changes.

step_meta now stores cache, but the reuse check below still keys definition identity only on dag_data. Re-submitting the same workflow name/version with identical edges but a different cache TTL (or adding/removing caching entirely) will silently reuse the old definition, so the new policy never reaches getWorkflowPlan. Please include relevant step metadata in the reuse comparison or reject same-version submissions when metadata changes.

🤖 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-java/src/workflows/mod.rs` around lines 221 - 239, The
workflow definition reuse check in the `step_meta`/`getWorkflowPlan` path is
still comparing only `dag_data`, so cache-policy changes can be missed. Update
the reuse logic in `workflows::mod` to include relevant step metadata from
`StepMetadata`—at least `cache`, and any other fields that affect execution
identity—when deciding whether a same-name/version workflow can be reused, or
explicitly reject submissions when those metadata fields differ.
sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java (1)

408-421: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Cache misses can be recreated on the wrong queue.

Adding step.cacheTtlMs != null here makes cacheable steps use the deferred-job path on a miss. getWorkflowPlan still only carries the per-step queue override, and PlanNode.queueOrDefault() falls back to "default", so a cacheable step without explicit .queue(...) can stall named-queue workflows by being recreated on the wrong lane. Either propagate the run default queue through the plan or reject cacheable deferred steps that do not set queue. Based on learnings, explicitly call .queue(...) on deferred steps because getWorkflowPlan may omit the run default queue and later break deferred job creation in WorkflowTracker.

🤖 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/DefaultTaskito.java` around
lines 408 - 421, The deferred-node selection in DefaultTaskito.deferredNodes is
now sending cacheable steps through the deferred-job path, but the plan data
used by WorkflowTracker/PlanNode.queueOrDefault() does not reliably preserve the
run’s default queue. Update the deferred-step handling so cacheable steps either
carry an explicit queue through getWorkflowPlan or are rejected unless
Step.queue is set; ensure deferred creation never falls back to "default" for
named-queue workflows, and reference the Step.cacheTtlMs, getWorkflowPlan, and
PlanNode.queueOrDefault flow when fixing it.

Source: Learnings

🤖 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/java/src/main/java/org/byteveda/taskito/workflows/Step.java`:
- Around line 215-227: The Step.Builder.cache(...) path currently allows
cacheable steps to be attached without predecessors, which creates root
cacheable steps that DefaultTaskito.deferredNodes(...) will defer forever and
never let WorkflowTracker promote to a terminal state. Update the build-time
validation in Step.Builder so that when cacheTtlMs is set and after is empty the
workflow is rejected immediately with an IllegalArgumentException, using the
existing cache(...) / after state in Step.Builder to catch this invalid
configuration before the workflow is built.

In `@sdks/java/src/main/java/org/byteveda/taskito/workflows/WorkflowTracker.java`:
- Around line 72-73: The issue is that WorkflowTracker.resultCache keeps expired
cache keys forever, causing unbounded growth on long-lived workers. Update the
caching flow in WorkflowTracker, especially cacheResult(...) and
reuseFromCache(...), so stale entries are actively removed when they expire
rather than only being ignored on reads. Use the existing resultCache and cache
key logic to either delete expired entries on access or replace the map with a
bounded TTL cache implementation that evicts old keys automatically.
- Around line 128-133: Cache reuse is bypassing the normal success flow in
WorkflowTracker, so cached nodes never run the same post-success hooks as a real
success. Update reuseFromCache(...) to restore the cached step’s serialized
result (or equivalent handle) and then execute the same success path used by
onOutcome(..., true, ...) so expandFanOutIfAny(...) and any callable-condition
context can see the predecessor result. Also ensure cacheResult(...) persists
that result data, not just expiry metadata, so buildContext(...) can reconstruct
it for later runs.

---

Outside diff comments:
In `@crates/taskito-java/src/workflows/mod.rs`:
- Around line 221-239: The workflow definition reuse check in the
`step_meta`/`getWorkflowPlan` path is still comparing only `dag_data`, so
cache-policy changes can be missed. Update the reuse logic in `workflows::mod`
to include relevant step metadata from `StepMetadata`—at least `cache`, and any
other fields that affect execution identity—when deciding whether a
same-name/version workflow can be reused, or explicitly reject submissions when
those metadata fields differ.

In `@sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java`:
- Around line 408-421: The deferred-node selection in
DefaultTaskito.deferredNodes is now sending cacheable steps through the
deferred-job path, but the plan data used by
WorkflowTracker/PlanNode.queueOrDefault() does not reliably preserve the run’s
default queue. Update the deferred-step handling so cacheable steps either carry
an explicit queue through getWorkflowPlan or are rejected unless Step.queue is
set; ensure deferred creation never falls back to "default" for named-queue
workflows, and reference the Step.cacheTtlMs, getWorkflowPlan, and
PlanNode.queueOrDefault flow when fixing it.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3167cee9-c5b4-4696-92c6-7e8f230b6974

📥 Commits

Reviewing files that changed from the base of the PR and between cb733a1 and 1ac5c7e.

📒 Files selected for processing (9)
  • crates/taskito-java/src/workflows/mod.rs
  • sdks/java/src/main/java/org/byteveda/taskito/DefaultTaskito.java
  • sdks/java/src/main/java/org/byteveda/taskito/internal/JniQueueBackend.java
  • sdks/java/src/main/java/org/byteveda/taskito/internal/NativeWorkflows.java
  • sdks/java/src/main/java/org/byteveda/taskito/spi/QueueBackend.java
  • sdks/java/src/main/java/org/byteveda/taskito/workflows/PlanNode.java
  • sdks/java/src/main/java/org/byteveda/taskito/workflows/Step.java
  • sdks/java/src/main/java/org/byteveda/taskito/workflows/WorkflowTracker.java
  • sdks/java/src/test/java/org/byteveda/taskito/WorkflowCacheTest.java

Comment thread sdks/java/src/main/java/org/byteveda/taskito/workflows/Step.java
@pratyush618
pratyush618 merged commit 3b1beee into master Jun 30, 2026
35 checks passed
@pratyush618
pratyush618 deleted the feat/java-workflows-incremental branch June 30, 2026 08:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant