perf(hooks): replace net/http with minimal HTTP/1.0 client to shrink hook binaries

This commit is contained in:
2026-09-30 01:24:34 +01:00
parent ee285441e4
commit 41b774f973
4 changed files with 230 additions and 152 deletions
+12
View File
@@ -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 |
+81
View File
@@ -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
View File
@@ -2,12 +2,10 @@
package hooks
import (
"bytes"
"context"
"encoding/json"
"fmt"
"net"
"net/http"
"os"
"os/exec"
"path/filepath"
@@ -28,6 +26,9 @@ const (
// HealthCheckTimeout is the timeout for health checks.
HealthCheckTimeout = 2 * time.Second
// hookTimeout bounds a single hook->worker request.
hookTimeout = 10 * time.Second
// StartupTimeout is the timeout for worker startup.
StartupTimeout = 10 * time.Second
@@ -52,26 +53,6 @@ var (
// circuitBreakerMu protects lastStartupFailure.
circuitBreakerMu sync.Mutex
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.
@@ -110,22 +91,21 @@ func GetWorkerPort() int {
// Parses the JSON health response to check the "ready" field when available.
// Falls back to HTTP status code check for backwards compatibility.
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 {
return false
}
defer func() { _ = resp.Body.Close() }()
// Try to parse JSON response for structured health check
var health struct {
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
}
// 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.
@@ -396,18 +376,17 @@ func updateCacheFromPort(port int) {
// GetWorkerVersion gets the version of the running worker.
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 {
return ""
}
defer func() { _ = resp.Body.Close() }()
if resp.StatusCode != http.StatusOK {
if resp.Status != 200 {
return ""
}
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 ""
}
@@ -510,58 +489,46 @@ func findWorkerBinary() string {
return ""
}
// POST sends a POST request to the worker.
func POST(port int, path string, body interface{}) (map[string]interface{}, error) {
jsonBody, err := json.Marshal(body)
// request performs one worker call and returns the decoded JSON object.
// A non-JSON body yields (nil, nil) when lenient, else the decode error.
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 {
return nil, err
}
resp, err := hookClient.Post(
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)
if resp.Status >= 400 {
return nil, fmt.Errorf("request failed: %s", resp.StatusText)
}
var result map[string]interface{}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
// Not all endpoints return JSON
return nil, nil
if err := json.Unmarshal(resp.Body, &result); err != nil {
if lenient {
// Not all endpoints return JSON
return nil, nil
}
return nil, err
}
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.
// 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 {
jsonBody, err := json.Marshal(body)
if err != nil {
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
_, err := request(ctx, "POST", port, path, body, true)
return err
}
// 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.
// 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) {
jsonBody, err := json.Marshal(body)
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
return request(ctx, "POST", port, path, body, true)
}
// GET sends a GET request to the worker.
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))
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
return request(context.Background(), "GET", port, path, nil, false)
}
// 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
// deadline instead of blocking for the full client timeout.
func GETWithContext(ctx context.Context, port int, path string) (map[string]interface{}, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet,
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
return request(ctx, "GET", port, path, nil, false)
}
// versionsCompatible checks if two versions are compatible for dev builds.
+98 -15
View File
@@ -5,8 +5,12 @@ import (
"context"
"encoding/json"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
"time"
@@ -1481,22 +1485,101 @@ func TestSleepCtx_Cancelled(t *testing.T) {
assert.Less(t, elapsed, 500*time.Millisecond)
}
// TestHookClients_DisableKeepAlives asserts shared clients disable keep-alives
// to prevent TIME_WAIT connection leaks in short-lived hook processes.
func TestHookClients_DisableKeepAlives(t *testing.T) {
hTransport, ok := hookClient.Transport.(*http.Transport)
require.True(t, ok, "hookClient.Transport should be *http.Transport")
assert.True(t, hTransport.DisableKeepAlives, "hookClient must disable keep-alives")
assert.Equal(t, 1, hTransport.MaxIdleConns)
// TestHTTPDo_Behaviour covers the raw HTTP/1.0 client against a real server.
func TestHTTPDo_Behaviour(t *testing.T) {
big := strings.Repeat("x", 200_000)
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/echo":
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)
require.True(t, ok, "healthClient.Transport should be *http.Transport")
assert.True(t, hcTransport.DisableKeepAlives, "healthClient must disable keep-alives")
assert.Equal(t, 1, hcTransport.MaxIdleConns)
tests := []struct {
name string
method string
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.
func TestHookClient_Timeout(t *testing.T) {
assert.Equal(t, 10*time.Second, hookClient.Timeout)
assert.Equal(t, HealthCheckTimeout, healthClient.Timeout)
// TestHTTPDo_Failures covers connection, timeout, cancel and malformed replies.
func TestHTTPDo_Failures(t *testing.T) {
t.Run("connection refused", func(t *testing.T) {
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)
}
})
}