Skip to content

Let the relay reflector wait 30 seconds for a request - #176

Open
aszarama wants to merge 6 commits into
mainfrom
as/onb-188-reflector-read-header-timeout
Open

aszarama wants to merge 6 commits into
mainfrom
as/onb-188-reflector-read-header-timeout

Conversation

@aszarama

@aszarama aszarama commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

What

The relay reflector now waits up to 30 seconds for the broker to write a request. Before, it closed any connection that had no complete request one second after the connection opened. Under load, the broker often writes later than that, so the reflector reset requests that were already on their way.

Each of the agent's HTTP servers now has a fixed read bound. There is no setting to change.

Server Read bound
Relay reflector 30s (was 1s)
Webhook server 30s (was 1s)
Main server 1s, except 30s for a /cortex-api/ request body

Why

ReadTimeout counts from when a connection is accepted, not from the first byte. The broker opens a connection and writes the request only when its event loop gets to it. That loop stalls for two to three seconds each time it sends a large (13 MB) response over the websocket. Any connection the broker opened just before a stall is closed before its request arrives.

The broker sees this as socket hang up (ECONNRESET), or EPIPE for a body it is still writing. It retries after 500ms, up to three times. In one 17-minute production export from a single pod on v0.3.4, that was about 8,000 retries, 440 for every 1,000 relayed requests, and 31 requests failed after three retries. In the v0.3.9 export, which has #168, it is 132 to 180 retries per 1,000 requests. #168 fixed idle keep-alive connections but not new ones.

The reset also hits the relay dispatcher's health probe. The dispatcher calls /__axon/info through the broker, which reaches the agent through the reflector, like other traffic. The same export has about 4,000 failed /__axon/info attempts. A failed probe makes the dispatcher mark the agent as not responding. With no responsive agent, callers get 555, even for low-volume requests unrelated to the large responses.

30 seconds covers every stall in the v0.3.4 export and 1,875 of 1,880 in a 75-minute, four-pod export on v0.3.9. It is also less than the relay dispatcher's 60-second request timeout.

How

Review focus: http_server.go (WithReadTimeout) and cortex_api_proxy_handler.go. The reflector and webhook changes are one line each.

  • New WithReadTimeout server option. A server without it keeps the one-second default.
  • The reflector and the webhook server use 30s. Webhook senders are outside the agent's network and can upload large payloads slowly.
  • The main server keeps 1s for its own endpoints. The /cortex-api/ proxy moves its read deadline to 30s with http.ResponseController. responseRecorder gets an Unwrap method, so the controller can reach the real connection.
  • ReadHeaderTimeout stays unset, so net/http uses ReadTimeout for headers too. In a test, a 30s header-only bound with the 1s ReadTimeout still failed a PUT body that arrived 1.5s late.
  • Not in this PR: the broker stalls themselves, and the websocket reconnects seen in the same logs. Deployed agents get the fix only after they upgrade.

Verification

  • New tests pass: a 30s server serves a request written 1.5s after connecting and a PUT body written 1.5s after its headers; a default server drops a request written 1.5s late; the /cortex-api/ proxy forwards a body that arrives 1.5s late.
  • Through the real reflector, I opened 50 connections and blocked a Node 20 client for 1.5 seconds before it wrote the requests. With 1s, all 50 reset. With 30s, all 50 were served. This ran on an earlier commit that set the value with an environment variable.
  • go test ./... in agent/ passes on the latest commit, except TestLoopbackOnlyRefusesOffHostConnection. It dials the machine's LAN address, and it fails the same way on unmodified v0.3.9 on my machine.
  • Not run: a full agent against a broker server, or a production relay with this build. The drop in health-probe failures and 555s is expected, not measured.

…r connection opened

The HTTP server's one-second ReadTimeout starts when a connection is
accepted. The broker opens a connection and writes its request only when
its event loop gets to it, and under load that loop stalls for more than a
second. The server then closed the connection before the request arrived,
so every connection opened before the stall failed at once with ECONNRESET
and a 500ms retry.

Wait up to 30 seconds for request headers and five minutes for a whole
request, and cover it with a test that writes its request 1.5 seconds after
connecting.
AXON_HTTP_READ_HEADER_TIMEOUT and AXON_HTTP_READ_TIMEOUT override the 30s
header wait and the 5m whole-request bound. A deployment whose broker stalls
for longer than 30s under load can raise the header wait without a new
release.
@aszarama aszarama changed the title Stop the reflector closing new connections after one second Stop closing new connections after one second, and make read timeouts configurable Oct 28, 2026
…setting

Leaving ReadHeaderTimeout unset makes net/http use ReadTimeout for headers
too, so one bound covers a late request and a late body. A header-only bound
left a body that arrives after the stall to time out. AXON_HTTP_READ_TIMEOUT
overrides the one-second default; without it the agent behaves as before.
@aszarama aszarama changed the title Stop closing new connections after one second, and make read timeouts configurable Make the agent's HTTP read timeout configurable Oct 28, 2026
The broker's event loop stalls for two to three seconds while it encodes a
large response, and a one-second ReadTimeout closed the connections it opened
just before the stall. Thirty seconds covers the stalls seen on a production
relay and stays below the relay dispatcher's 60-second request timeout.
AXON_HTTP_READ_TIMEOUT still overrides it.
@aszarama aszarama changed the title Make the agent's HTTP read timeout configurable Raise the agent's HTTP read timeout from 1s to 30s Oct 28, 2026
@aszarama
aszarama requested a review from shawnburke October 28, 2026 20:33
Deployments whose broker stalls under load raise it with
AXON_HTTP_READ_TIMEOUT, for example to 30s. Without the variable the agent
behaves as before.
@aszarama aszarama changed the title Raise the agent's HTTP read timeout from 1s to 30s Make the agent's HTTP read timeout configurable Oct 28, 2026

@shawnburke shawnburke left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

we should deal with this at the callsite rather than globally - see comment

Comment thread agent/server/http/http_server.go Outdated
// AXON_HTTP_READ_TIMEOUT is not set. It is counted from the moment the
// connection is accepted, so a client that opens a connection and writes its
// request later, as the broker does when its event loop stalls, needs a
// longer value.

@shawnburke shawnburke Oct 29, 2026 •

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I don't think this is the right approach. There are three different instances of httpServer:

  • Axon stuff (that is all fast)
  • Webhooks (its own instance and port)
  • Reflector

The second two need configurable timeouts as a startup WithReadTimeout option kind of thing so they should just have longer timeouts. This is an inheritance of defaults problem

The tougher case is that the cortex-api passthrough is on the main server so it picks up the 1s default. We can raise that with a thing like _ = http.NewResponseController(w).SetReadDeadline(time.Now().Add(apiProxyReadTimeout)) in the cortex_api_handler code.

@aszarama aszarama changed the title Make the agent's HTTP read timeout configurable Let the relay reflector wait 30 seconds for a request Oct 30, 2026

@shawnburke shawnburke left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Good - 30s is still small do we want these configurable?

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