mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-10-26 07:44:59 +03:00
be25314157
The public IPv4/IPv6 lookup was cached for the life of the panel process, so Overview kept showing an old address after the server's external IP changed, and a failed first lookup pinned N/A until a restart. Addresses now refresh every five minutes, a failed IPv4 lookup retries after a minute, failed IPv6 lookups back off 1/2/5/15 minutes, and a temporary failure keeps the last known address. Status sampling still never waits on the lookup.
567 lines
18 KiB
Go
567 lines
18 KiB
Go
package service
|
|
|
|
import (
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"testing/synctest"
|
|
"time"
|
|
|
|
"github.com/mhsanaei/3x-ui/v3/internal/database/dbtest"
|
|
)
|
|
|
|
// A box with no IPv6 route spends 3s per lookup service, and a status sample
|
|
// that waits for that is a panel reporting nothing for the first ~15s.
|
|
func TestStatusSampleDoesNotWaitOnPublicIPLookup(t *testing.T) {
|
|
dbDir := t.TempDir()
|
|
t.Setenv("XUI_DB_FOLDER", dbDir)
|
|
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
|
|
|
|
release := make(chan struct{})
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
select {
|
|
case <-release:
|
|
case <-r.Context().Done():
|
|
return
|
|
}
|
|
_, _ = w.Write([]byte("203.0.113.7"))
|
|
}))
|
|
t.Cleanup(srv.Close)
|
|
|
|
savedV4, savedV6 := publicIPv4Services, publicIPv6Services
|
|
publicIPv4Services = []string{srv.URL}
|
|
publicIPv6Services = []string{srv.URL}
|
|
t.Cleanup(func() { publicIPv4Services, publicIPv6Services = savedV4, savedV6 })
|
|
|
|
svc := &ServerService{}
|
|
status := svc.CurrentStatus()
|
|
if status == nil {
|
|
t.Fatal("CurrentStatus returned nil while the IP lookup was in flight")
|
|
}
|
|
if status.PublicIP.IPv4 != "" {
|
|
t.Fatalf("the sample waited for the lookup: PublicIP.IPv4 = %q, want it still unresolved", status.PublicIP.IPv4)
|
|
}
|
|
|
|
close(release)
|
|
deadline := time.Now().Add(10 * time.Second)
|
|
for {
|
|
if ipv4, _ := svc.publicIPs(); ipv4 == "203.0.113.7" {
|
|
return
|
|
}
|
|
if time.Now().After(deadline) {
|
|
t.Fatal("the background lookup never cached the public IP")
|
|
}
|
|
time.Sleep(20 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
// publicIPTestTransport keeps clock-driven tests entirely off the network.
|
|
type publicIPTestTransport func(*http.Request) (*http.Response, error)
|
|
|
|
func (f publicIPTestTransport) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) }
|
|
|
|
func stubPublicIPServices(t *testing.T, respond publicIPTestTransport) {
|
|
t.Helper()
|
|
oldTransport, old4, old6 := http.DefaultTransport, publicIPv4Services, publicIPv6Services
|
|
http.DefaultTransport = respond
|
|
publicIPv4Services = []string{"https://ipv4.example.test/primary", "https://ipv4.example.test/fallback"}
|
|
publicIPv6Services = []string{"https://ipv6.example.test/primary", "https://ipv6.example.test/fallback"}
|
|
t.Cleanup(func() { http.DefaultTransport, publicIPv4Services, publicIPv6Services = oldTransport, old4, old6 })
|
|
}
|
|
|
|
func publicIPResponse(status int, body string) (*http.Response, error) {
|
|
return &http.Response{StatusCode: status, Header: make(http.Header), Body: io.NopCloser(strings.NewReader(body))}, nil
|
|
}
|
|
|
|
func assertCachedPublicIPs(t *testing.T, svc *ServerService, want4, want6 string) {
|
|
t.Helper()
|
|
got4, got6 := svc.publicIPs()
|
|
if got4 != want4 || got6 != want6 {
|
|
t.Fatalf("cached IPs=(%q,%q), want (%q,%q)", got4, got6, want4, want6)
|
|
}
|
|
}
|
|
|
|
func TestPublicIPRefreshesAfterExpiryWithoutRestart(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls atomic.Int32
|
|
value4, value6 := "192.0.2.10", "2001:db8::10"
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
calls.Add(1)
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(200, value4)
|
|
}
|
|
return publicIPResponse(200, value6)
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
assertCachedPublicIPs(t, svc, value4, value6)
|
|
value4, value6 = "192.0.2.20", "2001:db8::20"
|
|
time.Sleep(5*time.Minute - time.Nanosecond)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
if got := calls.Load(); got != 2 {
|
|
t.Fatalf("lookup before expiry: %d requests", got)
|
|
}
|
|
time.Sleep(time.Nanosecond)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
assertCachedPublicIPs(t, svc, value4, value6)
|
|
if got := calls.Load(); got != 4 {
|
|
t.Fatalf("refresh requests=%d, want 4", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPublicIPFailedRefreshRetainsLastKnownAndRetries(t *testing.T) {
|
|
for _, failure := range []string{"http-error", "empty-body", "transport-error", "timeout"} {
|
|
t.Run(failure, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls atomic.Int32
|
|
unavailable := false
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
calls.Add(1)
|
|
if unavailable {
|
|
switch failure {
|
|
case "empty-body":
|
|
return publicIPResponse(200, "")
|
|
case "transport-error":
|
|
return nil, io.ErrUnexpectedEOF
|
|
case "timeout":
|
|
<-r.Context().Done()
|
|
return nil, r.Context().Err()
|
|
default:
|
|
return publicIPResponse(503, "")
|
|
}
|
|
}
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(200, "192.0.2.10")
|
|
}
|
|
return publicIPResponse(200, "2001:db8::10")
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
unavailable = true
|
|
time.Sleep(5 * time.Minute)
|
|
// Await the refresh, including HTTP timeouts, before checking its result.
|
|
done := svc.resolvePublicIPsInBackground()
|
|
if done == nil {
|
|
t.Fatal("expired cache did not start a refresh")
|
|
}
|
|
<-done
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "2001:db8::10")
|
|
if got := calls.Load(); got != 6 {
|
|
t.Fatalf("failed refresh made %d requests, want 6 including fallbacks", got)
|
|
}
|
|
time.Sleep(time.Minute - time.Nanosecond)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
if got := calls.Load(); got != 6 {
|
|
t.Fatalf("retried too early: %d requests", got)
|
|
}
|
|
unavailable = false
|
|
time.Sleep(time.Nanosecond)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
if got := calls.Load(); got != 8 {
|
|
t.Fatalf("retry requests=%d, want 8", got)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPublicIPInitialFailureRecoversBothFamilies(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
available := false
|
|
var calls atomic.Int32
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
calls.Add(1)
|
|
if !available {
|
|
return publicIPResponse(503, "")
|
|
}
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(200, "192.0.2.20")
|
|
}
|
|
return publicIPResponse(200, "2001:db8::20")
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
assertCachedPublicIPs(t, svc, "N/A", "N/A")
|
|
available = true
|
|
time.Sleep(time.Minute)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
assertCachedPublicIPs(t, svc, "192.0.2.20", "2001:db8::20")
|
|
if got := calls.Load(); got != 6 {
|
|
t.Fatalf("initial failure/recovery requests=%d, want 6", got)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPublicIPFamiliesRefreshIndependently(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls4, calls6 atomic.Int32
|
|
ipv6Available := false
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
calls4.Add(1)
|
|
return publicIPResponse(200, "192.0.2.10")
|
|
}
|
|
calls6.Add(1)
|
|
if !ipv6Available {
|
|
return publicIPResponse(503, "")
|
|
}
|
|
return publicIPResponse(200, "2001:db8::20")
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "N/A")
|
|
ipv6Available = true
|
|
time.Sleep(time.Minute)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "2001:db8::20")
|
|
if calls4.Load() != 1 || calls6.Load() != 3 {
|
|
t.Fatalf("family requests IPv4=%d IPv6=%d", calls4.Load(), calls6.Load())
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPublicIPRefreshIsSingleFlightAndKeepsCachedReadsAvailable(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls atomic.Int32
|
|
slow := false
|
|
release := make(chan struct{})
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
calls.Add(1)
|
|
if slow {
|
|
select {
|
|
case <-release:
|
|
case <-r.Context().Done():
|
|
return nil, r.Context().Err()
|
|
}
|
|
}
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(200, "192.0.2.10")
|
|
}
|
|
return publicIPResponse(200, "2001:db8::10")
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
slow = true
|
|
time.Sleep(5 * time.Minute)
|
|
svc.resolvePublicIPsInBackground()
|
|
synctest.Wait()
|
|
var wg sync.WaitGroup
|
|
var completed atomic.Int32
|
|
for range 20 {
|
|
wg.Go(func() { svc.resolvePublicIPsInBackground(); svc.resolvePublicIPs(); completed.Add(1) })
|
|
}
|
|
synctest.Wait()
|
|
// Cached reads must remain available while the HTTP request is blocked.
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "2001:db8::10")
|
|
count := calls.Load()
|
|
early := completed.Load()
|
|
close(release)
|
|
wg.Wait()
|
|
synctest.Wait()
|
|
if early != 20 {
|
|
t.Fatalf("only %d warm callers returned while the lookup was blocked", early)
|
|
}
|
|
if completed.Load() != 20 {
|
|
t.Fatalf("only %d synchronous callers completed", completed.Load())
|
|
}
|
|
if count != 3 {
|
|
t.Fatalf("concurrent refresh started %d requests, want 3 including initial pair", count)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestPublicIPStatusUsesRefreshedAddressWithoutBlocking(t *testing.T) {
|
|
dbDir := t.TempDir()
|
|
t.Setenv("XUI_DB_FOLDER", dbDir)
|
|
dbtest.InitDB(t, filepath.Join(dbDir, "x-ui.db"))
|
|
savedHistory := systemMetrics
|
|
systemMetrics = newMetricHistory()
|
|
t.Cleanup(func() { systemMetrics = savedHistory })
|
|
synctest.Test(t, func(t *testing.T) {
|
|
value4, value6 := "192.0.2.10", "2001:db8::10"
|
|
slow := false
|
|
release := make(chan struct{})
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
if slow {
|
|
select {
|
|
case <-release:
|
|
case <-r.Context().Done():
|
|
return nil, r.Context().Err()
|
|
}
|
|
}
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(200, value4)
|
|
}
|
|
return publicIPResponse(200, value6)
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
svc.RefreshStatus()
|
|
assertStatus := func(want string) {
|
|
t.Helper()
|
|
data, err := json.Marshal(svc.CurrentStatus())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var wire struct {
|
|
PublicIP struct {
|
|
IPv4 string `json:"ipv4"`
|
|
} `json:"publicIP"`
|
|
}
|
|
if err := json.Unmarshal(data, &wire); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if wire.PublicIP.IPv4 != want {
|
|
t.Fatalf("status JSON IPv4=%q, want %q", wire.PublicIP.IPv4, want)
|
|
}
|
|
}
|
|
assertStatus(value4)
|
|
value4, value6, slow = "192.0.2.20", "2001:db8::20", true
|
|
time.Sleep(5 * time.Minute)
|
|
start := time.Now()
|
|
svc.RefreshStatus()
|
|
if time.Since(start) != 0 {
|
|
t.Fatal("status sample waited for a public-IP request")
|
|
}
|
|
synctest.Wait()
|
|
assertStatus("192.0.2.10")
|
|
close(release)
|
|
synctest.Wait()
|
|
svc.RefreshStatus()
|
|
assertStatus("192.0.2.20")
|
|
})
|
|
}
|
|
|
|
func TestPublicIPColdSynchronousCallersShareInitialLookup(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls, completed atomic.Int32
|
|
release := make(chan struct{})
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
calls.Add(1)
|
|
select {
|
|
case <-release:
|
|
case <-r.Context().Done():
|
|
return nil, r.Context().Err()
|
|
}
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(200, "192.0.2.10")
|
|
}
|
|
return publicIPResponse(200, "2001:db8::10")
|
|
})
|
|
svc := &ServerService{}
|
|
var wg sync.WaitGroup
|
|
for range 20 {
|
|
wg.Go(func() { svc.resolvePublicIPs(); completed.Add(1) })
|
|
}
|
|
synctest.Wait()
|
|
early, started := completed.Load(), calls.Load()
|
|
close(release)
|
|
wg.Wait()
|
|
synctest.Wait()
|
|
if early != 0 || started != 1 {
|
|
t.Fatalf("cold callers completed=%d started requests=%d, want 0 and 1", early, started)
|
|
}
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "2001:db8::10")
|
|
if calls.Load() != 2 || completed.Load() != 20 {
|
|
t.Fatalf("after initial lookup requests=%d completed=%d", calls.Load(), completed.Load())
|
|
}
|
|
})
|
|
}
|
|
|
|
func waitForPublicIPLookup(t *testing.T, svc *ServerService) {
|
|
t.Helper()
|
|
if done := svc.resolvePublicIPsInBackground(); done != nil {
|
|
<-done
|
|
}
|
|
}
|
|
|
|
func TestPublicIPv6FailuresBackOffAndCapRetries(t *testing.T) {
|
|
for _, failure := range []string{"http-error", "empty-body", "transport-error", "timeout"} {
|
|
t.Run(failure, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls6 atomic.Int32
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(http.StatusOK, "192.0.2.10")
|
|
}
|
|
calls6.Add(1)
|
|
switch failure {
|
|
case "empty-body":
|
|
return publicIPResponse(http.StatusOK, "")
|
|
case "transport-error":
|
|
return nil, io.ErrUnexpectedEOF
|
|
case "timeout":
|
|
<-r.Context().Done()
|
|
return nil, r.Context().Err()
|
|
default:
|
|
return publicIPResponse(http.StatusServiceUnavailable, "")
|
|
}
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "N/A")
|
|
if got := calls6.Load(); got != 2 {
|
|
t.Fatalf("initial IPv6 requests=%d, want 2", got)
|
|
}
|
|
for _, delay := range []time.Duration{time.Minute, 2 * time.Minute, 5 * time.Minute, 15 * time.Minute, 15 * time.Minute, 15 * time.Minute} {
|
|
before := calls6.Load()
|
|
time.Sleep(delay - time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls6.Load(); got != before {
|
|
t.Fatalf("IPv6 retried before %v: requests=%d, want %d", delay, got, before)
|
|
}
|
|
time.Sleep(time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls6.Load(); got != before+2 {
|
|
t.Fatalf("IPv6 requests at %v=%d, want %d", delay, got, before+2)
|
|
}
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "N/A")
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPublicIPv6RecoveryResetsBackoffAndRefreshesNormally(t *testing.T) {
|
|
for _, recovered := range []string{"2001:db8::10", "2001:db8::20"} {
|
|
t.Run(recovered, func(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls6 atomic.Int32
|
|
available := true
|
|
value6 := "2001:db8::10"
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
return publicIPResponse(http.StatusOK, "192.0.2.10")
|
|
}
|
|
calls6.Add(1)
|
|
if !available {
|
|
return publicIPResponse(http.StatusServiceUnavailable, "")
|
|
}
|
|
return publicIPResponse(http.StatusOK, value6)
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
available = false
|
|
for _, delay := range []time.Duration{5 * time.Minute, time.Minute, 2 * time.Minute, 5 * time.Minute} {
|
|
time.Sleep(delay)
|
|
waitForPublicIPLookup(t, svc)
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", "2001:db8::10")
|
|
}
|
|
available, value6 = true, recovered
|
|
before := calls6.Load()
|
|
time.Sleep(15*time.Minute - time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls6.Load(); got != before {
|
|
t.Fatalf("recovery attempted before capped deadline: requests=%d, want %d", got, before)
|
|
}
|
|
time.Sleep(time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", recovered)
|
|
if got := calls6.Load(); got != before+1 {
|
|
t.Fatalf("recovery requests=%d, want %d", got, before+1)
|
|
}
|
|
|
|
before = calls6.Load()
|
|
time.Sleep(5*time.Minute - time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls6.Load(); got != before {
|
|
t.Fatalf("successful IPv6 refreshed before five minutes: requests=%d, want %d", got, before)
|
|
}
|
|
available = false
|
|
time.Sleep(time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls6.Load(); got != before+2 {
|
|
t.Fatalf("refresh requests=%d, want %d", got, before+2)
|
|
}
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", recovered)
|
|
before = calls6.Load()
|
|
time.Sleep(time.Minute - time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls6.Load(); got != before {
|
|
t.Fatalf("IPv6 retried before reset one-minute deadline: requests=%d, want %d", got, before)
|
|
}
|
|
available, value6 = true, "2001:db8::30"
|
|
time.Sleep(time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
assertCachedPublicIPs(t, svc, "192.0.2.10", value6)
|
|
if got := calls6.Load(); got != before+1 {
|
|
t.Fatalf("one-minute recovery requests=%d, want %d", got, before+1)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestPublicIPv6BackoffPreservesIPv4RefreshAndRetry(t *testing.T) {
|
|
synctest.Test(t, func(t *testing.T) {
|
|
var calls4, calls6 atomic.Int32
|
|
available4, value4 := true, "192.0.2.10"
|
|
stubPublicIPServices(t, func(r *http.Request) (*http.Response, error) {
|
|
if r.URL.Hostname() == "ipv4.example.test" {
|
|
calls4.Add(1)
|
|
if available4 {
|
|
return publicIPResponse(http.StatusOK, value4)
|
|
}
|
|
} else {
|
|
calls6.Add(1)
|
|
}
|
|
return publicIPResponse(http.StatusServiceUnavailable, "")
|
|
})
|
|
svc := &ServerService{}
|
|
svc.resolvePublicIPs()
|
|
for _, delay := range []time.Duration{time.Minute, 2 * time.Minute, 5 * time.Minute} {
|
|
time.Sleep(delay)
|
|
waitForPublicIPLookup(t, svc)
|
|
}
|
|
if got := calls6.Load(); got != 8 {
|
|
t.Fatalf("IPv6 requests before capped backoff=%d, want 8", got)
|
|
}
|
|
value4 = "192.0.2.20"
|
|
before := calls4.Load()
|
|
time.Sleep(5*time.Minute - time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls4.Load(); got != before {
|
|
t.Fatalf("IPv4 refreshed before five minutes: requests=%d, want %d", got, before)
|
|
}
|
|
time.Sleep(time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
assertCachedPublicIPs(t, svc, value4, "N/A")
|
|
if got := calls4.Load(); got != before+1 {
|
|
t.Fatalf("IPv4 refresh requests=%d, want %d", got, before+1)
|
|
}
|
|
available4 = false
|
|
time.Sleep(5 * time.Minute)
|
|
waitForPublicIPLookup(t, svc)
|
|
assertCachedPublicIPs(t, svc, value4, "N/A")
|
|
before = calls4.Load()
|
|
time.Sleep(time.Minute - time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
if got := calls4.Load(); got != before {
|
|
t.Fatalf("IPv4 retried before one minute: requests=%d, want %d", got, before)
|
|
}
|
|
available4, value4 = true, "192.0.2.30"
|
|
time.Sleep(time.Nanosecond)
|
|
waitForPublicIPLookup(t, svc)
|
|
assertCachedPublicIPs(t, svc, value4, "N/A")
|
|
if got := calls4.Load(); got != before+1 {
|
|
t.Fatalf("IPv4 retry requests=%d, want %d", got, before+1)
|
|
}
|
|
if got := calls6.Load(); got != 8 {
|
|
t.Fatalf("IPv6 retried during capped backoff: requests=%d, want 8", got)
|
|
}
|
|
})
|
|
}
|