Skip to content

batch action and query starts into single scheduled function#212

Merged
ianmacartney merged 10 commits into
mainfrom
ian/batch-try-catch-actions
Jul 25, 2026
Merged

batch action and query starts into single scheduled function#212
ianmacartney merged 10 commits into
mainfrom
ian/batch-try-catch-actions

Conversation

@ianmacartney

Copy link
Copy Markdown
Member

Actions and queries are now started together in a single batched runWork action wrapper, rather than each being scheduled individually. This reduces the number of scheduled function calls when multiple actions or queries are pending at once. Batches are capped at 32 items (ACTION_START_BATCH_SIZE) to keep individual invocations bounded. Mutations continue to be scheduled one at a time via runMutationWrapper, as before.

The beginWork function has been replaced by beginWorkBatch, which accepts a list of pending starts, separates them by function type, and schedules the appropriate wrappers. The runActionWrapper now delegates to the shared runActionOrQueryJob and completeActionOrQueryJobs helpers, which are also used by the new runWork action. Query execution is handled within the same action wrapper path as actions.

ianmacartney commented Jun 24, 2026

Copy link
Copy Markdown
Member Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@ianmacartney ianmacartney mentioned this pull request Jun 24, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jun 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/get-convex/workpool/@convex-dev/workpool@212

commit: 5d9aa7f

@ianmacartney
ianmacartney changed the base branch from ian/batch-worker to graphite-base/212 July 15, 2026 21:19
@ianmacartney
ianmacartney force-pushed the ian/batch-try-catch-actions branch from 84591e1 to 2dfc248 Compare July 23, 2026 21:05
@ianmacartney
ianmacartney changed the base branch from graphite-base/212 to main July 23, 2026 21:05
@ianmacartney
ianmacartney force-pushed the ian/batch-try-catch-actions branch from 2dfc248 to ad4ea78 Compare July 23, 2026 21:10
@ianmacartney
ianmacartney marked this pull request as ready for review July 24, 2026 02:29
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

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

Next review available in: 13 minutes

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

How can I continue?

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

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

How do review limits work?

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

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

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository: get-convex/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: dda73c25-080e-481b-91f8-40ea3737cf65

📥 Commits

Reviewing files that changed from the base of the PR and between 4ff7a9a and 5d9aa7f.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • CHANGELOG.md
  • package.json
  • src/component/worker.ts
📝 Walkthrough

Walkthrough

The component loop batches action and query starts through runBatch, while mutation starts remain individually scheduled. Workers execute batch items concurrently, isolate failures, and use single-item completion fallbacks. Completion tolerates missing payload records. The example adds noisy action/query workloads, contention counters, configurable presets, polling, and per-label metrics.

Sequence Diagram(s)

sequenceDiagram
  participant S as Scenario
  participant L as Loop
  participant B as runBatch
  participant C as noisyOnComplete
  participant M as metricsByLabel
  S->>L: schedule noisy tasks
  L->>B: submit action and query batch
  B->>C: record terminal result
  S->>M: poll run metrics
  M-->>S: return status and latency metrics
Loading

Possibly related PRs

Suggested reviewers: reeceyang

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: batching action and query starts into one scheduled function.
Description check ✅ Passed The description is directly related to the batching and scheduling changes described in the pull request.
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 ian/batch-try-catch-actions

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

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 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 `@example/convex/test/scenarios/noisyNeighbor.ts`:
- Around line 1-31: Run Prettier on the noisyNeighbor scenario file and commit
the resulting formatting changes, without altering its behavior or making
unrelated edits.

In `@src/component/worker.ts`:
- Around line 113-149: Limit concurrency in runWork so it does not launch all
items simultaneously under one parent action, using a workload-appropriate bound
that keeps the wrapper within Convex action budgets. Preserve independent
per-item completion and failure scheduling, and ensure a slow child cannot
terminate the entire batch.
🪄 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: Repository: get-convex/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 39bf4510-e4a1-4d98-8752-f7a28d4222db

📥 Commits

Reviewing files that changed from the base of the PR and between 665faef and 610143a.

⛔ Files ignored due to path filters (1)
  • example/convex/_generated/api.d.ts is excluded by !**/_generated/**
📒 Files selected for processing (9)
  • example/convex/schema.ts
  • example/convex/test/PLAN.md
  • example/convex/test/noisy.ts
  • example/convex/test/scenarios/noisyNeighbor.ts
  • src/component/complete.test.ts
  • src/component/complete.ts
  • src/component/loop.test.ts
  • src/component/loop.ts
  • src/component/worker.ts

Comment thread example/convex/test/scenarios/noisyNeighbor.ts
Comment thread src/component/worker.ts Outdated
Comment on lines +113 to +149
export const runWork = internalAction({
args: {
items: v.array(v.object(actionOrQueryRunArgs)),
logLevel,
},
handler: async (ctx, { items, logLevel }) => {
const console = createLogger(logLevel);
// Complete each job independently, running its onComplete inline. Only the
// job that hits an OCC gets scheduled (as a batch of one) — a conflicting
// onComplete can no longer drag the rest of the batch into a retry, since
// arbitrary user code in one onComplete shouldn't couple the others.
await Promise.all(
items.map(async (item) => {
// No single item may reject the batch: a thrown error here would fail
// the whole action and take the other in-flight items down with it.
try {
const job = await runActionOrQueryJob(ctx, console, item);
await completeJobInline(ctx, console, job);
} catch (e) {
console.error(
`[runWork] unexpected error for ${item.workId}, scheduling failed completion: ${e}`,
);
await ctx.scheduler.runAfter(0, internal.complete.complete, {
jobs: [
{
workId: item.workId,
runResult: { kind: "failed", error: formatError(e) },
attempt: item.attempt,
nonRetryable: isNonRetryableError(e),
},
],
});
}
}),
);
},
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Convex ctx.runAction from action concurrent execution time and resource limits

💡 Result:

When using ctx.runAction in Convex, it is important to understand its impact on execution and resource allocation. Execution Time and Resource Limits 1. Time Limits: Convex actions currently have execution time limits of 30 minutes for the Convex runtime and 10 minutes for the Node.js runtime [1][2]. Note that ctx.runAction is a blocking call; while it is executing, the parent action remains frozen and continues to consume its own allocated resources [3][4]. 2. Resource Overhead: The use of ctx.runAction incurs significant overhead because it initiates an entirely separate function call with its own system resource allocation [3][4]. 3. Concurrency Limits: Actions are subject to deployment-class concurrency limits, which vary depending on your plan (e.g., S16, S256, D1024, D2048) [1][2]. These limits constrain the number of functions that can run concurrently, including actions [1][2]. Additionally, individual actions can perform up to 1,000 concurrent I/O operations, such as database queries or fetch requests [3][1]. Best Practices The primary use case for ctx.runAction is to facilitate cross-runtime communication—specifically, calling a "use node" action from the default Convex runtime [3][4][5]. For logic that resides within the same runtime, using ctx.runAction is discouraged. Instead, you should extract shared code into a plain TypeScript helper function [3][4][5]. This approach is more efficient, as it avoids the overhead of a separate function invocation and the associated resource consumption of a secondary Convex execution context [3][4]. Self-Hosted Considerations If you are running a self-hosted Convex instance, execution limits and concurrency can be adjusted via environment variables. If you encounter errors related to concurrent function limits, you can configure thresholds such as APPLICATION_MAX_CONCURRENT_NODE_ACTIONS and APPLICATION_MAX_CONCURRENT_V8_ACTIONS to suit your hardware and workload requirements [6][7]. Timeout limits for self-hosted actions can also be managed via specific configuration knobs like ACTIONS_USER_TIMEOUT_SECS [8].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a 'worker\.ts$' . | sed 's#^\./##'

echo "== worker outline =="
worker=$(fd 'worker\.ts$' . | head -n1)
[ -n "${worker:-}" ] && ast-grep outline "$worker" --view expanded || true

echo "== relevant worker section =="
[ -n "${worker:-}" ] && nl -ba "$worker" | sed -n '1,220p'

echo "== ACTION_START_BATCH_SIZE references =="
rg -n "ACTION_START_BATCH_SIZE|ACTION_RUN_BATCH_SIZE|createAction|internalAction|ctx\.runAction|ctx\.runQuery" -S .

Repository: get-convex/workpool

Length of output: 774


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== relevant worker section =="
awk '{printf "%6d\t%s\n", NR, $0}' src/component/worker.ts | sed -n '1,260p'

echo "== batching and ctx.run* references =="
awk '{printf "%6d\t%s\n", NR, $0}' src/component/worker.ts | rg -n "ACTION_START_BATCH_SIZE|ACTION_RUN_BATCH_SIZE|int.*action|internalAction|ctx\.runAction|ctx\.runQuery|Promise\.all|map|batch" || true

echo "== all relevant action/job/config references =="
rg -n "ACTION_START_BATCH_SIZE|ACTION_RUN_BATCH_SIZE|int.*action|internalAction|ctx\.runAction|ctx\.runQuery|Promise\.all|map|completeJobInline|runActionOrQueryJob|runWork" src component . --glob '!node_modules' --glob '!dist' --glob '!build' | head -n 250

Repository: get-convex/workpool

Length of output: 30929


Bound the runWork fan-out to fit Convex action budgets.

runWork starts up to ACTION_START_BATCH_SIZE (32) child ctx.runAction/ctx.runQuery calls under one parent action. Since a child ctx.runAction blocks the parent until completion, one heavy child can exhaust the parent action’s wall-clock or resource budget and terminate the entire batch. Make the batch size workload-specific or limit concurrency/long-child protection so a single slow item cannot take out the wrapper action.

🤖 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 `@src/component/worker.ts` around lines 113 - 149, Limit concurrency in runWork
so it does not launch all items simultaneously under one parent action, using a
workload-appropriate bound that keeps the wrapper within Convex action budgets.
Preserve independent per-item completion and failure scheduling, and ensure a
slow child cannot terminate the entire batch.

@ianmacartney
ianmacartney requested a review from reeceyang July 24, 2026 03:32

@reeceyang reeceyang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

very exciting!

Comment thread src/component/worker.ts Outdated
await ctx.runMutation(internal.complete.complete, { jobs: [job] });
} catch (e) {
console.error(
`[runBatch] completing ${job.workId} inline failed, scheduling a batch of one instead: ${e}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

minor nit, but I wonder if "batch of one" might be misleading in this error message, as a "batch" elsewhere refers to work, but here we are just scheduling the completion, not the work itself

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

thanks! updated

@ianmacartney
ianmacartney merged commit ead29f2 into main Jul 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants