65 lines
2.2 KiB
Go
65 lines
2.2 KiB
Go
package httpapi
|
|||
|
|
|
||
|
|
import (
|
||
|
|
"context"
|
||
|
|
"net/http"
|
||
|
|
"testing"
|
||
|
|
"time"
|
||
|
|
|
||
|
|
"cloudipvalidator/internal/openstack"
|
||
|
|
)
|
||
|
|
|
||
|
|
// slowDetachOS delays every Disassociate so the client can give up first.
|
||
|
|
type slowDetachOS struct {
|
||
|
|
*openstack.MockClient
|
||
|
|
delay time.Duration
|
||
|
|
}
|
||
|
|
|
||
|
|
func (s slowDetachOS) DisassociateFloatingIP(ctx context.Context, fipID string) error {
|
||
|
|
time.Sleep(s.delay)
|
||
|
|
return s.MockClient.DisassociateFloatingIP(ctx, fipID)
|
||
|
|
}
|
||
|
|
|
||
|
|
// A client that gives up (a dashboard or curl timeout) must not abort
|
||
|
|
// "clear queue" halfway: on 2026-10-02 the request context was cancelled after
|
||
|
|
// part of the floating IPs were detached, the database was left untouched and
|
||
|
|
// the checks kept running.
|
||
|
|
func TestClearQueueSurvivesClientDisconnect(t *testing.T) {
|
||
|
|
fc, d, orch, mock := newConfigTestHarness(t)
|
||
|
|
ctx := context.Background()
|
||
|
|
mock.Seed("fip-1", "1.2.3.4", "svc")
|
||
|
|
if err := d.RegisterValidator(ctx, "validator-1", "host", "port-1", "v"); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
if err := d.SeedQueue(ctx, []string{"1.2.3.4", "5.6.7.8"}); err != nil {
|
||
|
|
t.Fatal(err)
|
||
|
|
}
|
||
|
|
orch.Tick(ctx) // validator-1 takes 1.2.3.4 and attaches its floating IP
|
||
|
|
if fip, _ := mock.GetFloatingIPByAddress(ctx, "1.2.3.4"); fip.PortID != "port-1" {
|
||
|
|
t.Fatalf("setup: floating ip not attached (port %q)", fip.PortID)
|
||
|
|
}
|
||
|
|
orch.OS = slowDetachOS{MockClient: mock, delay: 400 * time.Millisecond}
|
||
|
|
|
||
|
|
reqCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond)
|
||
|
|
defer cancel()
|
||
|
|
// No body: the server only notices that the client has left (and cancels
|
||
|
|
// the request context) when it is not waiting for a request body.
|
||
|
|
req, _ := http.NewRequestWithContext(reqCtx, http.MethodPost, fc.base+"/api/v1/admin/ips/clear", nil)
|
||
|
|
if resp, err := fc.client.Do(req); err == nil {
|
||
|
|
resp.Body.Close()
|
||
|
|
t.Fatalf("the client was expected to time out, got status %d", resp.StatusCode)
|
||
|
|
}
|
||
|
|
|
||
|
|
deadline := time.Now().Add(5 * time.Second)
|
||
|
|
for {
|
||
|
|
ips, _ := d.ListIPs(ctx)
|
||
|
|
fip, _ := mock.GetFloatingIPByAddress(ctx, "1.2.3.4")
|
||
|
|
if len(ips) == 0 && fip.PortID == "" {
|
||
|
|
return
|
||
|
|
}
|
||
|
|
if time.Now().After(deadline) {
|
||
|
|
t.Fatalf("clear did not finish after the client left: %d rows, floating ip on port %q", len(ips), fip.PortID)
|
||
|
|
}
|
||
|
|
time.Sleep(50 * time.Millisecond)
|
||
|
|
}
|
||
|
|
}
|