mirror of
https://github.com/soypat/lneto.git
synced 2026-08-22 15:39:06 +00:00
add low level Handle function and more tests
This commit is contained in:
+64
-54
@@ -20,13 +20,14 @@ type conn = io.ReadWriteCloser
|
|||||||
|
|
||||||
// Router hosts concurrent safe data.
|
// Router hosts concurrent safe data.
|
||||||
type Router struct {
|
type Router struct {
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
gen atomic.Uint32
|
gen atomic.Uint32
|
||||||
numGoro int
|
numGoro int
|
||||||
reqBuf int
|
reqBuf int
|
||||||
respBuf int
|
respBuf int
|
||||||
pendingConns chan job
|
normalizeKeys bool
|
||||||
mux Mux
|
pendingConns chan job
|
||||||
|
mux Mux
|
||||||
|
|
||||||
globbuf []byte
|
globbuf []byte
|
||||||
exchs []Exchange
|
exchs []Exchange
|
||||||
@@ -99,6 +100,7 @@ func (r *Router) Configure(cfg RouterConfig) error {
|
|||||||
r.reqBuf = cfg.RequestBufferSize
|
r.reqBuf = cfg.RequestBufferSize
|
||||||
r.respBuf = cfg.ResponseMinBufferSize
|
r.respBuf = cfg.ResponseMinBufferSize
|
||||||
r.mux = cfg.Mux
|
r.mux = cfg.Mux
|
||||||
|
r.normalizeKeys = cfg.NormalizeOutgoingKeys
|
||||||
if !workerMode {
|
if !workerMode {
|
||||||
r.backoff = cfg.Backoff
|
r.backoff = cfg.Backoff
|
||||||
r.numGoro = 0
|
r.numGoro = 0
|
||||||
@@ -127,6 +129,52 @@ func (r *Router) Configure(cfg RouterConfig) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Handle is a extremely low-level HTTP handling method used internally in [Router].
|
||||||
|
// Requires exchange to be acquired and configured. Will panic if any argument is nil.
|
||||||
|
func Handle(exch *Exchange, mux Mux, backoff lneto.BackoffStrategy) error {
|
||||||
|
reqhdr := &exch.reqHdr
|
||||||
|
reqhdr.Reset(nil)
|
||||||
|
var consecutiveBackoffs uint
|
||||||
|
for {
|
||||||
|
n, err := reqhdr.ReadFromLimited(exch.rw, reqhdr.BufferFree())
|
||||||
|
if err != nil {
|
||||||
|
exch.readErr = err
|
||||||
|
return err
|
||||||
|
} else if n == 0 {
|
||||||
|
backoff.Do(consecutiveBackoffs)
|
||||||
|
consecutiveBackoffs++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
consecutiveBackoffs = 0
|
||||||
|
const asRequest = false
|
||||||
|
needMore, err := reqhdr.TryParse(asRequest)
|
||||||
|
if !needMore && err == nil {
|
||||||
|
// Done!
|
||||||
|
break
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Setup Exchange fields necessary for correct functioning.
|
||||||
|
parsed := reqhdr.BufferParsed()
|
||||||
|
exch.respRemains = reqhdr.BufferReceived() - parsed
|
||||||
|
exch.respHeaderOff = uint16(parsed)
|
||||||
|
exch.respHeaderLen = 0
|
||||||
|
// Mux URI.
|
||||||
|
uri := reqhdr.RequestURI()
|
||||||
|
meth := reqhdr.Method()
|
||||||
|
handler := mux.LookupHandler(MethodFromBytes(meth), uri)
|
||||||
|
if handler != nil {
|
||||||
|
handler(exch)
|
||||||
|
exch.FlushHeader()
|
||||||
|
} else {
|
||||||
|
exch.WriteHeader(404)
|
||||||
|
}
|
||||||
|
// TODO write response from exchange here.
|
||||||
|
exch.rw.Close()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *Router) Handle(conn conn) error {
|
func (r *Router) Handle(conn conn) error {
|
||||||
exch := r.getExch(conn)
|
exch := r.getExch(conn)
|
||||||
if exch == nil {
|
if exch == nil {
|
||||||
@@ -161,50 +209,14 @@ func (r *Router) goroWorker(gen uint32, queue chan job, backoff lneto.BackoffStr
|
|||||||
|
|
||||||
func (r *Router) goroHandle(exch *Exchange, backoff lneto.BackoffStrategy, mux Mux) {
|
func (r *Router) goroHandle(exch *Exchange, backoff lneto.BackoffStrategy, mux Mux) {
|
||||||
defer r.freeExch(exch)
|
defer r.freeExch(exch)
|
||||||
reqhdr := &exch.reqHdr
|
err := Handle(exch, mux, backoff)
|
||||||
reqhdr.Reset(nil)
|
if err != nil {
|
||||||
var consecutiveBackoffs uint
|
if exch.readErr != nil {
|
||||||
for {
|
|
||||||
n, err := reqhdr.ReadFromLimited(exch.rw, reqhdr.BufferFree())
|
|
||||||
if err != nil {
|
|
||||||
r.error("goroHandle:ReadFromLimited", slog.String("err", err.Error()))
|
r.error("goroHandle:ReadFromLimited", slog.String("err", err.Error()))
|
||||||
exch.rw.Close()
|
} else {
|
||||||
return
|
r.error("goroHandle:TryParse?", slog.String("err", err.Error()))
|
||||||
} else if n == 0 {
|
|
||||||
backoff(consecutiveBackoffs)
|
|
||||||
consecutiveBackoffs++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
consecutiveBackoffs = 0
|
|
||||||
const asRequest = false
|
|
||||||
needMore, err := reqhdr.TryParse(asRequest)
|
|
||||||
if !needMore && err == nil {
|
|
||||||
// Done!
|
|
||||||
break
|
|
||||||
} else if err != nil {
|
|
||||||
r.error("goroHandle:TryParse", slog.String("err", err.Error()))
|
|
||||||
exch.rw.Close()
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
r.info("goroHandle:headerParsedSuccess")
|
|
||||||
// Setup Exchange fields necessary for correct functioning.
|
|
||||||
parsed := reqhdr.BufferParsed()
|
|
||||||
exch.respRemains = reqhdr.BufferReceived() - parsed
|
|
||||||
exch.respHeaderOff = uint16(parsed)
|
|
||||||
exch.respHeaderLen = 0
|
|
||||||
// Mux URI.
|
|
||||||
uri := reqhdr.RequestURI()
|
|
||||||
meth := reqhdr.Method()
|
|
||||||
handler := mux.LookupHandler(MethodFromBytes(meth), uri)
|
|
||||||
if handler != nil {
|
|
||||||
handler(exch)
|
|
||||||
}
|
|
||||||
// Reuse request space as response header start.
|
|
||||||
|
|
||||||
// TODO write response from exchange here.
|
|
||||||
exch.rw.Close()
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) freeExch(exch *Exchange) {
|
func (r *Router) freeExch(exch *Exchange) {
|
||||||
@@ -245,17 +257,13 @@ func (r *Router) getExch(conn conn) (exch *Exchange) {
|
|||||||
|
|
||||||
if r.numGoro == 0 {
|
if r.numGoro == 0 {
|
||||||
exch := new(Exchange)
|
exch := new(Exchange)
|
||||||
exch.Configure(make([]byte, r.respBuf+r.reqBuf), r.reqBuf, false)
|
exch.Configure(make([]byte, r.respBuf+r.reqBuf), r.reqBuf, r.normalizeKeys)
|
||||||
|
exch.Acquire(conn) // Fresh exchange, CAS cannot fail.
|
||||||
return exch
|
return exch
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Router) allocBuffers(exch *Exchange) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Router) error(msg string, attrs ...slog.Attr) {
|
func (r *Router) error(msg string, attrs ...slog.Attr) {
|
||||||
internal.LogAttrs(r.log, slog.LevelError, msg, attrs...)
|
internal.LogAttrs(r.log, slog.LevelError, msg, attrs...)
|
||||||
}
|
}
|
||||||
@@ -279,6 +287,7 @@ type Exchange struct {
|
|||||||
headerWritten bool
|
headerWritten bool
|
||||||
normalizeKeys bool
|
normalizeKeys bool
|
||||||
nextFree *Exchange
|
nextFree *Exchange
|
||||||
|
readErr error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (exch *Exchange) Configure(rawbuf []byte, requestLim int, normalizeKeys bool) {
|
func (exch *Exchange) Configure(rawbuf []byte, requestLim int, normalizeKeys bool) {
|
||||||
@@ -295,6 +304,7 @@ func (exch *Exchange) Acquire(conn conn) bool {
|
|||||||
if !exch.used.CompareAndSwap(false, true) {
|
if !exch.used.CompareAndSwap(false, true) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
exch.readErr = nil
|
||||||
exch.respTopWritten = 0
|
exch.respTopWritten = 0
|
||||||
exch.respHeaderOff = 0
|
exch.respHeaderOff = 0
|
||||||
exch.respHeaderLen = 0
|
exch.respHeaderLen = 0
|
||||||
|
|||||||
+164
-9
@@ -6,6 +6,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -13,26 +14,62 @@ import (
|
|||||||
"github.com/soypat/lneto/internal"
|
"github.com/soypat/lneto/internal"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// rwconn is a in-memory conn. The router handles connections on another
|
||||||
|
// goroutine so every field is guarded; onClose lets tests await the handler.
|
||||||
type rwconn struct {
|
type rwconn struct {
|
||||||
|
mu sync.Mutex
|
||||||
readable bytes.Buffer
|
readable bytes.Buffer
|
||||||
written bytes.Buffer
|
written bytes.Buffer
|
||||||
closed bool
|
closed bool
|
||||||
|
onClose chan struct{}
|
||||||
deadline time.Time
|
deadline time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newConn returns a conn preloaded with request and whose Close is observable
|
||||||
|
// with [rwconn.AwaitClose].
|
||||||
|
func newConn(request string) *rwconn {
|
||||||
|
r := &rwconn{onClose: make(chan struct{})}
|
||||||
|
r.AddReadable([]byte(request))
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func (r *rwconn) Close() error {
|
func (r *rwconn) Close() error {
|
||||||
r.closed = true
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
if !r.closed {
|
||||||
|
r.closed = true
|
||||||
|
if r.onClose != nil {
|
||||||
|
close(r.onClose)
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AwaitClose blocks until the connection is closed by its handler or timeout elapses.
|
||||||
|
func (r *rwconn) AwaitClose(t *testing.T, timeout time.Duration) {
|
||||||
|
t.Helper()
|
||||||
|
select {
|
||||||
|
case <-r.onClose:
|
||||||
|
case <-time.After(timeout):
|
||||||
|
t.Fatal("timed out awaiting connection close by handler")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (r *rwconn) Read(b []byte) (int, error) {
|
func (r *rwconn) Read(b []byte) (int, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
if r.closed {
|
if r.closed {
|
||||||
return 0, net.ErrClosed
|
return 0, net.ErrClosed
|
||||||
} else if r.deadlineExceeded() {
|
} else if r.deadlineExceeded() {
|
||||||
return 0, context.DeadlineExceeded
|
return 0, context.DeadlineExceeded
|
||||||
|
} else if r.readable.Len() == 0 {
|
||||||
|
return 0, io.EOF
|
||||||
}
|
}
|
||||||
return r.readable.Read(b)
|
return r.readable.Read(b)
|
||||||
}
|
}
|
||||||
func (r *rwconn) Write(b []byte) (int, error) {
|
func (r *rwconn) Write(b []byte) (int, error) {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
if r.closed {
|
if r.closed {
|
||||||
return 0, net.ErrClosed
|
return 0, net.ErrClosed
|
||||||
} else if r.deadlineExceeded() {
|
} else if r.deadlineExceeded() {
|
||||||
@@ -43,8 +80,16 @@ func (r *rwconn) Write(b []byte) (int, error) {
|
|||||||
func (r *rwconn) deadlineExceeded() bool {
|
func (r *rwconn) deadlineExceeded() bool {
|
||||||
return !r.deadline.IsZero() && time.Since(r.deadline) > 0
|
return !r.deadline.IsZero() && time.Since(r.deadline) > 0
|
||||||
}
|
}
|
||||||
func (r *rwconn) AddReadable(b []byte) { r.readable.Write(b) }
|
func (r *rwconn) AddReadable(b []byte) {
|
||||||
func (r *rwconn) ViewWritten() []byte { return r.written.Bytes() }
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
r.readable.Write(b)
|
||||||
|
}
|
||||||
|
func (r *rwconn) ViewWritten() string {
|
||||||
|
r.mu.Lock()
|
||||||
|
defer r.mu.Unlock()
|
||||||
|
return r.written.String()
|
||||||
|
}
|
||||||
|
|
||||||
var _ Mux = (*sliceMux)(nil)
|
var _ Mux = (*sliceMux)(nil)
|
||||||
|
|
||||||
@@ -99,29 +144,139 @@ func configSynchronousRouter(t *testing.T, router *Router, bufferSize int, mux M
|
|||||||
|
|
||||||
func TestRouterGet(t *testing.T) {
|
func TestRouterGet(t *testing.T) {
|
||||||
const bufferSize = 1024
|
const bufferSize = 1024
|
||||||
|
|
||||||
const expectResponse = "its time"
|
const expectResponse = "its time"
|
||||||
var (
|
var (
|
||||||
sm sliceMux
|
sm sliceMux
|
||||||
router Router
|
router Router
|
||||||
conn rwconn
|
|
||||||
)
|
)
|
||||||
conn.deadline = time.Now().Add(10 * time.Millisecond) // TODO: remove after tests run without blocking forever.
|
|
||||||
sm.Handle("GET /", staticPage(t, expectResponse))
|
sm.Handle("GET /", staticPage(t, expectResponse))
|
||||||
configSynchronousRouter(t, &router, bufferSize, &sm)
|
configSynchronousRouter(t, &router, bufferSize, &sm)
|
||||||
err := router.Handle(&conn)
|
|
||||||
|
conn := newConn("GET / HTTP/1.1\r\nHost: tinygo.org\r\n\r\n")
|
||||||
|
err := router.Handle(conn)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
conn.AwaitClose(t, time.Second)
|
||||||
|
|
||||||
|
got := conn.ViewWritten()
|
||||||
|
if !strings.HasPrefix(got, "HTTP/1.1 200 OK\r\n") {
|
||||||
|
t.Errorf("want 200 status line, got %q", got)
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(got, expectResponse) {
|
||||||
|
t.Errorf("want body %q at end of response, got %q", expectResponse, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The handler observes the request line and header fields the router parsed.
|
||||||
|
func TestRouterRequestVisibleToHandler(t *testing.T) {
|
||||||
|
const bufferSize = 1024
|
||||||
|
var (
|
||||||
|
sm sliceMux
|
||||||
|
router Router
|
||||||
|
)
|
||||||
|
var gotMethod, gotURI, gotHost string
|
||||||
|
sm.Handle("GET /index.html", func(ex *Exchange) {
|
||||||
|
gotMethod = string(ex.RequestMethod())
|
||||||
|
gotURI = string(ex.RequestURI())
|
||||||
|
gotHost = string(ex.RequestHeader("Host"))
|
||||||
|
ex.WriteHeader(200)
|
||||||
|
})
|
||||||
|
configSynchronousRouter(t, &router, bufferSize, &sm)
|
||||||
|
|
||||||
|
conn := newConn("GET /index.html HTTP/1.1\r\nHost: tinygo.org\r\n\r\n")
|
||||||
|
if err := router.Handle(conn); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
conn.AwaitClose(t, time.Second)
|
||||||
|
|
||||||
|
if gotMethod != "GET" {
|
||||||
|
t.Errorf("want method %q, got %q", "GET", gotMethod)
|
||||||
|
}
|
||||||
|
if gotURI != "/index.html" {
|
||||||
|
t.Errorf("want URI %q, got %q", "/index.html", gotURI)
|
||||||
|
}
|
||||||
|
if gotHost != "tinygo.org" {
|
||||||
|
t.Errorf("want Host %q, got %q", "tinygo.org", gotHost)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Router must route on method and URI, and must not invoke a handler for
|
||||||
|
// requests it has no registration for.
|
||||||
|
func TestRouterMux(t *testing.T) {
|
||||||
|
const bufferSize = 1024
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
request string
|
||||||
|
want string // Response body, empty means no handler must run.
|
||||||
|
}{
|
||||||
|
{name: "get root", request: "GET / HTTP/1.1\r\nHost: h\r\n\r\n", want: "root"},
|
||||||
|
{name: "get page", request: "GET /page HTTP/1.1\r\nHost: h\r\n\r\n", want: "page"},
|
||||||
|
{name: "any method", request: "DELETE /any HTTP/1.1\r\nHost: h\r\n\r\n", want: "any"},
|
||||||
|
{name: "method mismatch", request: "POST / HTTP/1.1\r\nHost: h\r\n\r\n", want: ""},
|
||||||
|
{name: "unknown uri", request: "GET /nowhere HTTP/1.1\r\nHost: h\r\n\r\n", want: ""},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
var (
|
||||||
|
sm sliceMux
|
||||||
|
router Router
|
||||||
|
)
|
||||||
|
sm.Handle("GET /", staticPage(t, "root"))
|
||||||
|
sm.Handle("GET /page", staticPage(t, "page"))
|
||||||
|
sm.Handle("/any", staticPage(t, "any")) // No method: matches any.
|
||||||
|
configSynchronousRouter(t, &router, bufferSize, &sm)
|
||||||
|
|
||||||
|
conn := newConn(test.request)
|
||||||
|
if err := router.Handle(conn); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
conn.AwaitClose(t, time.Second)
|
||||||
|
|
||||||
|
got := conn.ViewWritten()
|
||||||
|
if test.want == "" {
|
||||||
|
if strings.Contains(got, "root") || strings.Contains(got, "page") || strings.Contains(got, "any") {
|
||||||
|
t.Errorf("no handler must run, got response %q", got)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(got, test.want) {
|
||||||
|
t.Errorf("want body %q, got response %q", test.want, got)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A request arriving in pieces (TCP segmentation) must still be handled.
|
||||||
|
func TestRouterSplitRequest(t *testing.T) {
|
||||||
|
const bufferSize = 1024
|
||||||
|
const expectResponse = "split ok"
|
||||||
|
var (
|
||||||
|
sm sliceMux
|
||||||
|
router Router
|
||||||
|
)
|
||||||
|
sm.Handle("GET /", staticPage(t, expectResponse))
|
||||||
|
configSynchronousRouter(t, &router, bufferSize, &sm)
|
||||||
|
|
||||||
|
conn := newConn("GET / HTTP/1.1\r\nHo")
|
||||||
|
if err := router.Handle(conn); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
conn.AddReadable([]byte("st: tinygo.org\r\n\r\n"))
|
||||||
|
conn.AwaitClose(t, time.Second)
|
||||||
|
|
||||||
|
if got := conn.ViewWritten(); !strings.HasSuffix(got, expectResponse) {
|
||||||
|
t.Errorf("want body %q, got response %q", expectResponse, got)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func staticPage(t *testing.T, page string) HandlerFunc {
|
func staticPage(t *testing.T, page string) HandlerFunc {
|
||||||
return func(ex *Exchange) {
|
return func(ex *Exchange) {
|
||||||
n, err := io.WriteString(ex, page)
|
n, err := io.WriteString(ex, page)
|
||||||
|
// Handler runs on the router goroutine: Error, never Fatal.
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Error(err)
|
||||||
} else if n != len(page) {
|
} else if n != len(page) {
|
||||||
t.Fatal("expected written ", len(page), "got", n)
|
t.Error("expected written ", len(page), "got", n)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ const (
|
|||||||
flagReaderEOF
|
flagReaderEOF
|
||||||
// set if [Header.SetStatus] or [Header.SetStatusInt] has been called.
|
// set if [Header.SetStatus] or [Header.SetStatusInt] has been called.
|
||||||
FlagStatusSet
|
FlagStatusSet
|
||||||
|
FlagReaderError
|
||||||
)
|
)
|
||||||
|
|
||||||
func (f Flags) HasAny(checkThese Flags) bool {
|
func (f Flags) HasAny(checkThese Flags) bool {
|
||||||
|
|||||||
Reference in New Issue
Block a user