some tweaks to cart
Build and Publish / BuildAndDeployArm64 (push) Failing after 5s
Build and Publish / BuildAndDeployAmd64 (push) Failing after 4s

This commit is contained in:
2026-06-29 20:08:21 +02:00
parent de47ef4f14
commit 98e2fd8d41
7 changed files with 931 additions and 55 deletions
+24 -13
View File
@@ -9,6 +9,7 @@ import (
"io"
"log"
"net/http"
"sync"
"time"
"git.k6n.net/mats/go-cart-actor/pkg/actor"
@@ -27,7 +28,6 @@ type RemoteHost[V any] struct {
host string
httpBase string
conn *grpc.ClientConn
transport *http.Transport
client *http.Client
controlClient messages.ControlPlaneClient
missedPings int
@@ -38,8 +38,27 @@ const name = "proxy"
var (
tracer = otel.Tracer(name)
meter = otel.Meter(name)
// One process-wide HTTP transport pools idle connections to every peer.
// Per-host RemoteHost instances previously each owned a transport with
// MaxIdleConnsPerHost=100, multiplying file handles by replica count.
sharedTransport *http.Transport
sharedTransportOnce sync.Once
)
func sharedHTTPTransport() *http.Transport {
sharedTransportOnce.Do(func() {
sharedTransport = &http.Transport{
MaxIdleConns: 64,
MaxIdleConnsPerHost: 8,
MaxConnsPerHost: 32,
DisableKeepAlives: false,
IdleConnTimeout: 90 * time.Second,
}
})
return sharedTransport
}
// MockResponseWriter implements http.ResponseWriter to capture responses for proxy calls.
type MockResponseWriter struct {
StatusCode int
@@ -88,23 +107,13 @@ func NewRemoteHost[V any](host string, httpPort ...string) (*RemoteHost[V], erro
controlClient := messages.NewControlPlaneClient(conn)
transport := &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 100,
// Bound concurrently-open connections to this peer so a load spike
// can't exhaust file handles. Excess requests wait for a free conn
// rather than opening unbounded sockets.
MaxConnsPerHost: 256,
DisableKeepAlives: false,
IdleConnTimeout: 120 * time.Second,
}
transport := sharedHTTPTransport()
client := &http.Client{Transport: transport, Timeout: 10 * time.Second}
return &RemoteHost[V]{
host: host,
httpBase: fmt.Sprintf("http://%s:%s", host, port),
conn: conn,
transport: transport,
client: client,
controlClient: controlClient,
missedPings: 0,
@@ -136,7 +145,9 @@ func (h *RemoteHost[V]) Name() string {
func (h *RemoteHost[V]) Close() error {
if h.conn != nil {
h.conn.Close()
err := h.conn.Close()
h.conn = nil
return err
}
return nil
}
+109
View File
@@ -0,0 +1,109 @@
package proxy
import (
"io"
"net/http"
"net/http/httptest"
"net/url"
"sync/atomic"
"testing"
)
func TestRemoteHostSharesHTTPTransport(t *testing.T) {
h1, err := NewRemoteHost[struct{}]("10.0.0.1", "8080")
if err != nil {
t.Fatal(err)
}
h2, err := NewRemoteHost[struct{}]("10.0.0.2", "8080")
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = h1.Close()
_ = h2.Close()
})
if h1.client.Transport != h2.client.Transport {
t.Fatal("expected all RemoteHost instances to share one http.Transport")
}
}
func TestRemoteHostProxyUsesKeepAlive(t *testing.T) {
var requests atomic.Int32
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
requests.Add(1)
w.WriteHeader(http.StatusOK)
_, _ = io.WriteString(w, `{"ok":true}`)
}))
t.Cleanup(srv.Close)
host, port, err := splitHostPort(srv.URL)
if err != nil {
t.Fatal(err)
}
remote, err := NewRemoteHost[struct{}](host, port)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = remote.Close() })
for range 3 {
mw := NewMockResponseWriter()
req, err := http.NewRequest(http.MethodGet, "/cart/1", nil)
if err != nil {
t.Fatal(err)
}
ok, err := remote.Proxy(1, mw, req, nil)
if err != nil || !ok {
t.Fatalf("Proxy failed: ok=%v err=%v status=%d body=%q", ok, err, mw.StatusCode, mw.Body.String())
}
}
if requests.Load() != 3 {
t.Fatalf("server requests = %d, want 3", requests.Load())
}
}
func TestRemoteHostCloseDoesNotBreakSharedTransport(t *testing.T) {
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
w.WriteHeader(http.StatusOK)
}))
t.Cleanup(srv.Close)
host, port, err := splitHostPort(srv.URL)
if err != nil {
t.Fatal(err)
}
h1, err := NewRemoteHost[struct{}](host, port)
if err != nil {
t.Fatal(err)
}
h2, err := NewRemoteHost[struct{}](host, port)
if err != nil {
t.Fatal(err)
}
if err := h1.Close(); err != nil {
t.Fatal(err)
}
mw := NewMockResponseWriter()
req, err := http.NewRequest(http.MethodGet, "/health", nil)
if err != nil {
t.Fatal(err)
}
ok, err := h2.Proxy(0, mw, req, nil)
if err != nil || !ok || mw.StatusCode != http.StatusOK {
t.Fatalf("second host proxy after first close failed: ok=%v err=%v status=%d", ok, err, mw.StatusCode)
}
}
func splitHostPort(rawURL string) (host, port string, err error) {
u, err := url.Parse(rawURL)
if err != nil {
return "", "", err
}
return u.Hostname(), u.Port(), nil
}