improve locking and acquisition of Exchanges in reconfiguring

This commit is contained in:
Patricio Whittingslow
2026-07-25 21:31:01 -03:00
parent d53a7db37c
commit 97c6936cf9
3 changed files with 273 additions and 34 deletions
+84 -1
View File
@@ -23,9 +23,11 @@ import (
type rwconn struct {
mu sync.Mutex
readable bytes.Buffer
segments []string
written bytes.Buffer
closed bool
hangup bool
failWr int
onClose chan struct{}
deadline time.Time
}
@@ -38,6 +40,23 @@ func newConn(request string) *rwconn {
return r
}
// AddSegment queues data delivered on a later read, once everything already
// pending has been read. Models a request split over several TCP segments
// without depending on goroutine scheduling.
func (r *rwconn) AddSegment(b string) {
r.mu.Lock()
defer r.mu.Unlock()
r.segments = append(r.segments, b)
}
// FailWrites makes the next n writes fail, as a conn refusing further data
// would. Later writes succeed.
func (r *rwconn) FailWrites(n int) {
r.mu.Lock()
defer r.mu.Unlock()
r.failWr = n
}
// Hangup makes reads past the pending data return [io.EOF], as a peer that
// closed its side of the connection would.
func (r *rwconn) Hangup() {
@@ -76,6 +95,11 @@ func (r *rwconn) Read(b []byte) (int, error) {
} else if r.deadlineExceeded() {
return 0, context.DeadlineExceeded
} else if r.readable.Len() == 0 {
if len(r.segments) > 0 {
r.readable.WriteString(r.segments[0])
r.segments = r.segments[1:]
return r.readable.Read(b)
}
if r.hangup {
return 0, io.EOF
}
@@ -90,6 +114,9 @@ func (r *rwconn) Write(b []byte) (int, error) {
return 0, net.ErrClosed
} else if r.deadlineExceeded() {
return 0, context.DeadlineExceeded
} else if r.failWr > 0 {
r.failWr--
return 0, io.ErrShortWrite
}
return r.written.Write(b)
}
@@ -101,6 +128,14 @@ 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) {
r.mu.Lock()
defer r.mu.Unlock()
r.deadline = t
}
// IsClosed reports whether the connection was closed by its handler.
func (r *rwconn) IsClosed() bool {
r.mu.Lock()
@@ -281,10 +316,11 @@ func TestRouterSplitRequest(t *testing.T) {
configSynchronousRouter(t, &router, bufferSize, &sm)
conn := newConn("GET / HTTP/1.1\r\nHo")
conn.AddSegment("st: tinygo.org\r\n\r")
conn.AddSegment("\n") // Final CRLF lands in its own segment.
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) {
@@ -329,3 +365,50 @@ func TestRouterConfigureHandleRace(t *testing.T) {
}()
wg.Wait()
}
// Reconfiguring a running router tears down the job queue that Handle may be
// sending a connection on. Connections may be dropped, but never panic.
func TestRouterConfigureDuringWorkerHandle(t *testing.T) {
var (
sm sliceMux
router Router
)
sm.Handle("GET /", staticPage(t, "ok"))
cfg := RouterConfig{
FixedNumGoroutines: 2,
MaxAwaitingConns: 4,
Mux: &sm,
RequestBufferSize: 512,
ResponseMinBufferSize: 512,
Backoff: nopBackoff,
}
if err := router.Configure(cfg); err != nil {
t.Fatal(err)
}
defer router.TeardownGoroutines()
var wg sync.WaitGroup
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 300; i++ {
conn := newConn("GET / HTTP/1.1\r\nHost: h\r\n\r\n")
conn.Hangup()
router.Handle(conn) // Drops are fine, panics are not.
}
}()
go func() {
defer wg.Done()
// Each Configure sleeps 5ms tearing down the previous generation, keep
// the count low and let the Handle loop supply the concurrency.
for i := 0; i < 20; i++ {
// errBusyExchanges is legitimate backpressure: the previous
// generation was still serving when the buffers were needed.
if err := router.Configure(cfg); err != nil && err != errBusyExchanges {
t.Error(err)
return
}
}
}()
wg.Wait()
}