mirror of
https://github.com/soypat/lneto.git
synced 2026-08-15 12:23:44 +00:00
several bugfixes, add internal.IntLen, round up http-linux example with new router API
This commit is contained in:
@@ -188,7 +188,7 @@ func TestHandleRequestFields(t *testing.T) {
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var gotMethod, gotURI, gotHost string
|
||||
var sm sliceMux
|
||||
var sm MuxSlice
|
||||
sm.Handle(test.wantURI, func(ex *Exchange) {
|
||||
gotMethod = string(ex.RequestMethod())
|
||||
gotURI = string(ex.RequestURI())
|
||||
@@ -223,7 +223,7 @@ func TestHandleMalformedRequest(t *testing.T) {
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var handled bool
|
||||
var sm sliceMux
|
||||
var sm MuxSlice
|
||||
sm.Handle("/", func(ex *Exchange) { handled = true })
|
||||
conn := newConn(test.request)
|
||||
conn.Hangup()
|
||||
@@ -240,7 +240,7 @@ func TestHandleMalformedRequest(t *testing.T) {
|
||||
|
||||
// No registered handler must yield 404, not an empty response.
|
||||
func TestHandleNoHandler(t *testing.T) {
|
||||
var sm sliceMux
|
||||
var sm MuxSlice
|
||||
sm.Handle("GET /", func(ex *Exchange) { t.Error("handler must not run") })
|
||||
conn := serve(t, "GET /nowhere HTTP/1.1\r\nHost: h\r\n\r\n", &sm)
|
||||
const want = "HTTP/1.1 404 Not Found\r\n\r\n"
|
||||
@@ -251,7 +251,7 @@ func TestHandleNoHandler(t *testing.T) {
|
||||
|
||||
// A handler that writes nothing must still produce a valid response.
|
||||
func TestHandleSilentHandler(t *testing.T) {
|
||||
var sm sliceMux
|
||||
var sm MuxSlice
|
||||
sm.Handle("/", func(ex *Exchange) {})
|
||||
conn := serve(t, "GET / HTTP/1.1\r\nHost: h\r\n\r\n", &sm)
|
||||
const want = "HTTP/1.1 200 OK\r\n\r\n"
|
||||
@@ -265,7 +265,7 @@ func TestExchangeReadBody(t *testing.T) {
|
||||
const body = "message body"
|
||||
var got string
|
||||
var readErr error
|
||||
var sm sliceMux
|
||||
var sm MuxSlice
|
||||
sm.Handle("POST /", func(ex *Exchange) {
|
||||
dst := make([]byte, len(body))
|
||||
n, err := ex.ReadBody(dst)
|
||||
@@ -315,7 +315,7 @@ func TestExchangeSetHeaderExactFit(t *testing.T) {
|
||||
// Handle never closes the connection, on any outcome: the caller owns it so
|
||||
// that error policy and connection reuse stay the caller's decision.
|
||||
func TestHandleLeavesConnOpen(t *testing.T) {
|
||||
var sm sliceMux
|
||||
var sm MuxSlice
|
||||
sm.Handle("GET /", staticPage(t, "ok"))
|
||||
for _, request := range []string{
|
||||
"GET / HTTP/1.1\r\nHost: h\r\n\r\n", // Served.
|
||||
@@ -337,7 +337,7 @@ func TestHandleLeavesConnOpen(t *testing.T) {
|
||||
// Ownership must not carry over: the next connection the exchange serves is
|
||||
// the router's again and must be closed on Release.
|
||||
func TestExchangeHijackOwnership(t *testing.T) {
|
||||
var sm sliceMux
|
||||
var sm MuxSlice
|
||||
var hijackErr error
|
||||
sm.Handle("GET /", func(ex *Exchange) {
|
||||
_, _, hijackErr = ex.HijackRaw(nil)
|
||||
@@ -370,7 +370,7 @@ func TestExchangeHijackOwnership(t *testing.T) {
|
||||
// read until the conn itself reports failure, so a stalled peer ends the
|
||||
// exchange through the conn's deadline instead of pinning the exchange.
|
||||
func TestHandleIdlePeerEndsOnConnDeadline(t *testing.T) {
|
||||
var sm sliceMux
|
||||
var sm MuxSlice
|
||||
sm.Handle("/", func(ex *Exchange) { t.Error("handler must not run on partial request") })
|
||||
conn := newConn("GET / HTTP") // Peer stalls mid request line, never hangs up.
|
||||
conn.SetDeadline(time.Now().Add(10 * time.Millisecond))
|
||||
@@ -415,3 +415,40 @@ func TestExchangeWriteHeaderFlushFails(t *testing.T) {
|
||||
t.Errorf("want nothing on the wire, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeSetHeaderInt(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
value int64
|
||||
base int
|
||||
want string // Header block emitted after the status line.
|
||||
}{
|
||||
{value: 1234, base: 10, want: "N:1234\r\n\r\n"},
|
||||
{value: 0, base: 10, want: "N:0\r\n\r\n"},
|
||||
{value: -42, base: 10, want: "N:-42\r\n\r\n"},
|
||||
{value: 255, base: 16, want: "N:ff\r\n\r\n"},
|
||||
{value: 9223372036854775807, base: 10, want: "N:9223372036854775807\r\n\r\n"},
|
||||
{value: -9223372036854775808, base: 10, want: "N:-9223372036854775808\r\n\r\n"},
|
||||
{value: 1, base: 2, want: "\r\n"}, // Below base 10, dropped.
|
||||
{value: 1, base: 37, want: "\r\n"}, // Above base 36, dropped.
|
||||
} {
|
||||
conn := newConn("")
|
||||
exch := newExchange(t, conn, 256, false)
|
||||
exch.SetHeaderInt("N", test.value, test.base)
|
||||
exch.WriteHeader(200)
|
||||
got, _ := strings.CutPrefix(conn.ViewWritten(), "HTTP/1.1 200 OK\r\n")
|
||||
if got != test.want {
|
||||
t.Errorf("value %d base %d: want %q, got %q", test.value, test.base, test.want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SetHeaderInt must format into the response buffer without allocating.
|
||||
func TestExchangeSetHeaderIntNoAlloc(t *testing.T) {
|
||||
exch := newExchange(t, newConn(""), 256, false)
|
||||
allocs := testing.AllocsPerRun(100, func() {
|
||||
exch.SetHeaderInt("Content-Length", 1234567890, 10)
|
||||
})
|
||||
if allocs != 0 {
|
||||
t.Errorf("SetHeaderInt allocated %v times, want 0", allocs)
|
||||
}
|
||||
}
|
||||
|
||||
+32
-8
@@ -17,9 +17,9 @@ import (
|
||||
|
||||
//go:generate stringer -type Method,status -linecomment -output stringers.go
|
||||
|
||||
// defaultReconfigureWait is how long [Router.Configure] waits on a busy
|
||||
// previous generation when [RouterConfig.MaxReconfigureWait] is unset.
|
||||
const defaultReconfigureWait = 100 * time.Millisecond
|
||||
// reconfigureWait bounds how long [Router.Configure] waits for the previous
|
||||
// generation to stop serving before reusing its exchange buffers.
|
||||
const reconfigureWait = 10 * time.Millisecond
|
||||
|
||||
var (
|
||||
errNoRequestProto = errors.New("httphi: request line with no HTTP version")
|
||||
@@ -51,10 +51,6 @@ type job struct {
|
||||
exch *Exchange
|
||||
}
|
||||
|
||||
type Mux interface {
|
||||
LookupHandler(get Method, uri []byte) HandlerFunc
|
||||
}
|
||||
|
||||
type RouterConfig struct {
|
||||
// FixedNumGoroutines must be set to either -1 (freely allocate new goroutines) or to the number of goroutines
|
||||
// to spawn on [Router.Configure] being called.
|
||||
@@ -121,6 +117,7 @@ func (r *Router) Configure(cfg RouterConfig) error {
|
||||
r.reqBuf = cfg.RequestBufferSize
|
||||
r.respBuf = cfg.ResponseMinBufferSize
|
||||
r.mux = cfg.Mux
|
||||
r.log = cfg.Logger
|
||||
r.normalizeKeys = cfg.NormalizeOutgoingKeys
|
||||
if !workerMode {
|
||||
r.backoff = cfg.Backoff
|
||||
@@ -133,7 +130,7 @@ func (r *Router) Configure(cfg RouterConfig) error {
|
||||
if gen > 1 {
|
||||
// Exchange buffers below are reused: the previous generation must be
|
||||
// done serving before they may be handed to the new one.
|
||||
err := r.awaitIdleExchangesLocked(10 * time.Millisecond)
|
||||
err := r.awaitIdleExchangesLocked(reconfigureWait)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -454,6 +451,33 @@ func (exch *Exchange) SetHeader(key, value string) (enoughMemory bool) {
|
||||
return true
|
||||
}
|
||||
|
||||
// SetHeaderInt is [Exchange.SetHeader] with an integer value, i.e: Content-Length.
|
||||
// It formats the value directly into the response buffer without allocating.
|
||||
// base must be in the range 10..36; lower bases are dropped, no HTTP header
|
||||
// field value is written below base 10.
|
||||
func (exch *Exchange) SetHeaderInt(key string, value int64, base int) (enoughMemory bool) {
|
||||
if base < 10 || base > 36 {
|
||||
return false
|
||||
}
|
||||
off := int(exch.respHeaderOff) + int(exch.respHeaderLen)
|
||||
free := len(exch.rawbuf) - off
|
||||
if len(key)+internal.IntLen(value, base)+len(":\r\n")+len("\r\n") > free {
|
||||
return false
|
||||
}
|
||||
n := copy(exch.rawbuf[off:], key)
|
||||
if exch.normalizeKeys {
|
||||
httpraw.NormalizeHeaderKey(exch.rawbuf[off : off+n])
|
||||
}
|
||||
exch.rawbuf[off+n] = ':'
|
||||
n++
|
||||
n += len(strconv.AppendInt(exch.rawbuf[off+n:off+n], value, base))
|
||||
exch.rawbuf[off+n] = '\r'
|
||||
exch.rawbuf[off+n+1] = '\n'
|
||||
n += 2
|
||||
exch.respHeaderLen += uint16(n)
|
||||
return true
|
||||
}
|
||||
|
||||
func (exch *Exchange) StageWriteStatus(code int) {
|
||||
if code >= 1000 || exch.headerWritten {
|
||||
return
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package httphi
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
type Mux interface {
|
||||
LookupHandler(get Method, uri []byte) HandlerFunc
|
||||
}
|
||||
|
||||
type MuxSlice struct {
|
||||
// TODO: binary search worth it?
|
||||
_handlers []struct {
|
||||
method Method
|
||||
uri string
|
||||
handler HandlerFunc
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *MuxSlice) Reset(capacity int) {
|
||||
internal.SliceReuse(&sm._handlers, capacity)
|
||||
}
|
||||
|
||||
func (sm *MuxSlice) LookupHandler(method Method, uri []byte) HandlerFunc {
|
||||
for _, endpoint := range sm._handlers {
|
||||
if endpoint.method != MethUndefined && endpoint.method != method {
|
||||
continue
|
||||
}
|
||||
// Method matches.
|
||||
if b2s(uri) == endpoint.uri {
|
||||
return endpoint.handler
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (sm *MuxSlice) Handle(reg string, handler HandlerFunc) {
|
||||
v := internal.SliceReclaim(&sm._handlers)
|
||||
method := MethUndefined
|
||||
methodOrURL, url, methodFound := strings.Cut(reg, " ")
|
||||
if methodFound {
|
||||
method = MethodFromBytes([]byte(methodOrURL))
|
||||
} else {
|
||||
url = methodOrURL
|
||||
}
|
||||
v.method = method
|
||||
v.uri = url
|
||||
v.handler = handler
|
||||
}
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/soypat/lneto"
|
||||
"github.com/soypat/lneto/internal"
|
||||
)
|
||||
|
||||
// rwconn is a in-memory conn. The router handles connections on another
|
||||
@@ -128,6 +127,7 @@ func (r *rwconn) AddReadable(b []byte) {
|
||||
defer r.mu.Unlock()
|
||||
r.readable.Write(b)
|
||||
}
|
||||
|
||||
// SetDeadline makes reads and writes past t fail, as a conn with a read
|
||||
// deadline set would.
|
||||
func (r *rwconn) SetDeadline(t time.Time) {
|
||||
@@ -149,41 +149,7 @@ func (r *rwconn) ViewWritten() string {
|
||||
return r.written.String()
|
||||
}
|
||||
|
||||
var _ Mux = (*sliceMux)(nil)
|
||||
|
||||
type sliceMux struct {
|
||||
_handlers []struct {
|
||||
method Method
|
||||
uri string
|
||||
handler HandlerFunc
|
||||
}
|
||||
}
|
||||
|
||||
func (sm *sliceMux) LookupHandler(method Method, uri []byte) HandlerFunc {
|
||||
for _, endpoint := range sm._handlers {
|
||||
if endpoint.method != MethUndefined && endpoint.method != method {
|
||||
continue
|
||||
}
|
||||
// Method matches.
|
||||
if b2s(uri) == endpoint.uri {
|
||||
return endpoint.handler
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (sm *sliceMux) Handle(reg string, handler HandlerFunc) {
|
||||
v := internal.SliceReclaim(&sm._handlers)
|
||||
method := MethUndefined
|
||||
methodOrURL, url, methodFound := strings.Cut(reg, " ")
|
||||
if methodFound {
|
||||
method = MethodFromBytes([]byte(methodOrURL))
|
||||
} else {
|
||||
url = methodOrURL
|
||||
}
|
||||
v.method = method
|
||||
v.uri = url
|
||||
v.handler = handler
|
||||
}
|
||||
var _ Mux = (*MuxSlice)(nil)
|
||||
|
||||
func configSynchronousRouter(t *testing.T, router *Router, bufferSize int, mux Mux) {
|
||||
err := router.Configure(RouterConfig{
|
||||
@@ -204,7 +170,7 @@ func TestRouterGet(t *testing.T) {
|
||||
const bufferSize = 1024
|
||||
const expectResponse = "its time"
|
||||
var (
|
||||
sm sliceMux
|
||||
sm MuxSlice
|
||||
router Router
|
||||
)
|
||||
sm.Handle("GET /", staticPage(t, expectResponse))
|
||||
@@ -230,7 +196,7 @@ func TestRouterGet(t *testing.T) {
|
||||
func TestRouterRequestVisibleToHandler(t *testing.T) {
|
||||
const bufferSize = 1024
|
||||
var (
|
||||
sm sliceMux
|
||||
sm MuxSlice
|
||||
router Router
|
||||
)
|
||||
var gotMethod, gotURI, gotHost string
|
||||
@@ -276,7 +242,7 @@ func TestRouterMux(t *testing.T) {
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
var (
|
||||
sm sliceMux
|
||||
sm MuxSlice
|
||||
router Router
|
||||
)
|
||||
sm.Handle("GET /", staticPage(t, "root"))
|
||||
@@ -309,7 +275,7 @@ func TestRouterSplitRequest(t *testing.T) {
|
||||
const bufferSize = 1024
|
||||
const expectResponse = "split ok"
|
||||
var (
|
||||
sm sliceMux
|
||||
sm MuxSlice
|
||||
router Router
|
||||
)
|
||||
sm.Handle("GET /", staticPage(t, expectResponse))
|
||||
@@ -344,7 +310,7 @@ func staticPage(t *testing.T, page string) HandlerFunc {
|
||||
func TestRouterConfigureHandleRace(t *testing.T) {
|
||||
const bufferSize = 1024
|
||||
var (
|
||||
sm sliceMux
|
||||
sm MuxSlice
|
||||
router Router
|
||||
)
|
||||
sm.Handle("GET /", staticPage(t, "ok"))
|
||||
@@ -370,7 +336,7 @@ func TestRouterConfigureHandleRace(t *testing.T) {
|
||||
// sending a connection on. Connections may be dropped, but never panic.
|
||||
func TestRouterConfigureDuringWorkerHandle(t *testing.T) {
|
||||
var (
|
||||
sm sliceMux
|
||||
sm MuxSlice
|
||||
router Router
|
||||
)
|
||||
sm.Handle("GET /", staticPage(t, "ok"))
|
||||
|
||||
Reference in New Issue
Block a user