Skip to content

feat: execute a request's invocations concurrently - #66

Merged
bajtos merged 5 commits into
miroslav/fil-1261-recover-validation-panics-v2from
miroslav/fil-1261-concurrent-batch-execution
Sep 23, 2026
Merged

bajtos merged 5 commits into
miroslav/fil-1261-recover-validation-panics-v2from
miroslav/fil-1261-concurrent-batch-execution

Conversation

@bajtos

@bajtos bajtos commented Sep 22, 2026 •

Copy link
Copy Markdown
Member

Written by Claude.

The server ran the invocations of one request one after another, so a request carrying 100 invocations took longer than 100 requests carrying one each, and batching made large multipart uploads slower rather than faster.

Each invocation now runs on its own goroutine, the way net/http runs each connection, admitted through a per-request semaphore the way HTTP/2 and QUIC servers cap concurrent streams per connection. server.WithMaxConcurrency(n) sets the cap. The default of 100 matches quic-go's MaxIncomingStreams and the floor RFC 9113 recommends; x/net/http2 uses 250. Zero removes the cap for deployments that bound concurrency at the listener, and a negative value panics. The semaphore is acquired before spawning, so an oversized request waits in the loop rather than as a pile of blocked goroutines.

Receipts and metadata are collected into a slice indexed by request position and merged after every invocation finishes, so the response is the same regardless of which handler finished first and needs no lock. Dispatcher errors are joined and surface after all siblings complete; they still fail the batch as before.

Handlers must be safe to call concurrently within one request, as they already had to be across requests. The HandlerFunc doc, the batch package doc, the README and notes.md say so. piri's six RPC handlers were audited before this change: their shared state is stores, mutex-guarded caches and a singleflight group, and none reads a sibling's result.

Fourth PR in the stack for FIL-1261, on top of #67, which it depends on: without the dispatcher recovering panics from handlers (#64) and from validation code (#67), a panic on one of these goroutines would crash the process.

Tests: a barrier handler that only releases once all invocations have entered, so the test deadlocks under sequential execution; cap enforcement with a cap of 2 and 5 invocations; the zero cap running 150 at once; the negative value panicking; response metadata preserved in request order when later invocations finish first. Run three times under the race detector. The examples package could not run locally because the sandbox forbids binding a TCP port; everything else in make ci passed.

Left for the dependency bump in sprue and piri: sprue's AcceptBatch comment still says the node executes sequentially, and since sprue sends up to 1000 accepts per request piri may want a higher cap.

Benchmark results

Benchmark on an M-series laptop, one request of 100 invocations:

Handler Sequential (cap 1) Concurrent 100 requests of one
1 ms sleep 151 ms 4.6 ms 4.5 ms
SHA-256 over 256 KiB 18.9 ms 3.7 ms 5.1 ms

One request of 1000 invocations:

case ns/op
io/1x1000/sequential 1,562,330,862
io/1x1000 18,738,525
cpu/1x1000/sequential 187,075,104
cpu/1x1000 21,781,242

Concurrent execution is about 80x faster than sequential for the I/O handler and about 9x faster for the CPU handler.


Stack created with GitHub Stacks CLI • Give Feedback 💬

@bajtos
bajtos added this pull request to stack #65 September 22, 2026 07:21
@bajtos bajtos changed the title feat: execute a request's invocations concurrently Execute a request's invocations concurrently Sep 22, 2026
@bajtos
bajtos force-pushed the miroslav/fil-1261-concurrent-batch-execution branch 2 times, most recently from 0df41bb to 6ed2ab1 Compare September 22, 2026 07:26
Comment thread server/http.go Outdated
Comment thread server/options.go Outdated
Comment thread server/options.go Outdated
@bajtos
bajtos force-pushed the miroslav/fil-1261-concurrent-batch-execution branch from 7868d54 to 5344a2e Compare September 22, 2026 07:48
bajtos added a commit that referenced this pull request Sep 22, 2026
Say that the cap is per request rather than per server, how to force
serial execution with a cap of one, and drop the sentence about the
default's derivation that read as confusing.

Addresses review comments on #66.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
@bajtos
bajtos marked this pull request as ready for review September 22, 2026 07:59
@bajtos
bajtos requested review from alanshaw and a lite review from Copilot September 22, 2026 07:59

Copilot AI 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.

Copilot review overview

🔵 Needs a closer look

It introduces a fundamental concurrency behavior change in the server execution path, which warrants final human review despite strong test coverage.

Review effort: Lite
Findings: 2 Low severity

Open (2)
What changed in this PR

This PR changes the HTTP server’s batch execution model so invocations within a single batch request can run concurrently, controlled by a new per-request concurrency cap. This aligns batch performance more closely with sending many single-invocation requests, while keeping response assembly deterministic.

Changes:

  • Execute batch invocations concurrently with a per-request semaphore and server.WithMaxConcurrency(n) (default 100; 0 disables; negative panics).
  • Preserve deterministic response construction by collecting per-invocation results by request index and merging after all goroutines complete.
  • Add targeted tests/benchmarks and update public documentation to reflect the new concurrency semantics and handler expectations.
File Description
server/​options.go Adds DefaultMaxConcurrency and WithMaxConcurrency option wired into server config.
server/​http.go Implements concurrent ExecuteBatch with per-request semaphore and ordered merge.
server/​http_test.go Adjusts an existing test to be safe under concurrent handler execution.
server/​concurrency_test.go Adds concurrency behavior tests (barrier, cap enforcement, order preservation, panic on negative).
server/​bench_test.go Adds benchmarks comparing sequential vs concurrent batching and many single requests.
README.md Updates user-facing docs to mention concurrent batch execution and configurability.
notes.md Records the design rationale and operational model for per-request concurrency.
execution/​execution.go Documents handler concurrency expectations for batch execution.
execution/​batch/​batch.go Updates package docs to reflect concurrency + ordering constraints.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread README.md Outdated
Comment thread server/http.go

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4c917ee23d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread server/http.go Outdated
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-22T08:06:00.191665Z 4c917ee Draft marked ready
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@bajtos bajtos changed the title Execute a request's invocations concurrently feat: execute a request's invocations concurrently Sep 22, 2026
Comment thread server/http.go
// Each goroutine writes only its own slot, so the slice needs no lock and
// the merge below runs in request order.
results := make([]batchResult, len(req.Invocations()))
var wg sync.WaitGroup

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Can we use errgroup with SetLimit instead?

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.

I asked Claude, it rejected that idea for three reasons:

  • AGENTS.md keeps the dependency list to what we have today, and golang.org/x/sync would be a new module.
  • SetLimit has the opposite conventions from our option: 0 blocks every Go call and -1 means unlimited, so we would need a translation layer.
  • errgroup deliberately does not propagate panics from goroutines to Wait (the comment in errgroup.go explains why), so it would not have helped with the panic boundary Codex found, which fix: recover panics from validation code #67 now handles in the dispatcher.

However, Go 1.27 added a new API that simplifies the pattern we have here, and we reworked the code to use sync.WaitGroup.Go, which removes the Add/Done boilerplate without a new dependency.

bajtos added a commit that referenced this pull request Sep 22, 2026
Say that the cap is per request rather than per server, how to force
serial execution with a cap of one, and drop the sentence about the
default's derivation that read as confusing.

Addresses review comments on #66.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
@bajtos
bajtos force-pushed the miroslav/fil-1261-concurrent-batch-execution branch from 4c917ee to a71785c Compare September 22, 2026 13:10
bajtos added a commit that referenced this pull request Sep 22, 2026
Say that the cap is per request rather than per server, how to force
serial execution with a cap of one, and drop the sentence about the
default's derivation that read as confusing.

Addresses review comments on #66.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
@bajtos
bajtos force-pushed the miroslav/fil-1261-concurrent-batch-execution branch from a71785c to a46976c Compare September 22, 2026 13:48
@bajtos
bajtos removed this pull request from stack #65 September 22, 2026 14:50
bajtos added a commit that referenced this pull request Sep 22, 2026
Say that the cap is per request rather than per server, how to force
serial execution with a cap of one, and drop the sentence about the
default's derivation that read as confusing.

Addresses review comments on #66.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
@bajtos
bajtos changed the base branch from miroslav/fil-1261-recover-handler-panics to miroslav/fil-1261-recover-validation-panics-v2 September 22, 2026 14:51
@bajtos
bajtos force-pushed the miroslav/fil-1261-concurrent-batch-execution branch from a46976c to 5ede701 Compare September 22, 2026 14:51
@bajtos
bajtos added this pull request to stack #68 September 22, 2026 14:52
@bajtos
bajtos requested a review from alanshaw September 22, 2026 14:59

@alanshaw alanshaw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Nice 👍

The server ran the invocations of one request one after another, so a
request carrying 100 invocations took longer than 100 requests
carrying one each, and batching made large multipart uploads slower
rather than faster.

Run each invocation on its own goroutine, the way net/http runs each
connection, admitted through a per-request semaphore the way HTTP/2 and
QUIC servers cap concurrent streams per connection. WithMaxConcurrency
sets the cap; the default of 100 matches quic-go and the RFC 9113
floor, zero removes the cap and a negative value panics. The semaphore
is acquired before spawning, so an oversized request waits in the loop
rather than as a pile of blocked goroutines.

Receipts and metadata are collected into a slice indexed by request
position and merged after every invocation finishes, so the response
is the same regardless of which handler finished first and needs no
lock. Dispatcher errors are joined and surface after all siblings have
completed; they still fail the batch as before.

Benchmark, one request of 100 invocations, 1ms sleeping handler:
sequential 151ms, concurrent 4.6ms, against 4.5ms for 100 concurrent
requests of one. The CPU-bound handler goes from 18.9ms to 3.7ms.

Fixes FIL-1261

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
The sub-benchmark names spelled out the batch size, so measuring a
different size meant editing four lines instead of one constant.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Say that the cap is per request rather than per server, how to force
serial execution with a cap of one, and drop the sentence about the
default's derivation that read as confusing.

Addresses review comments on #66.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
The README and the ExecuteBatch comment read "up to
WithMaxConcurrency at a time", as if the option were a number. Name
DefaultMaxConcurrency and say the option changes it.

While here, replace the manual Add/Done pair with WaitGroup.Go.

Assisted-by: Claude:claude-fable-5-1
Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
@bajtos
bajtos force-pushed the miroslav/fil-1261-concurrent-batch-execution branch from 051e406 to a23cedf Compare September 23, 2026 13:38
@bajtos
bajtos merged commit 7eea01e into main Sep 23, 2026
6 checks passed
@bajtos
bajtos deleted the miroslav/fil-1261-concurrent-batch-execution branch September 23, 2026 13:43
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.

3 participants