The cloud is live: the address list submitted as "free" (bootstrap config
or POST /api/v1/admin/ips) can drift by the time the orchestrator claims
it, or an operator can queue an already-occupied address by mistake.
Neutron's floating-IP association is a blind "last write wins" PUT with
no conflict error to catch, so associateFIP now checks the FIP's PortID
(already fetched via GetFloatingIPByAddress) before associating, guarded
against the false-positive of the FIP already belonging to this same
validator's own port.
A match routes the address straight to a new terminal ip_queue.state
("occupied", distinct from failed/fail) via db.MarkFIPOccupied — no
retries, since Neutron won't free it on its own and requeuing would let
it be reclaimed again next tick, starving the rest of the queue — plus a
dedicated fip_occupied audit event. Resubmitting the address later (once
the conflict is resolved) resets it to queued via the existing
POST /api/v1/admin/ips resubmit path (CancelIP/ListExpiredLeases updated
to treat occupied as terminal too). admin-dashboard gets its own "занят"
badge, distinct from fail/partial/cancelled.
Rebuilt bin/{control-api,admin-dashboard,prober,validator-agent} and
bin/SHA256SUMS per docs/SETUP.md's documented build recipe, since
control-api and admin-dashboard source changed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NeVbMVEiE7XQAkBd7HQgj6
177 lines
5.2 KiB
Go
177 lines
5.2 KiB
Go
package db
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestSetFIPAssociatedStampsTimestamp(t *testing.T) {
|
|
d, ctx := newTestDB(t)
|
|
|
|
if err := d.SeedQueue(ctx, []string{"1.2.3.4"}); err != nil {
|
|
t.Fatalf("seed queue: %v", err)
|
|
}
|
|
if err := d.AdminCreateValidator(ctx, "validator-1", "port-1"); err != nil {
|
|
t.Fatalf("create validator: %v", err)
|
|
}
|
|
claimed, err := d.ClaimNextQueued(ctx, "validator-1", time.Minute)
|
|
if err != nil || claimed == nil {
|
|
t.Fatalf("claim: item=%+v err=%v", claimed, err)
|
|
}
|
|
if claimed.FIPAssociatedAt != nil {
|
|
t.Fatalf("expected FIPAssociatedAt nil before association, got %v", claimed.FIPAssociatedAt)
|
|
}
|
|
|
|
before := Now()
|
|
if err := d.SetFIPAssociated(ctx, claimed.ID, "fip-1", time.Minute); err != nil {
|
|
t.Fatalf("set fip associated: %v", err)
|
|
}
|
|
item, err := d.GetIP(ctx, claimed.ID)
|
|
if err != nil {
|
|
t.Fatalf("get ip: %v", err)
|
|
}
|
|
if item.FIPAssociatedAt == nil {
|
|
t.Fatalf("expected FIPAssociatedAt to be set")
|
|
}
|
|
if item.FIPAssociatedAt.Before(before) {
|
|
t.Fatalf("expected FIPAssociatedAt >= %v, got %v", before, *item.FIPAssociatedAt)
|
|
}
|
|
}
|
|
|
|
func TestMarkFIPOccupiedIsTerminalAndFreesValidator(t *testing.T) {
|
|
d, ctx := newTestDB(t)
|
|
|
|
if err := d.SeedQueue(ctx, []string{"1.2.3.4"}); err != nil {
|
|
t.Fatalf("seed queue: %v", err)
|
|
}
|
|
if err := d.AdminCreateValidator(ctx, "validator-1", "port-1"); err != nil {
|
|
t.Fatalf("create validator: %v", err)
|
|
}
|
|
claimed, err := d.ClaimNextQueued(ctx, "validator-1", time.Minute)
|
|
if err != nil || claimed == nil {
|
|
t.Fatalf("claim: item=%+v err=%v", claimed, err)
|
|
}
|
|
|
|
if err := d.MarkFIPOccupied(ctx, claimed.ID, "validator-1"); err != nil {
|
|
t.Fatalf("mark fip occupied: %v", err)
|
|
}
|
|
|
|
item, err := d.GetIP(ctx, claimed.ID)
|
|
if err != nil {
|
|
t.Fatalf("get ip: %v", err)
|
|
}
|
|
if item.State != IPOccupied {
|
|
t.Fatalf("expected occupied, got state=%s", item.State)
|
|
}
|
|
if item.OwnerValidatorID != nil {
|
|
t.Fatalf("expected owner cleared, got %v", *item.OwnerValidatorID)
|
|
}
|
|
if item.FIPID != "" {
|
|
t.Fatalf("expected fip_id cleared, got %q", item.FIPID)
|
|
}
|
|
if item.LeaseExpiresAt != nil {
|
|
t.Fatalf("expected lease cleared, got %v", item.LeaseExpiresAt)
|
|
}
|
|
|
|
v, err := d.GetValidator(ctx, "validator-1")
|
|
if err != nil {
|
|
t.Fatalf("get validator: %v", err)
|
|
}
|
|
if v.State != ValidatorIdle || v.CurrentIPID != nil {
|
|
t.Fatalf("expected validator freed to idle, got state=%s current_ip=%v", v.State, v.CurrentIPID)
|
|
}
|
|
}
|
|
|
|
func TestSubmitIPsResubmitsOccupiedAddress(t *testing.T) {
|
|
d, ctx := newTestDB(t)
|
|
|
|
if err := d.SeedQueue(ctx, []string{"1.2.3.4"}); err != nil {
|
|
t.Fatalf("seed queue: %v", err)
|
|
}
|
|
if err := d.AdminCreateValidator(ctx, "validator-1", "port-1"); err != nil {
|
|
t.Fatalf("create validator: %v", err)
|
|
}
|
|
claimed, err := d.ClaimNextQueued(ctx, "validator-1", time.Minute)
|
|
if err != nil || claimed == nil {
|
|
t.Fatalf("claim: item=%+v err=%v", claimed, err)
|
|
}
|
|
if err := d.MarkFIPOccupied(ctx, claimed.ID, "validator-1"); err != nil {
|
|
t.Fatalf("mark fip occupied: %v", err)
|
|
}
|
|
|
|
result, err := d.SubmitIPs(ctx, []string{"1.2.3.4"})
|
|
if err != nil {
|
|
t.Fatalf("submit ips: %v", err)
|
|
}
|
|
if len(result.Requeued) != 1 || result.Requeued[0] != "1.2.3.4" {
|
|
t.Fatalf("expected address requeued, got %+v", result)
|
|
}
|
|
if len(result.SkippedInProgress) != 0 {
|
|
t.Fatalf("expected nothing skipped as in-progress, got %+v", result.SkippedInProgress)
|
|
}
|
|
|
|
item, err := d.GetIP(ctx, claimed.ID)
|
|
if err != nil {
|
|
t.Fatalf("get ip: %v", err)
|
|
}
|
|
if item.State != IPQueued {
|
|
t.Fatalf("expected queued after resubmit, got state=%s", item.State)
|
|
}
|
|
}
|
|
|
|
func TestCancelIPRejectsAlreadyOccupied(t *testing.T) {
|
|
d, ctx := newTestDB(t)
|
|
|
|
if err := d.SeedQueue(ctx, []string{"1.2.3.4"}); err != nil {
|
|
t.Fatalf("seed queue: %v", err)
|
|
}
|
|
if err := d.AdminCreateValidator(ctx, "validator-1", "port-1"); err != nil {
|
|
t.Fatalf("create validator: %v", err)
|
|
}
|
|
claimed, err := d.ClaimNextQueued(ctx, "validator-1", time.Minute)
|
|
if err != nil || claimed == nil {
|
|
t.Fatalf("claim: item=%+v err=%v", claimed, err)
|
|
}
|
|
if err := d.MarkFIPOccupied(ctx, claimed.ID, "validator-1"); err != nil {
|
|
t.Fatalf("mark fip occupied: %v", err)
|
|
}
|
|
|
|
err = d.CancelIP(ctx, claimed.ID)
|
|
if !errors.Is(err, ErrInvalidState) {
|
|
t.Fatalf("expected ErrInvalidState cancelling an occupied ip, got %v", err)
|
|
}
|
|
}
|
|
|
|
func TestRequeueClearsFIPAssociatedAt(t *testing.T) {
|
|
d, ctx := newTestDB(t)
|
|
|
|
if err := d.SeedQueue(ctx, []string{"1.2.3.4"}); err != nil {
|
|
t.Fatalf("seed queue: %v", err)
|
|
}
|
|
if err := d.AdminCreateValidator(ctx, "validator-1", "port-1"); err != nil {
|
|
t.Fatalf("create validator: %v", err)
|
|
}
|
|
claimed, err := d.ClaimNextQueued(ctx, "validator-1", time.Minute)
|
|
if err != nil || claimed == nil {
|
|
t.Fatalf("claim: item=%+v err=%v", claimed, err)
|
|
}
|
|
if err := d.SetFIPAssociated(ctx, claimed.ID, "fip-1", time.Minute); err != nil {
|
|
t.Fatalf("set fip associated: %v", err)
|
|
}
|
|
|
|
if err := d.RequeueOrFail(ctx, claimed.ID, "validator-1", 3); err != nil {
|
|
t.Fatalf("requeue or fail: %v", err)
|
|
}
|
|
item, err := d.GetIP(ctx, claimed.ID)
|
|
if err != nil {
|
|
t.Fatalf("get ip after requeue: %v", err)
|
|
}
|
|
if item.State != IPQueued {
|
|
t.Fatalf("expected requeued, got state=%s", item.State)
|
|
}
|
|
if item.FIPAssociatedAt != nil {
|
|
t.Fatalf("expected FIPAssociatedAt cleared on requeue, got %v", item.FIPAssociatedAt)
|
|
}
|
|
}
|