stacks-network / stacks-network/stacks-core
StacksHttp rebuilds the entire RPC route table (~100 regex compilations, ~5 ms) on every inbound HTTP connection
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 3.1k
- Forks
- 762
- Avg merge
- 4d 6h
- Merged PRs (30d)
- 76
Description
Context
Every accepted HTTP socket constructs a fresh StacksHttp protocol state machine, and with it the full RPC route table:
HttpPeer::register_http(stackslib/src/net/server.rs) →ConversationHttp::new(stackslib/src/net/rpc.rs) →StacksHttp::new(stackslib/src/net/httpcore.rs), which callsregister_rpc_methods()(stackslib/src/net/api/mod.rs).register_rpc_methods()instantiates all ~50 RPC handlers and, for each one, compiles a strict path regex (handler.path_regex()returns a freshly compiledRegexon every call) plus a permissive regex derived bymake_permissive_regex()for 400/405 detection — roughly 100Regex::newcompilations per inbound connection.
Measured with the real patterns (release build): a param-heavy strict regex (standard-principal + contract-name + clarity-name character classes, e.g. the /v2/map_entry/... pattern) costs ~175 µs to compile, and a simulated full register_rpc_methods() costs ~5 ms. Cloning an already-compiled Regex costs ~0.1 µs, since regex::Regex is internally reference-counted.
This matters because the compilation happens inline on the p2p thread — the single thread that services all RPC and p2p traffic — so every new connection head-of-line-blocks every other request for ~5 ms. Connection creation is also far more frequent than "once per client":
idle_timeoutdefaults to 15 s (ConnectionOptions,stackslib/src/net/connection.rs), so gateway keep-alive pools churn whenever traffic isn't continuous;max_http_clientsdefaults to 10 concurrent inbound connections per source IP, so a reverse proxy or API gateway (a single IP) that bursts past the cap gets accept-then-drop behavior and reconnect churn;- load balancers open additional upstream connections precisely when the node is under load, adding this cost at the worst time.
It is also a mild DoS amplifier: an attacker can force ~5 ms of event-loop CPU per otherwise-empty TCP connect.
Solution tl;dr
Build the route table once and clone it per connection. All handler structs already derive Clone; add a box_clone() method to the RPCRequestHandler trait (or use the dyn-clone crate), build a template Vec<(String, Regex, Regex, Box<dyn RPCRequestHandler>)> a single time, and make StacksHttp::new clone the template instead of re-registering. Regex clones are refcount bumps, so per-connection setup drops from ~5 ms to ~10 µs (>99% reduction).
Two constraints to respect:
- Handler instances must remain per-conversation — parsed request state is stored on the handler between
try_parse_requestandtry_handle_request— so the fix is clone-from-template, not a globally shared table. - The template depends on a few
ConnectionOptions-derived fields captured at construction time (maximum_call_argument_size,read_only_call_limit,read_only_max_execution_time,read_only_call_max_mem_bytes,auth_token). These are identical for every inbound connection of a givenHttpPeer, so the template can live onHttpPeer(built from itsConnectionOptionsat startup) rather than in a global.
Self-contained implementation prompt
In the stacks-core repository, eliminate the per-connection construction of the RPC route table in the HTTP server.
Current behavior. StacksHttp::new in stackslib/src/net/httpcore.rs calls self.register_rpc_methods() (defined in stackslib/src/net/api/mod.rs). That method registers ~50 RPC endpoint handlers via register_rpc_endpoint, which stores (verb, strict_regex, permissive_regex, Box<dyn RPCRequestHandler>) tuples in StacksHttp::request_handlers. The strict regex comes from handler.path_regex() (which compiles a new Regex on each call) and the permissive regex from handler.path_regex_permissive() / make_permissive_regex(). StacksHttp::new is invoked from ConversationHttp::new (stackslib/src/net/rpc.rs), which is invoked from HttpPeer::register_http (stackslib/src/net/server.rs) for every accepted HTTP socket. Net effect: ~100 regex compilations (~5 ms of CPU on the p2p thread) per inbound TCP connection.
Required change. Make route-table construction happen once, and make per-connection setup a cheap clone:
- Add a boxed-clone capability to the
RPCRequestHandlertrait (instackslib/src/net/httpcore.rs): e.g. afn box_clone(&self) -> Box<dyn RPCRequestHandler>default-less method implemented via a blanketimpl<T: RPCRequestHandler + Clone + 'static>helper trait, or adopt thedyn-clonecrate. All existing handlers deriveClonealready; verify each of the ~50 handlers instackslib/src/net/api/compiles with the new bound. - Introduce a route-table template type holding the
(String, Regex, Regex, Box<dyn RPCRequestHandler>)tuples. Build it once perHttpPeerfrom itsConnectionOptions(the handlers capturemaximum_call_argument_size,read_only_call_limit,read_only_max_execution_time,read_only_call_max_mem_bytes, andauth_tokenat registration time — seeregister_rpc_methodsinstackslib/src/net/api/mod.rs). Store it onHttpPeer(stackslib/src/net/server.rs) and thread it throughConversationHttp::newintoStacksHttp::new, which should clone the tuples (Regex::cloneis anArcbump; handlers clone viabox_clone) instead of callingregister_rpc_methods(). - Keep the public behavior of
StacksHttp::newintact for callers that construct it without anHttpPeer(tests, clients viaStacksHttp::new_client): if no template is supplied, fall back to building the table as today.new_clientdoes not register handlers and needs no change. - Do not share one table across conversations: handlers store parsed request state on themselves between
try_parse_requestandtry_handle_request, so eachConversationHttpmust own its own cloned handler instances.
Verification.
cargo check -p stacksliband the HTTP test suites:cargo test -p stackslib net::apiandcargo test -p stackslib net::http(alsonet::tests::httpcoreif present in the tree).- Add a unit test asserting that two
StacksHttpinstances cloned from the same template route identically (same handler order and regex patterns) and that per-connection construction no longer callspath_regex()(e.g., assert template build count via the template API shape, or benchmark-style assert that constructing 100StacksHttpfrom a template completes well under the cost of 100 fresh registrations). - Confirm no behavioral change to routing: registration order must be preserved exactly, since
try_parse_requestmatches handlers linearly in insertion order.
Success criteria. Accepting an inbound HTTP connection performs zero Regex::new calls in the steady state; all existing tests pass; handler per-conversation state semantics are unchanged.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start with StacksHttp::new and register_rpc_methods in stackslib/src/net/httpcore.rs and stackslib/src/net/api/mod.rs, then trace ConversationHttp::new and HttpPeer::register_http. Follow how ConnectionOptions values and handler state enter the route table, preserving registration order and per-conversation handler ownership. Run cargo check -p stackslib and the net::api and net::http test suites; done means steady-state inbound setup performs no Regex::new calls and routing behavior remains unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- api, backend, networking, performance
- Issue type
- Refactor
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100