mirror of
https://github.com/lukaszraczylo/claude-mnemonic.git
synced 2026-10-12 04:55:18 +00:00
perf(hooks): replace net/http with minimal HTTP/1.0 client to shrink hook binaries
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
# Experiment journal
|
||||||
|
|
||||||
|
## Campaign B: binary size
|
||||||
|
|
||||||
|
Metric: sum of bytes of worker, mcp and 6 hook binaries (release flags, `-s -w -trimpath`, `-tags fts5`). Lower is better.
|
||||||
|
Threshold: at least 3% drop, tests green. Stop: 5 consecutive no-improve trials or 15 trials.
|
||||||
|
Mutate: Go code and build flags. Frozen: public API, DB schema, plugin manifest.
|
||||||
|
|
||||||
|
| id | mutation | total bytes | delta vs base | kept? |
|
||||||
|
|----|----------|-------------|---------------|-------|
|
||||||
|
| 01 | baseline | 102698416 | - | base |
|
||||||
|
| 02 | pkg/hooks: raw HTTP/1.0 client replaces net/http | 87603952 | -14.7% | KEEP |
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
package hooks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxResponseBytes caps a worker response so a misbehaving peer cannot exhaust memory.
|
||||||
|
const maxResponseBytes = 32 << 20
|
||||||
|
|
||||||
|
// Minimal HTTP/1.0 client over a raw TCP socket. Linking net/http adds ~3 MB to
|
||||||
|
// every hook binary; hooks only speak plain JSON to the local worker. HTTP/1.0
|
||||||
|
// makes the server skip chunked encoding and close the connection, so the body
|
||||||
|
// is simply everything after the header block.
|
||||||
|
type httpResponse struct {
|
||||||
|
Status int
|
||||||
|
StatusText string
|
||||||
|
Body []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func httpDo(ctx context.Context, timeout time.Duration, method string, port int, path string, body []byte) (*httpResponse, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
addr := net.JoinHostPort("127.0.0.1", strconv.Itoa(port))
|
||||||
|
var d net.Dialer
|
||||||
|
conn, err := d.DialContext(ctx, "tcp", addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() { _ = conn.Close() }()
|
||||||
|
|
||||||
|
if dl, ok := ctx.Deadline(); ok {
|
||||||
|
_ = conn.SetDeadline(dl)
|
||||||
|
}
|
||||||
|
stop := context.AfterFunc(ctx, func() { _ = conn.Close() })
|
||||||
|
defer stop()
|
||||||
|
|
||||||
|
var req bytes.Buffer
|
||||||
|
fmt.Fprintf(&req, "%s %s HTTP/1.0\r\nHost: %s\r\n", method, path, addr)
|
||||||
|
if body != nil {
|
||||||
|
fmt.Fprintf(&req, "Content-Type: application/json\r\nContent-Length: %d\r\n", len(body))
|
||||||
|
}
|
||||||
|
req.WriteString("\r\n")
|
||||||
|
req.Write(body)
|
||||||
|
if _, err := conn.Write(req.Bytes()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := io.ReadAll(io.LimitReader(bufio.NewReader(conn), maxResponseBytes))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseHTTPResponse(raw)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseHTTPResponse(raw []byte) (*httpResponse, error) {
|
||||||
|
head, body, found := bytes.Cut(raw, []byte("\r\n\r\n"))
|
||||||
|
if !found {
|
||||||
|
return nil, errors.New("malformed http response")
|
||||||
|
}
|
||||||
|
statusLine, _, _ := strings.Cut(string(head), "\r\n")
|
||||||
|
_, rest, ok := strings.Cut(statusLine, " ")
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("malformed status line %q", statusLine)
|
||||||
|
}
|
||||||
|
codeStr, _, _ := strings.Cut(rest, " ")
|
||||||
|
code, err := strconv.Atoi(codeStr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("malformed status line %q", statusLine)
|
||||||
|
}
|
||||||
|
return &httpResponse{Status: code, StatusText: rest, Body: body}, nil
|
||||||
|
}
|
||||||
+39
-137
@@ -2,12 +2,10 @@
|
|||||||
package hooks
|
package hooks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -28,6 +26,9 @@ const (
|
|||||||
// HealthCheckTimeout is the timeout for health checks.
|
// HealthCheckTimeout is the timeout for health checks.
|
||||||
HealthCheckTimeout = 2 * time.Second
|
HealthCheckTimeout = 2 * time.Second
|
||||||
|
|
||||||
|
// hookTimeout bounds a single hook->worker request.
|
||||||
|
hookTimeout = 10 * time.Second
|
||||||
|
|
||||||
// StartupTimeout is the timeout for worker startup.
|
// StartupTimeout is the timeout for worker startup.
|
||||||
StartupTimeout = 10 * time.Second
|
StartupTimeout = 10 * time.Second
|
||||||
|
|
||||||
@@ -52,26 +53,6 @@ var (
|
|||||||
// circuitBreakerMu protects lastStartupFailure.
|
// circuitBreakerMu protects lastStartupFailure.
|
||||||
circuitBreakerMu sync.Mutex
|
circuitBreakerMu sync.Mutex
|
||||||
lastStartupFailure time.Time
|
lastStartupFailure time.Time
|
||||||
|
|
||||||
// hookClient is a shared HTTP client for hook->worker requests.
|
|
||||||
// DisableKeepAlives prevents TIME_WAIT connection leaks since each hook
|
|
||||||
// is a separate OS process that exits quickly.
|
|
||||||
hookClient = &http.Client{
|
|
||||||
Timeout: 10 * time.Second,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DisableKeepAlives: true,
|
|
||||||
MaxIdleConns: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// healthClient is a shared HTTP client for health/version checks.
|
|
||||||
healthClient = &http.Client{
|
|
||||||
Timeout: HealthCheckTimeout,
|
|
||||||
Transport: &http.Transport{
|
|
||||||
DisableKeepAlives: true,
|
|
||||||
MaxIdleConns: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsWorkerAvailable performs a fast check without network calls.
|
// IsWorkerAvailable performs a fast check without network calls.
|
||||||
@@ -110,22 +91,21 @@ func GetWorkerPort() int {
|
|||||||
// Parses the JSON health response to check the "ready" field when available.
|
// Parses the JSON health response to check the "ready" field when available.
|
||||||
// Falls back to HTTP status code check for backwards compatibility.
|
// Falls back to HTTP status code check for backwards compatibility.
|
||||||
func IsWorkerRunning(port int) bool {
|
func IsWorkerRunning(port int) bool {
|
||||||
resp, err := healthClient.Get(fmt.Sprintf("http://127.0.0.1:%d/api/health", port))
|
resp, err := httpDo(context.Background(), HealthCheckTimeout, "GET", port, "/api/health", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
// Try to parse JSON response for structured health check
|
// Try to parse JSON response for structured health check
|
||||||
var health struct {
|
var health struct {
|
||||||
Ready bool `json:"ready"`
|
Ready bool `json:"ready"`
|
||||||
}
|
}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&health); err == nil {
|
if err := json.Unmarshal(resp.Body, &health); err == nil {
|
||||||
return health.Ready
|
return health.Ready
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: treat HTTP 200 as healthy (backwards compatibility)
|
// Fallback: treat HTTP 200 as healthy (backwards compatibility)
|
||||||
return resp.StatusCode == http.StatusOK
|
return resp.Status == 200
|
||||||
}
|
}
|
||||||
|
|
||||||
// workerCachePath returns the path to the worker cache file.
|
// workerCachePath returns the path to the worker cache file.
|
||||||
@@ -396,18 +376,17 @@ func updateCacheFromPort(port int) {
|
|||||||
|
|
||||||
// GetWorkerVersion gets the version of the running worker.
|
// GetWorkerVersion gets the version of the running worker.
|
||||||
func GetWorkerVersion(port int) string {
|
func GetWorkerVersion(port int) string {
|
||||||
resp, err := healthClient.Get(fmt.Sprintf("http://127.0.0.1:%d/api/version", port))
|
resp, err := httpDo(context.Background(), HealthCheckTimeout, "GET", port, "/api/version", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.Status != 200 {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
var result map[string]string
|
var result map[string]string
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
if err := json.Unmarshal(resp.Body, &result); err != nil {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -510,58 +489,46 @@ func findWorkerBinary() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST sends a POST request to the worker.
|
// request performs one worker call and returns the decoded JSON object.
|
||||||
func POST(port int, path string, body interface{}) (map[string]interface{}, error) {
|
// A non-JSON body yields (nil, nil) when lenient, else the decode error.
|
||||||
jsonBody, err := json.Marshal(body)
|
func request(ctx context.Context, method string, port int, path string, body interface{}, lenient bool) (map[string]interface{}, error) {
|
||||||
|
var payload []byte
|
||||||
|
if body != nil {
|
||||||
|
var err error
|
||||||
|
if payload, err = json.Marshal(body); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := httpDo(ctx, hookTimeout, method, port, path, payload)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if resp.Status >= 400 {
|
||||||
resp, err := hookClient.Post(
|
return nil, fmt.Errorf("request failed: %s", resp.StatusText)
|
||||||
fmt.Sprintf("http://127.0.0.1:%d%s", port, path),
|
|
||||||
"application/json",
|
|
||||||
bytes.NewReader(jsonBody),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
return nil, fmt.Errorf("request failed: %s", resp.Status)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var result map[string]interface{}
|
var result map[string]interface{}
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
if err := json.Unmarshal(resp.Body, &result); err != nil {
|
||||||
// Not all endpoints return JSON
|
if lenient {
|
||||||
return nil, nil
|
// Not all endpoints return JSON
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// POST sends a POST request to the worker.
|
||||||
|
func POST(port int, path string, body interface{}) (map[string]interface{}, error) {
|
||||||
|
return request(context.Background(), "POST", port, path, body, true)
|
||||||
|
}
|
||||||
|
|
||||||
// POSTWithContext sends a POST request using the provided context.
|
// POSTWithContext sends a POST request using the provided context.
|
||||||
// Used for fire-and-forget calls where we want to control the timeout externally.
|
// Used for fire-and-forget calls where we want to control the timeout externally.
|
||||||
func POSTWithContext(ctx context.Context, port int, path string, body interface{}) error {
|
func POSTWithContext(ctx context.Context, port int, path string, body interface{}) error {
|
||||||
jsonBody, err := json.Marshal(body)
|
_, err := request(ctx, "POST", port, path, body, true)
|
||||||
if err != nil {
|
return err
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
|
||||||
fmt.Sprintf("http://127.0.0.1:%d%s", port, path),
|
|
||||||
bytes.NewReader(jsonBody))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
resp, err := hookClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// POSTWithContextResult sends a POST request using the provided context and
|
// POSTWithContextResult sends a POST request using the provided context and
|
||||||
@@ -570,56 +537,12 @@ func POSTWithContext(ctx context.Context, port int, path string, body interface{
|
|||||||
// aborts at the hook deadline instead of blocking for the full client timeout.
|
// aborts at the hook deadline instead of blocking for the full client timeout.
|
||||||
// A non-JSON body is returned as (nil, nil), matching POST's behavior.
|
// A non-JSON body is returned as (nil, nil), matching POST's behavior.
|
||||||
func POSTWithContextResult(ctx context.Context, port int, path string, body interface{}) (map[string]interface{}, error) {
|
func POSTWithContextResult(ctx context.Context, port int, path string, body interface{}) (map[string]interface{}, error) {
|
||||||
jsonBody, err := json.Marshal(body)
|
return request(ctx, "POST", port, path, body, true)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost,
|
|
||||||
fmt.Sprintf("http://127.0.0.1:%d%s", port, path),
|
|
||||||
bytes.NewReader(jsonBody))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
|
|
||||||
resp, err := hookClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
return nil, fmt.Errorf("request failed: %s", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
var result map[string]interface{}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
||||||
// Not all endpoints return JSON
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GET sends a GET request to the worker.
|
// GET sends a GET request to the worker.
|
||||||
func GET(port int, path string) (map[string]interface{}, error) {
|
func GET(port int, path string) (map[string]interface{}, error) {
|
||||||
resp, err := hookClient.Get(fmt.Sprintf("http://127.0.0.1:%d%s", port, path))
|
return request(context.Background(), "GET", port, path, nil, false)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
return nil, fmt.Errorf("request failed: %s", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
var result map[string]interface{}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// GETWithContext sends a GET request using the provided context and decodes the
|
// GETWithContext sends a GET request using the provided context and decodes the
|
||||||
@@ -627,28 +550,7 @@ func GET(port int, path string) (map[string]interface{}, error) {
|
|||||||
// Used on the prompt critical path so a wedged worker aborts at the hook
|
// Used on the prompt critical path so a wedged worker aborts at the hook
|
||||||
// deadline instead of blocking for the full client timeout.
|
// deadline instead of blocking for the full client timeout.
|
||||||
func GETWithContext(ctx context.Context, port int, path string) (map[string]interface{}, error) {
|
func GETWithContext(ctx context.Context, port int, path string) (map[string]interface{}, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
|
return request(ctx, "GET", port, path, nil, false)
|
||||||
fmt.Sprintf("http://127.0.0.1:%d%s", port, path), nil)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := hookClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
defer func() { _ = resp.Body.Close() }()
|
|
||||||
|
|
||||||
if resp.StatusCode >= 400 {
|
|
||||||
return nil, fmt.Errorf("request failed: %s", resp.Status)
|
|
||||||
}
|
|
||||||
|
|
||||||
var result map[string]interface{}
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// versionsCompatible checks if two versions are compatible for dev builds.
|
// versionsCompatible checks if two versions are compatible for dev builds.
|
||||||
|
|||||||
+98
-15
@@ -5,8 +5,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -1481,22 +1485,101 @@ func TestSleepCtx_Cancelled(t *testing.T) {
|
|||||||
assert.Less(t, elapsed, 500*time.Millisecond)
|
assert.Less(t, elapsed, 500*time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHookClients_DisableKeepAlives asserts shared clients disable keep-alives
|
// TestHTTPDo_Behaviour covers the raw HTTP/1.0 client against a real server.
|
||||||
// to prevent TIME_WAIT connection leaks in short-lived hook processes.
|
func TestHTTPDo_Behaviour(t *testing.T) {
|
||||||
func TestHookClients_DisableKeepAlives(t *testing.T) {
|
big := strings.Repeat("x", 200_000)
|
||||||
hTransport, ok := hookClient.Transport.(*http.Transport)
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
require.True(t, ok, "hookClient.Transport should be *http.Transport")
|
switch r.URL.Path {
|
||||||
assert.True(t, hTransport.DisableKeepAlives, "hookClient must disable keep-alives")
|
case "/echo":
|
||||||
assert.Equal(t, 1, hTransport.MaxIdleConns)
|
b, _ := io.ReadAll(r.Body)
|
||||||
|
assert.Equal(t, "application/json", r.Header.Get("Content-Type"))
|
||||||
|
_, _ = w.Write(b)
|
||||||
|
case "/big":
|
||||||
|
_, _ = w.Write([]byte(`{"v":"` + big + `"}`))
|
||||||
|
case "/missing":
|
||||||
|
w.WriteHeader(http.StatusNotFound)
|
||||||
|
case "/empty":
|
||||||
|
w.WriteHeader(http.StatusNoContent)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
port, err := strconv.Atoi(srv.URL[strings.LastIndex(srv.URL, ":")+1:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
hcTransport, ok := healthClient.Transport.(*http.Transport)
|
tests := []struct {
|
||||||
require.True(t, ok, "healthClient.Transport should be *http.Transport")
|
name string
|
||||||
assert.True(t, hcTransport.DisableKeepAlives, "healthClient must disable keep-alives")
|
method string
|
||||||
assert.Equal(t, 1, hcTransport.MaxIdleConns)
|
path string
|
||||||
|
body []byte
|
||||||
|
wantStatus int
|
||||||
|
wantLen int
|
||||||
|
}{
|
||||||
|
{"post echoes body", "POST", "/echo", []byte(`{"a":1}`), 200, 7},
|
||||||
|
{"large body over server buffer", "GET", "/big", nil, 200, len(big) + 8},
|
||||||
|
{"not found", "GET", "/missing", nil, 404, 0},
|
||||||
|
{"no content", "GET", "/empty", nil, 204, 0},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
resp, err := httpDo(context.Background(), time.Second, tt.method, port, tt.path, tt.body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, tt.wantStatus, resp.Status)
|
||||||
|
assert.Len(t, resp.Body, tt.wantLen)
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHookClient_Timeout verifies hookClient timeout is set.
|
// TestHTTPDo_Failures covers connection, timeout, cancel and malformed replies.
|
||||||
func TestHookClient_Timeout(t *testing.T) {
|
func TestHTTPDo_Failures(t *testing.T) {
|
||||||
assert.Equal(t, 10*time.Second, hookClient.Timeout)
|
t.Run("connection refused", func(t *testing.T) {
|
||||||
assert.Equal(t, HealthCheckTimeout, healthClient.Timeout)
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
port := l.Addr().(*net.TCPAddr).Port
|
||||||
|
require.NoError(t, l.Close())
|
||||||
|
_, err = httpDo(context.Background(), time.Second, "GET", port, "/", nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("wedged server times out", func(t *testing.T) {
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = l.Close() }()
|
||||||
|
go func() {
|
||||||
|
c, err := l.Accept()
|
||||||
|
if err == nil {
|
||||||
|
defer func() { _ = c.Close() }()
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
start := time.Now()
|
||||||
|
_, err = httpDo(context.Background(), 200*time.Millisecond, "GET", l.Addr().(*net.TCPAddr).Port, "/", nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Less(t, time.Since(start), time.Second)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cancelled context aborts", func(t *testing.T) {
|
||||||
|
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() { _ = l.Close() }()
|
||||||
|
go func() {
|
||||||
|
c, err := l.Accept()
|
||||||
|
if err == nil {
|
||||||
|
defer func() { _ = c.Close() }()
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 100*time.Millisecond)
|
||||||
|
defer cancel()
|
||||||
|
start := time.Now()
|
||||||
|
_, err = httpDo(ctx, 5*time.Second, "GET", l.Addr().(*net.TCPAddr).Port, "/", nil)
|
||||||
|
assert.Error(t, err)
|
||||||
|
assert.Less(t, time.Since(start), time.Second)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("malformed response", func(t *testing.T) {
|
||||||
|
for _, raw := range []string{"", "garbage", "HTTP/1.0\r\n\r\n", "HTTP/1.0 abc OK\r\n\r\n"} {
|
||||||
|
_, err := parseHTTPResponse([]byte(raw))
|
||||||
|
assert.Error(t, err, "input %q", raw)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user