feat: add resilient updates and access controls
Container-Image veröffentlichen / container (push) Has been cancelled
Container-Image veröffentlichen / container (push) Has been cancelled
This commit is contained in:
1 parent
a000152423
commit
f02ec9977c
41 files changed
+3277
-151
No files matched your search
@@ -5,11 +5,13 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"io"
|
||||
"log/slog"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -20,6 +22,24 @@ import (
|
||||
"github.com/root-kdw12/hetzner-api-dyndns-records-webinterface/internal/store"
|
||||
)
|
||||
|
||||
type retryResolver struct {
|
||||
calls atomic.Int32
|
||||
recovered chan struct{}
|
||||
}
|
||||
|
||||
func (r *retryResolver) ResolveWithIPv6(_ context.Context, _, _ []string, _ bool) ipresolve.Result {
|
||||
call := r.calls.Add(1)
|
||||
if call == 1 {
|
||||
return ipresolve.Result{IPv4Err: errors.New("network is not ready")}
|
||||
}
|
||||
if call == 2 {
|
||||
close(r.recovered)
|
||||
}
|
||||
return ipresolve.Result{IPv4: "198.51.100.42"}
|
||||
}
|
||||
|
||||
func (r *retryResolver) ValidateEndpoint(_ context.Context, _ string) error { return nil }
|
||||
|
||||
func TestSyncZoneImportsAddressRecordsAndKeepsArchivedTargetsDetached(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx := context.Background()
|
||||
@@ -164,6 +184,81 @@ func TestDisabledUpdaterRejectsRunsAndTriggerResetsCountdown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchedulerRetriesFailedStartupResolutionWithoutWaitingForFullInterval(t *testing.T) {
|
||||
t.Parallel()
|
||||
database, err := store.Open(context.Background(), filepath.Join(t.TempDir(), "app.db"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = database.Close() })
|
||||
settings, err := database.GetSettings(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := database.UpdateSettings(context.Background(), settings.UpdateIntervalSeconds, true, false, settings.IPv4Endpoints, settings.IPv6Endpoints); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resolver := &retryResolver{recovered: make(chan struct{})}
|
||||
schedule := New(database, nil, ipresolve.New(), nil, slog.New(slog.NewTextHandler(io.Discard, nil)))
|
||||
schedule.resolver = resolver
|
||||
schedule.retryMin = 5 * time.Millisecond
|
||||
schedule.retryMax = 20 * time.Millisecond
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(cancel)
|
||||
schedule.Start(ctx)
|
||||
|
||||
select {
|
||||
case <-resolver.recovered:
|
||||
case <-time.After(2 * time.Second):
|
||||
t.Fatal("scheduler did not retry the failed startup resolution")
|
||||
}
|
||||
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for {
|
||||
settings, err = database.GetSettings(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if settings.LastIPv4 == "198.51.100.42" {
|
||||
break
|
||||
}
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("successful retry did not persist IPv4: %#v", settings)
|
||||
}
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
|
||||
runs, err := database.RecentRuns(context.Background(), 10)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(runs) != 2 || runs[0].Status != "ok" || runs[1].Status != "error" {
|
||||
t.Fatalf("unexpected startup retry history: %#v", runs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryDelayBacksOffToTheConfiguredSchedule(t *testing.T) {
|
||||
t.Parallel()
|
||||
schedule := &Scheduler{retryMin: 5 * time.Second}
|
||||
tests := []struct {
|
||||
failures int
|
||||
want time.Duration
|
||||
}{
|
||||
{failures: 1, want: 5 * time.Second},
|
||||
{failures: 2, want: 10 * time.Second},
|
||||
{failures: 3, want: 20 * time.Second},
|
||||
{failures: 4, want: 40 * time.Second},
|
||||
{failures: 5, want: time.Minute},
|
||||
{failures: 20, want: time.Minute},
|
||||
}
|
||||
for _, test := range tests {
|
||||
if got := schedule.retryDelay(test.failures, time.Minute); got != test.want {
|
||||
t.Errorf("retry delay after %d failures = %s, want %s", test.failures, got, test.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGloballyDisabledIPv6SkipsAAAATargetWithoutError(t *testing.T) {
|
||||
t.Parallel()
|
||||
schedule := &Scheduler{}
|
||||
|
||||
Reference in new issue
Block a user