Skip to content

Commit 4713bad

Browse files
fix(serverless-init): attach per-instance tag to MicroVM enhanced usage metric (#53230)
Codex flagged this on PR #53093 ([review comment](#53093 (comment))): MicroVM's enhanced usage metric (`aws.lambda.microvm.instance`) is emitted with the same static tag set for every MicroVM booted from a given image. `MicroVM.GetEnhancedMetricTags` computes the `Usage` tag set once at startup — before the MicroVM's instance ID is known, since the platform only reveals it via the `/run` lifecycle hook — and nothing ever updates it afterward. Under normal autoscaling, with multiple concurrent MicroVMs running from the same image, their periodic usage samples are indistinguishable from one another: instance-level usage collapses into a single series. The obvious fix — mutate the frozen tag set on `ServerlessMetricAgent` once `/run` reveals the instance ID, mirroring the existing `LogsTagSetter`/`TraceTagSetter` pattern — turned out to require a lock/atomic on `ServerlessMetricAgent`. That struct is passed **by value** into `CloudService.Shutdown` across every cloud service (`AppService`, `CloudRun`, `CloudRunJobs`, `ContainerApp`, `LocalService`, `MicroVM`), so adding any lock-bearing field trips `go vet`'s copylocks check everywhere, not just for MicroVM. Instead, this attaches the tag at the point of *emission* rather than mutating shared state: - `lifecycle.Server` already tracks the instance ID race-free (`instanceID *atomic.String`, captured in `handleRun`). Added `InstanceID()`, a nil-safe accessor, so callers outside the `lifecycle` package can read it. - `MicroVM.CurrentUsageMetricTags()` turns that into an `"instance:<id>"` tag, or `nil` before `/run` has fired. - `enhancedmetrics.Collector` gained an optional `usageMetricTagsFunc`, invoked on every collection tick and passed straight through the existing `AddEnhancedUsageMetric(..., extraTags ...string)` parameter — no new shared mutable state needed. - `main.go` duck-types `cloudService` against a local `usageMetricTagProvider` interface to wire this hook. Every other cloud service doesn't implement it, so `usageMetricTagsFunc` stays `nil` for them and their usage metrics are unaffected. This keeps the change scoped to MicroVM plus small, nil-safe, additive plumbing in the shared collector — no other cloud service, `ServerlessMetricAgent`, or the `Shutdown` interface needed to change. - [x] `dda inv test --targets=./cmd/serverless-init/...` — all 281 tests pass (12 new: `InstanceID()` nil/before/after `/run`, `CurrentUsageMetricTags()` nil-server/before-run/after-run end-to-end, collector dynamic-tag forwarding + nil-func regression guard, `NewCollector` wiring, and the `usageMetricTagProvider` type-assertion pinning `*MicroVM` in vs. every other cloud service out) - [x] `gofmt -l` clean on all changed files - [x] `dda inv linter.go --targets=./cmd/serverless-init/...` — 0 issues - [ ] `cmd/serverless-init/enhanced-metrics/collector.go`/`collector_test.go` carry a pre-existing `//go:build linux` tag; this session ran on macOS without a Linux cross-toolchain, so those specific new tests are syntax-checked (`gofmt -e`) and reviewed but not yet executed — first real run will be in Linux CI 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 4360a98 commit 4713bad

11 files changed

Lines changed: 456 additions & 93 deletions

File tree

cmd/serverless-init/cloudservice/microvm.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,19 @@ func (m *MicroVM) GetMetricPrefix() string { return MicroVMPrefix }
115115
// GetUsageMetricSuffix returns the usage metric suffix.
116116
func (m *MicroVM) GetUsageMetricSuffix() string { return MicroVMUsageMetricSuffix }
117117

118+
// CurrentUsageMetricTags returns the dynamic tags to attach to the enhanced
119+
// usage metric on each periodic emission: the per-instance tag, once known
120+
// from /run, or nil before that (see GetEnhancedMetricTags's doc comment on
121+
// why Usage never carries it directly). "instance" matches the tag key
122+
// AppService.GetEnhancedMetricTags and CloudRun.GetEnhancedMetricTags use for
123+
// their own per-instance usage tag.
124+
func (m *MicroVM) CurrentUsageMetricTags() []string {
125+
if id := m.server.InstanceID(); id != "" {
126+
return []string{"instance:" + id}
127+
}
128+
return nil
129+
}
130+
118131
// GetOrigin returns the origin tag value.
119132
func (m *MicroVM) GetOrigin() string { return MicroVMOrigin }
120133

cmd/serverless-init/cloudservice/microvm_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,80 @@ func TestMicroVMGetEnhancedMetricTagsMissingARN(t *testing.T) {
128128
assert.Equal(t, result.Base["resource_id"], result.Usage["resource_id"])
129129
}
130130

131+
// TestMicroVM_CurrentUsageMetricTags_NilServer_ReturnsNil verifies that
132+
// CurrentUsageMetricTags is safe to call before Init (m.server is nil) — the
133+
// enhanced-metrics collector may call it before the lifecycle server exists.
134+
func TestMicroVM_CurrentUsageMetricTags_NilServer_ReturnsNil(t *testing.T) {
135+
m := &MicroVM{}
136+
assert.Nil(t, m.CurrentUsageMetricTags())
137+
}
138+
139+
// TestMicroVM_CurrentUsageMetricTags_BeforeRun_ReturnsNil verifies that no
140+
// instance tag is produced before /run fires, matching GetEnhancedMetricTags'
141+
// documented behavior that instance_id is unknown until then.
142+
func TestMicroVM_CurrentUsageMetricTags_BeforeRun_ReturnsNil(t *testing.T) {
143+
metricAgent := &serverlessMetrics.ServerlessMetricAgent{}
144+
srv := lifecycle.NewServer(
145+
0,
146+
metricAgent, &noopTraceAgent{}, &noopLogsFlusher{},
147+
metricAgent, nil,
148+
(&MicroVM{}).GetSource(),
149+
time.Second,
150+
lifecycle.NewNoopChildHandle(),
151+
nil, // no forwarder
152+
nil, // no heartbeat
153+
)
154+
m := &MicroVM{server: srv}
155+
assert.Nil(t, m.CurrentUsageMetricTags())
156+
}
157+
158+
// TestMicroVM_CurrentUsageMetricTags_AfterRun_ReturnsInstanceTag verifies the
159+
// end-to-end path: once /run has captured the MicroVM instance ID,
160+
// CurrentUsageMetricTags returns the "instance:<id>" tag the enhanced-metrics
161+
// collector attaches to the usage metric on every subsequent tick.
162+
func TestMicroVM_CurrentUsageMetricTags_AfterRun_ReturnsInstanceTag(t *testing.T) {
163+
metricAgent := &serverlessMetrics.ServerlessMetricAgent{}
164+
srv := lifecycle.NewServer(
165+
0,
166+
metricAgent, &noopTraceAgent{}, &noopLogsFlusher{},
167+
metricAgent, nil,
168+
(&MicroVM{}).GetSource(),
169+
time.Second,
170+
lifecycle.NewNoopChildHandle(),
171+
nil, // no forwarder
172+
nil, // no heartbeat
173+
)
174+
require.NoError(t, srv.ListenAndServe(nil))
175+
t.Cleanup(func() {
176+
shutCtx, cancel := context.WithTimeout(context.Background(), time.Second)
177+
defer cancel()
178+
_ = srv.Stop(shutCtx)
179+
})
180+
181+
port := srv.Addr().(*net.TCPAddr).Port
182+
runPath := "/aws/lambda-microvms/runtime/v1/run"
183+
body := strings.NewReader(`{"microvmId":"vm-abc123"}`)
184+
resp, err := http.Post("http://127.0.0.1:"+strconv.Itoa(port)+runPath, "application/json", body)
185+
require.NoError(t, err)
186+
require.NoError(t, resp.Body.Close())
187+
188+
m := &MicroVM{server: srv}
189+
assert.Equal(t, []string{"instance:vm-abc123"}, m.CurrentUsageMetricTags())
190+
}
191+
192+
// TestMicroVM_SatisfiesUsageMetricTagProvider is a compile-time guard: main.go
193+
// duck-types cloudService against an unexported usageMetricTagProvider
194+
// interface with this exact method set to wire the enhanced-metrics
195+
// collector's dynamic tag hook. If CurrentUsageMetricTags' signature ever
196+
// drifts, that type assertion silently stops matching instead of failing to
197+
// compile — this pins the method set so such drift shows up here instead.
198+
func TestMicroVM_SatisfiesUsageMetricTagProvider(t *testing.T) {
199+
var m any = &MicroVM{}
200+
provider, ok := m.(interface{ CurrentUsageMetricTags() []string })
201+
require.True(t, ok, "*MicroVM must implement CurrentUsageMetricTags() []string")
202+
assert.NotPanics(t, func() { provider.CurrentUsageMetricTags() })
203+
}
204+
131205
// Compile-time guard: *MicroVM must satisfy the CloudService interface,
132206
// including the new Run method.
133207
var _ CloudService = (*MicroVM)(nil)

cmd/serverless-init/enhanced-metrics/collector.go

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -78,10 +78,15 @@ type Collector struct {
7878
usageMetricSuffix string
7979
// Previous stats for rate calculation
8080
previousRateStats ServerlessRateStats
81+
// usageMetricTagsFunc, when non-nil, is called on every collection tick to
82+
// obtain extra tags for the enhanced usage metric — e.g. MicroVM's
83+
// per-instance tag, which is only known once the /run lifecycle hook
84+
// fires. nil for every cloud service that has no such dynamic tag.
85+
usageMetricTagsFunc func() []string
8186
}
8287

83-
// NewCollector creates a new Collector
84-
func NewCollector(metricAgent EnhancedMetricSender, metricSource metrics.MetricSource, metricPrefix string, usageMetricSuffix string, collectionInterval time.Duration) (*Collector, error) {
88+
// NewCollector creates a new Collector. usageMetricTagsFunc may be nil.
89+
func NewCollector(metricAgent EnhancedMetricSender, metricSource metrics.MetricSource, metricPrefix string, usageMetricSuffix string, collectionInterval time.Duration, usageMetricTagsFunc func() []string) (*Collector, error) {
8590
if metricAgent == nil || reflect.ValueOf(metricAgent).IsNil() {
8691
return nil, errors.New("metricAgent cannot be nil")
8792
}
@@ -92,13 +97,14 @@ func NewCollector(metricAgent EnhancedMetricSender, metricSource metrics.MetricS
9297
}
9398

9499
return &Collector{
95-
metricAgent: metricAgent,
96-
metricSource: metricSource,
97-
cgroupReader: cgroupReader,
98-
collectionInterval: collectionInterval,
99-
metricPrefix: metricPrefix + "enhanced.",
100-
usageMetricSuffix: usageMetricSuffix,
101-
previousRateStats: NullServerlessRateStats,
100+
metricAgent: metricAgent,
101+
metricSource: metricSource,
102+
cgroupReader: cgroupReader,
103+
collectionInterval: collectionInterval,
104+
metricPrefix: metricPrefix + "enhanced.",
105+
usageMetricSuffix: usageMetricSuffix,
106+
previousRateStats: NullServerlessRateStats,
107+
usageMetricTagsFunc: usageMetricTagsFunc,
102108
}, nil
103109
}
104110

@@ -151,7 +157,11 @@ func (c *Collector) collect() {
151157

152158
// Always send the usage metric, regardless of cgroup collection success.
153159
if c.usageMetricSuffix != "" {
154-
c.metricAgent.AddEnhancedUsageMetric(c.metricPrefix+c.usageMetricSuffix, 1, c.metricSource, timestamp)
160+
var extraTags []string
161+
if c.usageMetricTagsFunc != nil {
162+
extraTags = c.usageMetricTagsFunc()
163+
}
164+
c.metricAgent.AddEnhancedUsageMetric(c.metricPrefix+c.usageMetricSuffix, 1, c.metricSource, timestamp, extraTags...)
155165
}
156166

157167
if err := c.cgroupReader.RefreshCgroups(0); err != nil {

cmd/serverless-init/enhanced-metrics/collector_test.go

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,14 +310,93 @@ func TestCollectorSendsUsageMetricOnCgroupFailure(t *testing.T) {
310310

311311
func TestNewCollectorNilMetricAgent(t *testing.T) {
312312
// Untyped nil interface.
313-
c, err := NewCollector(nil, metrics.MetricSourceGoogleCloudRunEnhanced, "gcp.run.", "instance", time.Second)
313+
c, err := NewCollector(nil, metrics.MetricSourceGoogleCloudRunEnhanced, "gcp.run.", "instance", time.Second, nil)
314314
assert.Nil(t, c)
315315
assert.Error(t, err)
316316

317317
// Typed nil implementing EnhancedMetricSender, which is what main.go passes
318318
// when metricAgent is a nil *ServerlessMetricAgent (use_dogstatsd disabled).
319319
var typedNil *mockEnhancedMetricSender
320-
c, err = NewCollector(typedNil, metrics.MetricSourceGoogleCloudRunEnhanced, "gcp.run.", "instance", time.Second)
320+
c, err = NewCollector(typedNil, metrics.MetricSourceGoogleCloudRunEnhanced, "gcp.run.", "instance", time.Second, nil)
321321
assert.Nil(t, c)
322322
assert.Error(t, err)
323323
}
324+
325+
// TestCollectorUsageMetricIncludesDynamicTags verifies that when
326+
// usageMetricTagsFunc is set (MicroVM's use case: attaching the per-instance
327+
// tag once known from /run), its return value is forwarded as extraTags on
328+
// every AddEnhancedUsageMetric call.
329+
func TestCollectorUsageMetricIncludesDynamicTags(t *testing.T) {
330+
mockAgent := new(mockEnhancedMetricSender)
331+
mockAgent.On("AddEnhancedUsageMetric", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return()
332+
333+
mockReader := &mockCgroupReader{refreshErr: errors.New("cgroup failure"), version: 1}
334+
335+
c := &Collector{
336+
metricAgent: mockAgent,
337+
metricSource: metrics.MetricSourceAWSMicroVMEnhanced,
338+
cgroupReader: mockReader,
339+
metricPrefix: "aws.lambda.microvm.enhanced.",
340+
usageMetricSuffix: "instance",
341+
previousRateStats: NullServerlessRateStats,
342+
usageMetricTagsFunc: func() []string { return []string{"instance:vm-abc123"} },
343+
}
344+
345+
c.collect()
346+
347+
mockAgent.AssertCalled(t, "AddEnhancedUsageMetric",
348+
"aws.lambda.microvm.enhanced.instance", float64(1),
349+
metrics.MetricSourceAWSMicroVMEnhanced, mock.Anything, []string{"instance:vm-abc123"})
350+
}
351+
352+
// TestCollectorUsageMetricNilTagsFuncSendsNoExtraTags verifies that when
353+
// usageMetricTagsFunc is nil (every cloud service except MicroVM), no extra
354+
// tags are added to the usage metric — pinning today's behavior.
355+
func TestCollectorUsageMetricNilTagsFuncSendsNoExtraTags(t *testing.T) {
356+
mockAgent := new(mockEnhancedMetricSender)
357+
mockAgent.On("AddEnhancedUsageMetric", mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).Return()
358+
359+
mockReader := &mockCgroupReader{refreshErr: errors.New("cgroup failure"), version: 1}
360+
361+
c := &Collector{
362+
metricAgent: mockAgent,
363+
metricSource: metrics.MetricSourceGoogleCloudRunEnhanced,
364+
cgroupReader: mockReader,
365+
metricPrefix: "gcp.run.container.enhanced.",
366+
usageMetricSuffix: "instance",
367+
previousRateStats: NullServerlessRateStats,
368+
}
369+
370+
c.collect()
371+
372+
mockAgent.AssertCalled(t, "AddEnhancedUsageMetric",
373+
"gcp.run.container.enhanced.instance", float64(1),
374+
metrics.MetricSourceGoogleCloudRunEnhanced, mock.Anything, []string(nil))
375+
}
376+
377+
// TestNewCollectorWiresUsageMetricTagsFunc verifies that NewCollector stores
378+
// the provided usageMetricTagsFunc on the returned Collector so collect()
379+
// picks it up on every tick.
380+
func TestNewCollectorWiresUsageMetricTagsFunc(t *testing.T) {
381+
mockAgent := new(mockEnhancedMetricSender)
382+
tagsFunc := func() []string { return []string{"instance:vm-abc123"} }
383+
384+
c, err := NewCollector(mockAgent, metrics.MetricSourceAWSMicroVMEnhanced, "aws.lambda.microvm.", "instance", time.Second, tagsFunc)
385+
386+
assert.NoError(t, err)
387+
if assert.NotNil(t, c.usageMetricTagsFunc) {
388+
assert.Equal(t, []string{"instance:vm-abc123"}, c.usageMetricTagsFunc())
389+
}
390+
}
391+
392+
// TestNewCollectorNilUsageMetricTagsFuncIsAccepted verifies that
393+
// NewCollector accepts a nil usageMetricTagsFunc — the case for every cloud
394+
// service except MicroVM.
395+
func TestNewCollectorNilUsageMetricTagsFuncIsAccepted(t *testing.T) {
396+
mockAgent := new(mockEnhancedMetricSender)
397+
398+
c, err := NewCollector(mockAgent, metrics.MetricSourceGoogleCloudRunEnhanced, "gcp.run.container.", "instance", time.Second, nil)
399+
400+
assert.NoError(t, err)
401+
assert.Nil(t, c.usageMetricTagsFunc)
402+
}

cmd/serverless-init/enhanced-metrics/collector_unsupported.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ type EnhancedMetricSender interface{}
2020

2121
type Collector struct{}
2222

23-
func NewCollector(_ EnhancedMetricSender, _ metrics.MetricSource, _ string, _ string, _ time.Duration) (*Collector, error) {
23+
func NewCollector(_ EnhancedMetricSender, _ metrics.MetricSource, _ string, _ string, _ time.Duration, _ func() []string) (*Collector, error) {
2424
return nil, errors.New("Collector is only supported on Linux")
2525
}
2626

cmd/serverless-init/lifecycle/forwarder.go

Lines changed: 48 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,34 @@ import (
2727
const defaultMaxResponseBodyBytes int64 = 1 << 20
2828

2929
const (
30+
// Per AWS, Run/Resume/Suspend/Terminate: 1 second
31+
// Ready/Validate: 30 seconds
3032
defaultForwardTimeout = 1 * time.Second
31-
defaultReadyTimeout = 60 * time.Second
32-
defaultValidateTimeout = 1 * time.Second
33+
defaultReadyTimeout = 30 * time.Second
34+
defaultValidateTimeout = 30 * time.Second
3335
)
3436

37+
// dialCheckTimeout bounds the single TCP dial attempt PassThroughWaiting uses
38+
// to check user-app reachability for /ready and /validate. Per the hook
39+
// contract, the platform — not the hook — owns the retry loop for these two
40+
// hooks, so the check must answer fast rather than block until the app is up.
41+
// A refused loopback connection fails in microseconds (the kernel sends RST
42+
// immediately), so this timeout only matters for a slow/hung connect (e.g.
43+
// host scheduling jitter on an oversubscribed MicroVM host). 200ms is
44+
// generous enough to absorb that jitter while staying well inside the "answer
45+
// fast" contract, and costs at most one extra platform retry (a 503) on the
46+
// rare call where it's hit.
47+
const dialCheckTimeout = 200 * time.Millisecond
48+
3549
// Forwarder POSTs lifecycle hooks to the user app. It is constructed only
3650
// when DD_AWS_MICROVM_USER_APP_PORT is set and we're in init-container
3751
// mode + MicroVM origin.
3852
type Forwarder struct {
3953
target string // e.g. "http://127.0.0.1:8080"
4054
client *http.Client // shared; no client-level Timeout (per-call deadlines via ctx)
4155
forwardTimeout time.Duration // default 1s, used for suspend/terminate/run/resume
42-
readyTimeout time.Duration // default 60s, used for /ready
43-
validateTimeout time.Duration // default 1s, used for /validate
56+
readyTimeout time.Duration // default 30s, used for /ready
57+
validateTimeout time.Duration // default 30s, used for /validate
4458
maxResponseBodyBytes int64 // default defaultMaxResponseBodyBytes; cap on user-app body surfaced to platform
4559
}
4660

@@ -83,63 +97,58 @@ func NewForwarder(port int, forwardTimeout, readyTimeout, validateTimeout time.D
8397
}
8498
}
8599

86-
// PassThroughWaiting waits for the user app to accept TCP connections bounded
87-
// by timeout, then forwards the request and mirrors the response. Used for:
100+
// PassThroughWaiting checks user-app reachability with a single fast TCP dial
101+
// (bounded by dialCheckTimeout), then forwards the request bounded by timeout
102+
// and mirrors the response. Used for:
88103
// - /ready ("I am booted and ready to be snapshotted"): the platform
89-
// retries on non-200, so the TCP wait absorbs the startup race.
104+
// retries on non-200 until its own configured timeout, so the hook
105+
// answers fast rather than blocking on the startup race.
90106
// - /validate ("I was resumed from a snapshot and everything is good"): the
91-
// TCP wait handles a crash-then-restart between resume and this
92-
// call.
107+
// same fast-answer contract applies to the crash-then-restart window
108+
// between resume and this call.
93109
//
94-
// Body is buffered before the TCP wait so the bytes survive waitForUserApp.
95-
// Deadline exceeded maps to 504. Body Close contract documented on PassThrough.
110+
// Per the /ready and /validate hook contract, only 200 and 503 are meaningful
111+
// responses — the platform retries on 503 until its own configured timeout,
112+
// while any other non-200 (including 504) fails the build. So unlike
113+
// PassThrough, an unreachable app or a deadline exceeded here always maps to
114+
// 503, never 504. Body Close contract documented on PassThrough.
96115
func (f *Forwarder) PassThroughWaiting(timeout time.Duration, path string, headers http.Header, body io.Reader) *http.Response {
97116
var bodyBytes []byte
98117
if body != nil {
99118
var err error
100-
// Read the full inbound body before the TCP wait. A read error is a
101-
// server-side failure (network, OS, memory) — not a client mistake —
102-
// so return 500 rather than 400. We still forward nothing to the user
103-
// app to avoid passing a partial body (which could make it answer
104-
// /validate "healthy" off incomplete data).
119+
// Read the full inbound body before the reachability check. A read
120+
// error is a server-side failure (network, OS, memory) — not a client
121+
// mistake — so return 500 rather than 400. We still forward nothing to
122+
// the user app to avoid passing a partial body (which could make it
123+
// answer /validate "healthy" off incomplete data).
105124
if bodyBytes, err = io.ReadAll(body); err != nil {
106125
return statusOnlyResponse(http.StatusInternalServerError)
107126
}
108127
}
109-
ctx, cancel := context.WithTimeout(context.Background(), timeout)
110-
if err := f.waitForUserApp(ctx); err != nil {
111-
cancel()
112-
return statusOnlyResponse(mapErrToStatus(err))
128+
if !f.reachable(dialCheckTimeout) {
129+
return statusOnlyResponse(http.StatusServiceUnavailable)
113130
}
131+
ctx, cancel := context.WithTimeout(context.Background(), timeout)
114132
resp, err := f.do(ctx, path, headers, bytes.NewReader(bodyBytes))
115133
if err != nil {
116134
cancel()
117-
return statusOnlyResponse(mapErrToStatus(err))
135+
return statusOnlyResponse(http.StatusServiceUnavailable)
118136
}
119137
resp.Body = wrapResponseBody(resp.Body, f.maxResponseBodyBytes, cancel)
120138
return resp
121139
}
122140

123-
// waitForUserApp polls the user app's TCP port until a connection succeeds or
124-
// ctx is cancelled. Returns nil when the port is reachable, ctx.Err() when
125-
// the deadline is exceeded. Polls every 50ms.
126-
func (f *Forwarder) waitForUserApp(ctx context.Context) error {
141+
// reachable performs a single TCP dial attempt to the user app, bounded by
142+
// timeout. No polling/retrying: per the hook contract, the platform (not the
143+
// hook) owns the retry loop for /ready and /validate.
144+
func (f *Forwarder) reachable(timeout time.Duration) bool {
127145
addr := strings.TrimPrefix(f.target, "http://")
128-
dialer := &net.Dialer{}
129-
ticker := time.NewTicker(50 * time.Millisecond)
130-
defer ticker.Stop()
131-
for {
132-
conn, err := dialer.DialContext(ctx, "tcp", addr)
133-
if err == nil {
134-
_ = conn.Close()
135-
return nil
136-
}
137-
select {
138-
case <-ctx.Done():
139-
return ctx.Err()
140-
case <-ticker.C:
141-
}
146+
conn, err := net.DialTimeout("tcp", addr, timeout)
147+
if err != nil {
148+
return false
142149
}
150+
_ = conn.Close()
151+
return true
143152
}
144153

145154
// PassThrough forwards a per-MicroVM lifecycle hook (/run, /resume,

0 commit comments

Comments
 (0)