Skip to content

Commit 616817c

Browse files
committed
Drain HTTP responses before scale-to-zero
1 parent df9a38a commit 616817c

9 files changed

Lines changed: 1988 additions & 79 deletions

File tree

server/cmd/api/main.go

Lines changed: 18 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"encoding/json"
66
"fmt"
77
"log/slog"
8+
"net"
89
"net/http"
910
"net/url"
1011
"os"
@@ -377,6 +378,7 @@ func main() {
377378
metrics.NewChromeCollector(upstreamMgr),
378379
metrics.NewGPUCollector(),
379380
metrics.NewSystemCollector(),
381+
metrics.NewResponseDrainCollector(scaletozero.ResponseDrainOutcomeCounts, scaletozero.ActiveResponseHolds, scaletozero.FailClosedResponseHolds),
380382
}
381383
if otlpMetrics != nil {
382384
metricsCollectors = append(metricsCollectors, metrics.NewOTLPCollector(otlpMetrics))
@@ -387,29 +389,22 @@ func main() {
387389
Handler: rMetrics,
388390
}
389391

390-
go func() {
391-
slogger.Info("http server starting", "addr", srv.Addr)
392-
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
393-
slogger.Error("http server failed", "err", err)
394-
stop()
395-
}
396-
}()
397-
398-
go func() {
399-
slogger.Info("devtools websocket proxy starting", "addr", srvDevtools.Addr)
400-
if err := srvDevtools.ListenAndServe(); err != nil && err != http.ErrServerClosed {
401-
slogger.Error("devtools websocket proxy failed", "err", err)
402-
stop()
403-
}
404-
}()
405-
406-
go func() {
407-
slogger.Info("chromedriver proxy starting", "addr", srvChromeDriver.Addr)
408-
if err := srvChromeDriver.ListenAndServe(); err != nil && err != http.ErrServerClosed {
409-
slogger.Error("chromedriver proxy failed", "err", err)
410-
stop()
411-
}
412-
}()
392+
serveHTTP := func(name string, server *http.Server) {
393+
go func() {
394+
slogger.Info(name+" starting", "addr", server.Addr)
395+
listener, err := net.Listen("tcp", server.Addr)
396+
if err == nil {
397+
err = scaletozero.Serve(server, listener)
398+
}
399+
if err != nil && err != http.ErrServerClosed {
400+
slogger.Error(name+" failed", "err", err)
401+
stop()
402+
}
403+
}()
404+
}
405+
serveHTTP("http server", srv)
406+
serveHTTP("devtools websocket proxy", srvDevtools)
407+
serveHTTP("chromedriver proxy", srvChromeDriver)
413408

414409
go func() {
415410
slogger.Info("metrics server starting", "addr", srvMetrics.Addr)

server/lib/metrics/scaletozero.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
package metrics
2+
3+
import (
4+
"context"
5+
"sort"
6+
)
7+
8+
type ResponseDrainSource func() map[string]uint64
9+
type ResponseDrainGauge func() int64
10+
11+
type ResponseDrainCollector struct {
12+
snapshot ResponseDrainSource
13+
active ResponseDrainGauge
14+
failClosed ResponseDrainGauge
15+
}
16+
17+
func NewResponseDrainCollector(snapshot ResponseDrainSource, active, failClosed ResponseDrainGauge) *ResponseDrainCollector {
18+
return &ResponseDrainCollector{snapshot: snapshot, active: active, failClosed: failClosed}
19+
}
20+
21+
func (c *ResponseDrainCollector) Name() string { return "scale-to-zero response drain" }
22+
23+
func (c *ResponseDrainCollector) Collect(_ context.Context, w *Writer) error {
24+
w.Metric("kernel_scale_to_zero_response_drain_total", "HTTP response drain events before scale-to-zero is re-enabled.", "counter")
25+
counts := c.snapshot()
26+
outcomes := make([]string, 0, len(counts))
27+
for outcome := range counts {
28+
outcomes = append(outcomes, outcome)
29+
}
30+
sort.Strings(outcomes)
31+
for _, outcome := range outcomes {
32+
w.Sample("kernel_scale_to_zero_response_drain_total", []Label{{Name: "outcome", Value: outcome}}, float64(counts[outcome]))
33+
}
34+
35+
w.Metric("kernel_scale_to_zero_response_holds", "HTTP response scale-to-zero holds currently active.", "gauge")
36+
w.Sample("kernel_scale_to_zero_response_holds", nil, float64(c.active()))
37+
w.Metric("kernel_scale_to_zero_response_fail_closed_holds", "HTTP response holds awaiting terminal connection recovery or guest termination.", "gauge")
38+
w.Sample("kernel_scale_to_zero_response_fail_closed_holds", nil, float64(c.failClosed()))
39+
return nil
40+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package metrics
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestResponseDrainCollector(t *testing.T) {
12+
collector := NewResponseDrainCollector(func() map[string]uint64 {
13+
return map[string]uint64{
14+
"timeout": 2,
15+
"drained": 7,
16+
}
17+
}, func() int64 { return 3 }, func() int64 { return 1 })
18+
writer := &Writer{}
19+
20+
require.NoError(t, collector.Collect(context.Background(), writer))
21+
22+
assert.Equal(t, `# HELP kernel_scale_to_zero_response_drain_total HTTP response drain events before scale-to-zero is re-enabled.
23+
# TYPE kernel_scale_to_zero_response_drain_total counter
24+
kernel_scale_to_zero_response_drain_total{outcome="drained"} 7
25+
kernel_scale_to_zero_response_drain_total{outcome="timeout"} 2
26+
# HELP kernel_scale_to_zero_response_holds HTTP response scale-to-zero holds currently active.
27+
# TYPE kernel_scale_to_zero_response_holds gauge
28+
kernel_scale_to_zero_response_holds 3
29+
# HELP kernel_scale_to_zero_response_fail_closed_holds HTTP response holds awaiting terminal connection recovery or guest termination.
30+
# TYPE kernel_scale_to_zero_response_fail_closed_holds gauge
31+
kernel_scale_to_zero_response_fail_closed_holds 1
32+
`, string(writer.Bytes()))
33+
}

0 commit comments

Comments
 (0)