nodebuilder/rpc: Make maxConcurrentConns configurable - #5124
Conversation
Websocket subscriptions count against the connLimit middleware for the lifetime of the connection, so a bridge with many long-lived subscribers saturates the hardcoded 500 cap and returns HTTP 503 to new upgrade requests -- surfacing to clients as "websocket: bad handshake". Operators had no way to raise the cap without a custom build. Exposes MaxConcurrentConns on the RPC Config (default 500, must be > 0) with a matching --rpc.max-concurrent-conns flag, and threads it through NewServer. Zero passed to NewServer falls back to DefaultMaxConcurrentConns so callers that leave the field unset get the pre-change behavior. A separate WS-only cap and richer reject signaling (issue celestiaorg#5071 Q3/Q5) are left for follow-up. Closes celestiaorg#5071
|
| Filename | Overview |
|---|---|
| api/rpc/server.go | Adds the exported default connection cap and stores the configured limit on Server. |
| api/rpc/server_test.go | Updates NewServer call sites and tests configured limiting plus zero-value fallback. |
| nodebuilder/rpc/config.go | Adds MaxConcurrentConns to RPC config defaults and validation. |
| nodebuilder/rpc/config_test.go | Covers the new config default and invalid non-positive values. |
| nodebuilder/rpc/constructors.go | Passes the configured connection cap into api/rpc.NewServer. |
| nodebuilder/rpc/flags.go | Adds the CLI override, but negative values are silently ignored. |
Sequence Diagram
sequenceDiagram
participant Operator
participant CLI as nodebuilder/rpc.ParseFlags
participant Config as nodebuilder/rpc.Config
participant Constructor as nodebuilder/rpc.server
participant Server as api/rpc.NewServer
participant Middleware as connLimit
Operator->>CLI: "--rpc.max-concurrent-conns=N"
CLI->>Config: "set MaxConcurrentConns when N > 0"
Config->>Config: "Validate MaxConcurrentConns > 0"
Config->>Constructor: pass cfg.MaxConcurrentConns
Constructor->>Server: NewServer(..., maxConcurrentConns)
Server->>Server: "fallback to DefaultMaxConcurrentConns if <= 0"
Server->>Middleware: connLimit(maxConcurrentConns, handler)
Middleware-->>Operator: 503 when concurrent slots are exhausted
Prompt To Fix All With AI
### Issue 1
nodebuilder/rpc/flags.go:207-211
**Reject negative flag values**
`--rpc.max-concurrent-conns=-1` is accepted by `pflag.GetInt`, but this branch ignores it because only `val > 0` is applied. With a valid config already loaded, startup continues using the old config value even though the operator supplied an invalid override; `Validate()` never sees the negative value. Returning an error here keeps CLI overrides from being silently misapplied.
```suggestion
if val, err := cmd.Flags().GetInt(maxConcurrentConnsFlag); err != nil {
return err
} else if val < 0 {
return fmt.Errorf("%s must be >= 0", maxConcurrentConnsFlag)
} else if val > 0 {
cfg.MaxConcurrentConns = val
}
```
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.Reviews (3): Last reviewed commit: "fix(nodebuilder/rpc): fail on invalid Ma..." | Re-trigger Greptile
An older config.toml that predates this field decodes with MaxConcurrentConns == 0. Rejecting that in Validate makes binary upgrades fail with an opaque startup error until the operator runs `config update`. Match NewServer's zero-fallback behavior and backfill the default in Validate so old configs keep working. Swap the previous "zero/negative rejected" test cases for a fallback test that asserts Validate mutates the field to DefaultMaxConcurrentConns.
|
@vgonkivs could you take a look at this? |
vgonkivs
left a comment
There was a problem hiding this comment.
Thanks for the PR. Have a few comments before we proceed with it:
- Please verify MaxConcurrentConn but dont mutate them(cfg.Validate() in rpc module). The node should fail out loud on startup in case MaxConcurrentConn <= 0
- The PR contains a lot of AI-generated comments. Please keep comments short and only where a reader would ask why
Validate no longer backfills the default -- it rejects <= 0 so a bad value fails at startup instead of being silently corrected. The error points at `celestia config update` for configs predating the field.
|
Both addressed in the amended commit:
Release note: configs predating this field need |
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #5124 +/- ##
==========================================
- Coverage 44.83% 37.69% -7.15%
==========================================
Files 265 307 +42
Lines 14620 21373 +6753
==========================================
+ Hits 6555 8056 +1501
- Misses 7313 12304 +4991
- Partials 752 1013 +261 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
| if val, err := cmd.Flags().GetInt(maxConcurrentConnsFlag); err != nil { | ||
| return err | ||
| } else if val > 0 { | ||
| cfg.MaxConcurrentConns = val | ||
| } |
There was a problem hiding this comment.
Reject negative flag values
--rpc.max-concurrent-conns=-1 is accepted by pflag.GetInt, but this branch ignores it because only val > 0 is applied. With a valid config already loaded, startup continues using the old config value even though the operator supplied an invalid override; Validate() never sees the negative value. Returning an error here keeps CLI overrides from being silently misapplied.
| if val, err := cmd.Flags().GetInt(maxConcurrentConnsFlag); err != nil { | |
| return err | |
| } else if val > 0 { | |
| cfg.MaxConcurrentConns = val | |
| } | |
| if val, err := cmd.Flags().GetInt(maxConcurrentConnsFlag); err != nil { | |
| return err | |
| } else if val < 0 { | |
| return fmt.Errorf("%s must be >= 0", maxConcurrentConnsFlag) | |
| } else if val > 0 { | |
| cfg.MaxConcurrentConns = val | |
| } |
Context Used: CLAUDE.md (source)
Artifacts
Repro: generated focused Go test source
- Evidence file captured while the check ran.
Repro: focused Go test output showing nil error and unchanged MaxConcurrentConns
- The full command output behind this check.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: nodebuilder/rpc/flags.go
Line: 207-211
Comment:
**Reject negative flag values**
`--rpc.max-concurrent-conns=-1` is accepted by `pflag.GetInt`, but this branch ignores it because only `val > 0` is applied. With a valid config already loaded, startup continues using the old config value even though the operator supplied an invalid override; `Validate()` never sees the negative value. Returning an error here keeps CLI overrides from being silently misapplied.
```suggestion
if val, err := cmd.Flags().GetInt(maxConcurrentConnsFlag); err != nil {
return err
} else if val < 0 {
return fmt.Errorf("%s must be >= 0", maxConcurrentConnsFlag)
} else if val > 0 {
cfg.MaxConcurrentConns = val
}
```
**Context Used:** CLAUDE.md ([source](https://app.greptile.com/celestia/github/celestiaorg/celestia-node/-/custom-context?memory=f5c7a6d3-9be6-4d68-83d6-efd8541b6403))
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary
Fixes #5071. After the RPC hardening in #4909 shipped in v0.31.3, the
connLimitmiddleware caps all incoming HTTP connections at a hardcoded 500. Websocket subscriptions occupy a slot for the lifetime of the connection, so a bridge with many long-lived subscribers exhausts the pool; the next WS upgrade gets HTTP 503 back, which surfaces client-side aswebsocket: bad handshake. Operators had no way to raise the cap without a custom build.This PR:
MaxConcurrentConnstonodebuilder/rpc.Config(default 500, must be> 0).--rpc.max-concurrent-connsflag (0 = fall back to config value).api/rpc.NewServerintoconnLimit. A zero value passed toNewServerfalls back toDefaultMaxConcurrentConnsso callers that leave the field unset keep the pre-change behavior.limit=1, blocks one request, and asserts the second gets 503; a fallback test for the zero-value path; validation-rejection tests for≤ 0.Out of scope (issue questions Q3 and Q5): a separate WS-only limit / exempting WS upgrades from the shared cap, and surfacing the HTTP reject reason on failed upgrades. Both are worth doing as follow-ups but are larger design changes.
Test plan
go build ./...go vet ./...(no new warnings introduced)golangci-lint run --new-from-rev=origin/main— 0 new issuesgo test ./api/rpc/... ./nodebuilder/rpc/...go test -short ./nodebuilder/...— full nodebuilder suite passesMaxConcurrentConnsto a low value inconfig.toml, confirm the new cap is honored and default value still works when unset