feat: cancel native requests on disconnect
This commit is contained in:
104
transport/native/disconnect_test.go
Normal file
104
transport/native/disconnect_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
package native
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
type observedReadConn struct {
|
||||
net.Conn
|
||||
reading chan struct{}
|
||||
}
|
||||
|
||||
func (c *observedReadConn) Read(data []byte) (int, error) {
|
||||
select {
|
||||
case c.reading <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
return c.Conn.Read(data)
|
||||
}
|
||||
|
||||
func TestDisconnectWatchStopsBeforeReaderReuse(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
conn := &observedReadConn{Conn: server, reading: make(chan struct{}, 1)}
|
||||
reader := bufio.NewReader(conn)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
watch := newClientDisconnectWatch(reader, conn, cancel)
|
||||
select {
|
||||
case <-conn.reading:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("disconnect watcher did not start reading")
|
||||
}
|
||||
watch.stop()
|
||||
if ctx.Err() != nil {
|
||||
t.Fatalf("interrupting the watcher canceled a healthy request: %v", ctx.Err())
|
||||
}
|
||||
if err := server.SetReadDeadline(time.Now().Add(time.Second)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
writeDone := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := io.WriteString(client, "GET /next HTTP/1.1\r\n")
|
||||
writeDone <- err
|
||||
}()
|
||||
line, err := reader.ReadString('\n')
|
||||
if err != nil || line != "GET /next HTTP/1.1\r\n" {
|
||||
t.Fatalf("reader retained watcher data or deadline: line=%q err=%v", line, err)
|
||||
}
|
||||
if err := <-writeDone; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDisconnectWatchPreservesPipelineAndWriteFailureCancels(t *testing.T) {
|
||||
server, client := net.Pipe()
|
||||
defer server.Close()
|
||||
defer client.Close()
|
||||
pipeline := "GET /next HTTP/1.1\r\nHost: test\r\n\r\n"
|
||||
reader := bufio.NewReader(strings.NewReader(pipeline))
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
watch := newClientDisconnectWatch(reader, server, cancel)
|
||||
select {
|
||||
case <-watch.done:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("watcher should stop when pipelined data is available")
|
||||
}
|
||||
watch.stop()
|
||||
if ctx.Err() != nil {
|
||||
t.Fatal("pipeline data canceled the current request")
|
||||
}
|
||||
remaining, err := io.ReadAll(reader)
|
||||
if err != nil || string(remaining) != pipeline {
|
||||
t.Fatalf("pipeline bytes changed: data=%q err=%v", remaining, err)
|
||||
}
|
||||
// With pending pipeline data, EOF detection is deferred. A failed stream
|
||||
// write must still cancel downstream work even when the handler ignores it.
|
||||
client.Close()
|
||||
writer := newResponseWriter(http.MethodGet)
|
||||
writer.configure(&http.Request{ProtoMajor: 1, ProtoMinor: 1}, server, false, time.Second, cancel)
|
||||
writer.Flush()
|
||||
if !errors.Is(ctx.Err(), context.Canceled) {
|
||||
t.Fatal("failed Flush did not cancel request context")
|
||||
}
|
||||
if _, err := writer.Write([]byte("more")); err == nil {
|
||||
t.Fatal("failed Flush was not returned by subsequent Write")
|
||||
}
|
||||
if err := writer.writeTo(server, false, time.Second); err == nil {
|
||||
t.Fatal("failed Flush was not returned on response completion")
|
||||
}
|
||||
writer.reset("")
|
||||
if writer.cancel != nil || writer.writeErr != nil || writer.conn != nil {
|
||||
t.Fatal("pooled response retained the previous request")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user