crucible depot: Increment 1 core (status/push/verify/get/pull + local/cdn/bunny backends) #30
@@ -11,6 +11,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewBackend returns the backend for name ("local"|"cdn"|"bunny").
|
// NewBackend returns the backend for name ("local"|"cdn"|"bunny").
|
||||||
@@ -85,9 +86,26 @@ func (b *httpBackend) cdnURL(sha string) string {
|
|||||||
return fmt.Sprintf("%s/%s", b.cfg.CDNBase, BlobKey(sha))
|
return fmt.Sprintf("%s/%s", b.cfg.CDNBase, BlobKey(sha))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// probeRetrySleep is the backoff sleeper for transient probe retries;
|
||||||
|
// tests stub it (same pattern as sweep.go's confirmSleep).
|
||||||
|
var probeRetrySleep = time.Sleep
|
||||||
|
|
||||||
// rangeProbe issues GET <url> with Range: bytes=0-0 and classifies the
|
// rangeProbe issues GET <url> with Range: bytes=0-0 and classifies the
|
||||||
// response. Never HEAD (banned by spec).
|
// response. Never HEAD (banned by spec). Transient outcomes (transport
|
||||||
|
// error, or a status that is neither 2xx nor 404/410) retry up to 2 more
|
||||||
|
// times with linear backoff (1s, 2s) — the CDN edge throttles sustained
|
||||||
|
// sweeps; the bash this ports absorbed that with curl --retry 2.
|
||||||
func (b *httpBackend) rangeProbe(ctx context.Context, url string, headers map[string]string) (ProbeState, bool, error) {
|
func (b *httpBackend) rangeProbe(ctx context.Context, url string, headers map[string]string) (ProbeState, bool, error) {
|
||||||
|
for attempt := 0; ; attempt++ {
|
||||||
|
state, transient, err := b.rangeProbeOnce(ctx, url, headers)
|
||||||
|
if !transient || attempt >= 2 {
|
||||||
|
return state, transient, err
|
||||||
|
}
|
||||||
|
probeRetrySleep(time.Duration(attempt+1) * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *httpBackend) rangeProbeOnce(ctx context.Context, url string, headers map[string]string) (ProbeState, bool, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Unconfirmed, true, err
|
return Unconfirmed, true, err
|
||||||
@@ -212,6 +230,7 @@ func (b *httpBackend) fetch(ctx context.Context, sha string) (*http.Response, er
|
|||||||
if resp, err := b.get(ctx, b.cdnURL(sha), nil); err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
if resp, err := b.get(ctx, b.cdnURL(sha), nil); err == nil && resp.StatusCode >= 200 && resp.StatusCode < 300 {
|
||||||
return resp, nil
|
return resp, nil
|
||||||
} else if err == nil {
|
} else if err == nil {
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
}
|
}
|
||||||
resp, err := b.get(ctx, b.storageURL(sha), map[string]string{"AccessKey": b.cfg.ReadKey})
|
resp, err := b.get(ctx, b.storageURL(sha), map[string]string{"AccessKey": b.cfg.ReadKey})
|
||||||
@@ -219,6 +238,7 @@ func (b *httpBackend) fetch(ctx context.Context, sha string) (*http.Response, er
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
return nil, fmt.Errorf("bunny get %s: unexpected status %d", sha, resp.StatusCode)
|
return nil, fmt.Errorf("bunny get %s: unexpected status %d", sha, resp.StatusCode)
|
||||||
}
|
}
|
||||||
@@ -230,6 +250,7 @@ func (b *httpBackend) fetch(ctx context.Context, sha string) (*http.Response, er
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
return nil, fmt.Errorf("cdn get %s: unexpected status %d", sha, resp.StatusCode)
|
return nil, fmt.Errorf("cdn get %s: unexpected status %d", sha, resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,6 +120,81 @@ func hostPort(srv *httptest.Server) string {
|
|||||||
return srv.URL
|
return srv.URL
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// stubProbeSleep replaces probeRetrySleep for the test, recording durations.
|
||||||
|
func stubProbeSleep(t *testing.T) *[]time.Duration {
|
||||||
|
t.Helper()
|
||||||
|
var slept []time.Duration
|
||||||
|
old := probeRetrySleep
|
||||||
|
probeRetrySleep = func(d time.Duration) { slept = append(slept, d) }
|
||||||
|
t.Cleanup(func() { probeRetrySleep = old })
|
||||||
|
return &slept
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProbeRetriesTransientThenPresent(t *testing.T) {
|
||||||
|
slept := stubProbeSleep(t)
|
||||||
|
sha := shaOf("throttled-blob")
|
||||||
|
|
||||||
|
var calls int
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
calls++
|
||||||
|
if calls == 1 {
|
||||||
|
w.WriteHeader(428)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w.WriteHeader(206)
|
||||||
|
}))
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
cfg := testCfg(hostPort(srv), "readkey", "writekey")
|
||||||
|
b, err := NewBackend("bunny", "", cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBackend: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state, transient, err := b.Probe(context.Background(), sha)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Probe: %v", err)
|
||||||
|
}
|
||||||
|
if state != Present || transient {
|
||||||
|
t.Fatalf("expected Present/non-transient after retry, got %v/%v", state, transient)
|
||||||
|
}
|
||||||
|
if calls != 2 {
|
||||||
|
t.Fatalf("expected 2 requests (428 then 206), got %d", calls)
|
||||||
|
}
|
||||||
|
if len(*slept) != 1 || (*slept)[0] != time.Second {
|
||||||
|
t.Fatalf("expected one 1s backoff, got %v", *slept)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProbe404NoRetry(t *testing.T) {
|
||||||
|
slept := stubProbeSleep(t)
|
||||||
|
sha := shaOf("gone-blob")
|
||||||
|
|
||||||
|
f := newFakeBunny()
|
||||||
|
srv := httptest.NewServer(f.handler())
|
||||||
|
defer srv.Close()
|
||||||
|
|
||||||
|
cfg := testCfg(hostPort(srv), "readkey", "writekey")
|
||||||
|
b, err := NewBackend("bunny", "", cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewBackend: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
state, transient, err := b.Probe(context.Background(), sha)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Probe: %v", err)
|
||||||
|
}
|
||||||
|
if state != Absent || transient {
|
||||||
|
t.Fatalf("expected Absent/non-transient, got %v/%v", state, transient)
|
||||||
|
}
|
||||||
|
if got := len(f.requestsSnapshot()); got != 1 {
|
||||||
|
t.Fatalf("expected exactly 1 request for 404, got %d", got)
|
||||||
|
}
|
||||||
|
if len(*slept) != 0 {
|
||||||
|
t.Fatalf("expected zero backoffs for 404, got %v", *slept)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestProbeUsesRangeGetNotHead(t *testing.T) {
|
func TestProbeUsesRangeGetNotHead(t *testing.T) {
|
||||||
sha := shaOf("present-blob")
|
sha := shaOf("present-blob")
|
||||||
f := newFakeBunny()
|
f := newFakeBunny()
|
||||||
@@ -157,6 +232,7 @@ func TestProbeUsesRangeGetNotHead(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestProbeStates(t *testing.T) {
|
func TestProbeStates(t *testing.T) {
|
||||||
|
stubProbeSleep(t)
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
status int
|
status int
|
||||||
wantState ProbeState
|
wantState ProbeState
|
||||||
|
|||||||
@@ -105,6 +105,7 @@ func TestStatusExitCodes(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("unconfirmed-only", func(t *testing.T) {
|
t.Run("unconfirmed-only", func(t *testing.T) {
|
||||||
|
stubProbeSleep(t)
|
||||||
f := newFakeBunny()
|
f := newFakeBunny()
|
||||||
sha := shaOf("flaky-blob")
|
sha := shaOf("flaky-blob")
|
||||||
f.statusFor[sha] = 428
|
f.statusFor[sha] = 428
|
||||||
|
|||||||
Reference in New Issue
Block a user