Repository navigation
Conversation
…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.
…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.
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.
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.
shawnburke
left a comment
There was a problem hiding this comment.
we should deal with this at the callsite rather than globally - see comment
| // 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. |
There was a problem hiding this comment.
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.
shawnburke
left a comment
There was a problem hiding this comment.
Good - 30s is still small do we want these configurable?
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.
/cortex-api/request bodyWhy
ReadTimeoutcounts 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), orEPIPEfor 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/infothrough the broker, which reaches the agent through the reflector, like other traffic. The same export has about 4,000 failed/__axon/infoattempts. 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) andcortex_api_proxy_handler.go. The reflector and webhook changes are one line each.WithReadTimeoutserver option. A server without it keeps the one-second default./cortex-api/proxy moves its read deadline to 30s withhttp.ResponseController.responseRecordergets anUnwrapmethod, so the controller can reach the real connection.ReadHeaderTimeoutstays unset, so net/http usesReadTimeoutfor headers too. In a test, a 30s header-only bound with the 1sReadTimeoutstill failed a PUT body that arrived 1.5s late.Verification
/cortex-api/proxy forwards a body that arrives 1.5s late.go test ./...inagent/passes on the latest commit, exceptTestLoopbackOnlyRefusesOffHostConnection. It dials the machine's LAN address, and it fails the same way on unmodified v0.3.9 on my machine.