mirror of
https://github.com/soypat/lneto.git
synced 2026-08-09 01:13:41 +00:00
d05cd14018
* httphi: RouterConfig refactor to enable DefaultRouterConfig * RequestHeader not case sensitive anymore * httphi: use DefaultRouterConfig in examples * httphi: remove gated stage complexity Misusing Stage methods by calling them once header has been written is totally harmless as far as I can tell. We simplify the codebase on this occasion by removing the headerWritten check for all stage methods * httphi: improve APIs * httphi: remove status
123 lines
6.1 KiB
Go
123 lines
6.1 KiB
Go
package httphi
|
|
|
|
import (
|
|
"math"
|
|
"unsafe"
|
|
|
|
"github.com/soypat/lneto/http/httpraw"
|
|
)
|
|
|
|
const (
|
|
// minRequestHeaderBuffer is the smallest request buffer [httpraw.Header]
|
|
// accepts with buffer growth disabled, which is how exchanges are configured.
|
|
minRequestHeaderBuffer = 32
|
|
// minResponseHeaderBuffer is the room [Exchange.FlushHeader] needs for the
|
|
// CRLF closing the header block, written even when no field was staged.
|
|
minResponseHeaderBuffer = len("\r\n")
|
|
// maxExchangeBuffer bounds an exchange's whole buffer: [Exchange] indexes it
|
|
// with uint16 offsets, so a larger one would be addressed truncated.
|
|
maxExchangeBuffer = math.MaxUint16
|
|
|
|
// sizeofExchange is the fixed cost of an exchange, which a Router pays per
|
|
// connection it can serve concurrently on top of the buffers it hands it.
|
|
// It dwarfs a small request buffer, so budgets must account for it.
|
|
sizeofExchange = int(unsafe.Sizeof(Exchange{}))
|
|
// sizeofPathValue is the per-wildcard cost of the path value table.
|
|
sizeofPathValue = int(unsafe.Sizeof(PathValue{}))
|
|
// sizeofJob is an exchange's slot in the queue connections wait on for a
|
|
// worker goroutine, sized to the goroutine count in worker mode.
|
|
sizeofJob = int(unsafe.Sizeof(job{}))
|
|
|
|
// bytesPerHeaderField is the request buffer [DefaultRouterConfig] budgets per
|
|
// parseable header field. Real fields run a little longer than this
|
|
// ("Accept-Encoding: gzip, deflate, br\r\n" is 35 bytes), so a request fills
|
|
// the buffer before it exhausts the field table, which is the cheaper of the
|
|
// two limits to hit: growing the table costs [httpraw.SizeKV] per field on top
|
|
// of the bytes the field already occupies.
|
|
bytesPerHeaderField = 32
|
|
// defaultResponseHeaderBuffer is the response header room
|
|
// [DefaultRouterConfig] reserves when the budget can afford it: enough for a
|
|
// Content-Type, a Content-Length and a Connection field with room to spare.
|
|
// It does not scale with the request buffer because what a response header
|
|
// costs depends on the fields a handler stages, not on the request's size.
|
|
defaultResponseHeaderBuffer = 128
|
|
)
|
|
|
|
// MemoryUsagePerConnection returns the heap bytes a [Router] configured with cfg
|
|
// reserves for each connection it can serve concurrently, maxPathValues being
|
|
// the [Mux.MaxPathValues] of the mux it is configured with. Goroutine stacks are
|
|
// not counted: those are the runtime's to size, not the router's.
|
|
//
|
|
// In worker mode this is exact and fixed, so a router's whole heap footprint is
|
|
// this times FixedNumGoroutines, plus the runtime's own header for the job
|
|
// queue. With FixedNumGoroutines -1 the router allocates one of these per
|
|
// connection in flight instead, so the total grows with peak concurrency.
|
|
//
|
|
// It is the inverse of [DefaultRouterConfig] and useful to check a hand written
|
|
// configuration against a memory budget.
|
|
func (cfg RouterConfig) MemoryUsagePerConnection(maxPathValues int) int {
|
|
if maxPathValues < 0 {
|
|
maxPathValues = 0 // Mux with no routes registered yet, see [Mux.MaxPathValues].
|
|
}
|
|
n := sizeofExchange + // Exchange itself, an element of the router's exchange store.
|
|
cfg.RequestHeaderBufferSize + cfg.ResponseHeaderMinBufferSize + // Its window into the raw buffer.
|
|
cfg.RequestNumHeaderKVCap*httpraw.SizeKV + // The request header's field table.
|
|
maxPathValues*sizeofPathValue // Its window into the path value store.
|
|
if cfg.workerMode() {
|
|
n += sizeofJob // Slot in the queue connections wait on for a worker.
|
|
}
|
|
return n
|
|
}
|
|
|
|
// DefaultRouterConfig is a general purpose configuration creator
|
|
// for small, medium, large, performant or embedded projects.
|
|
//
|
|
// Generalness is achieved with parameters that let the Configuration
|
|
// determine allocation buffer sizes based on typical usage for that
|
|
// number of goroutines and heap allocation on a per-connection basis.
|
|
func DefaultRouterConfig(numGoroutines, memoryPerConnectionBytes, maxPathValues int) RouterConfig {
|
|
// The budget is spent on the request header buffer first, since that is what
|
|
// decides which requests are answered at all, then on a field table sized to
|
|
// match it and a small response header reserve. A budget too small to fund the
|
|
// minimum viable exchange yields the minimum instead, so the returned config is
|
|
// always one [Router.Configure] accepts but may exceed a budget under roughly
|
|
// sizeofExchange + 200 bytes. Check it with MemoryUsagePerConnection when the
|
|
// bound has to hold.
|
|
if numGoroutines <= 0 {
|
|
numGoroutines = -1 // Unbounded mode, the only non-positive value Validate accepts.
|
|
}
|
|
// Everything the exchange costs before any buffer is sized: subtract it first
|
|
// so the buffers below divide up what is actually left to spend.
|
|
fixed := sizeofExchange + maxPathValues*sizeofPathValue
|
|
if numGoroutines > 0 {
|
|
fixed += sizeofJob
|
|
}
|
|
// A budget past what the buffers may grow to is only spendable up to the cap
|
|
// below, so clamp before the products: on a 32 bit target an unclamped
|
|
// multiply would overflow and wrap a generous budget into a tiny buffer.
|
|
const maxSpendable = (maxExchangeBuffer + defaultResponseHeaderBuffer) *
|
|
(bytesPerHeaderField + httpraw.SizeKV) / bytesPerHeaderField
|
|
avail := min(memoryPerConnectionBytes-fixed, maxSpendable)
|
|
|
|
// The response reserve is a floor rather than a share of the budget, but a
|
|
// budget this small cannot afford the full one without starving the request.
|
|
respBuf := min(defaultResponseHeaderBuffer, avail/4)
|
|
|
|
// Solve avail-respBuf = reqBuf + reqBuf/bytesPerHeaderField*httpraw.SizeKV for
|
|
// reqBuf, the field table growing with the buffer it parses.
|
|
reqBuf := (avail - respBuf) * bytesPerHeaderField / (bytesPerHeaderField + httpraw.SizeKV)
|
|
|
|
// Clamp to what [RouterConfig.Validate] accepts. Truncating division above
|
|
// keeps the result under budget; these floors are what can push it over.
|
|
respBuf = max(respBuf, minResponseHeaderBuffer)
|
|
reqBuf = max(reqBuf, minRequestHeaderBuffer)
|
|
reqBuf = min(reqBuf, maxExchangeBuffer-respBuf)
|
|
return RouterConfig{
|
|
FixedNumGoroutines: numGoroutines,
|
|
RequestHeaderBufferSize: reqBuf,
|
|
ResponseHeaderMinBufferSize: respBuf,
|
|
RequestNumHeaderKVCap: max(reqBuf/bytesPerHeaderField, 1),
|
|
NormalizeOutgoingKeys: false,
|
|
}
|
|
}
|