Files
ayurishchevandClaude Sonnet 5 93c79b63ea Skip the check cycle for a Floating IP already occupied by another port
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
2026-09-13 23:54:35 +03:00

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)
}
}