Files
Felis/internal/pgint/resize_test.go
T

215 lines
8.5 KiB
Go

//go:build pgint
package pgint
import (
"context"
"errors"
"testing"
"felis.lolicon.best/internal/api"
)
// ---- server resize (ResizeServer) --------------------------------------------------
func seedSized(t *testing.T, name, ownerID string, size api.ResourceSpec) {
t.Helper()
var owner any
if ownerID != "" {
owner = ownerID
}
mustExec(t, `INSERT INTO servers (name, cached_cpu_milli, cached_memory_mb, cached_storage_mb, owner_id)
VALUES ($1, $2, $3, $4, $5)`, name, size.CPUMilli, size.MemoryMB, size.StorageMB, owner)
}
func cachedSize(t *testing.T, name string) api.ResourceSpec {
t.Helper()
var s api.ResourceSpec
if err := db.QueryRow(`SELECT cached_cpu_milli, cached_memory_mb, cached_storage_mb FROM servers WHERE name = $1`,
name).Scan(&s.CPUMilli, &s.MemoryMB, &s.StorageMB); err != nil {
t.Fatalf("cached size of %s: %v", name, err)
}
return s
}
// A resize that grows CPU or memory must fit every cap with the server's whole size
// counted, its storage and itself among the owner's servers included; one that does
// not writes nothing. A resize that grows neither is written whatever the caps say,
// and so is any resize of a server nobody owns.
func TestResizeServerQuota(t *testing.T) {
ctx := context.Background()
n := func(v int) *int { return &v }
u := newUser(t, "user", "rsz")
sfx := suffix(t)
a, b := "rsa-"+sfx, "rsb-"+sfx
seedSized(t, a, u.ID, api.ResourceSpec{CPUMilli: 1000, MemoryMB: 2048, StorageMB: 400})
seedSized(t, b, u.ID, api.ResourceSpec{CPUMilli: 1000, MemoryMB: 2048, StorageMB: 400})
if _, err := repo.SetQuotas(ctx, u.ID, api.QuotaInput{MaxServers: n(2), MaxCPUMilli: n(3000),
MaxMemoryMB: n(6144), MaxStorageGB: n(1)}, "pgint"); err != nil {
t.Fatalf("SetQuotas: %v", err)
}
for _, c := range []struct {
name string
cpu, mem int
refused bool
after api.ResourceSpec
}{
{"cpu grows to the cap", 2000, 2048, false, api.ResourceSpec{CPUMilli: 2000, MemoryMB: 2048, StorageMB: 400}},
{"cpu grows past the cap", 2001, 2048, true, api.ResourceSpec{CPUMilli: 2000, MemoryMB: 2048, StorageMB: 400}},
{"memory grows past the cap", 2000, 4097, true, api.ResourceSpec{CPUMilli: 2000, MemoryMB: 2048, StorageMB: 400}},
{"memory grows to the cap as cpu shrinks", 1500, 4096, false, api.ResourceSpec{CPUMilli: 1500, MemoryMB: 4096, StorageMB: 400}},
} {
prevWant := cachedSize(t, a)
prev, err := repo.ResizeServer(ctx, a, c.cpu, c.mem)
switch {
case c.refused && !errors.Is(err, api.ErrQuotaExceeded):
t.Fatalf("%s: err = %v, want ErrQuotaExceeded", c.name, err)
case !c.refused && (err != nil || prev != prevWant):
t.Fatalf("%s: prev = %+v, err = %v; want %+v, nil", c.name, prev, err, prevWant)
}
if got := cachedSize(t, a); got != c.after {
t.Fatalf("%s: cache = %+v, want %+v", c.name, got, c.after)
}
}
// Storage over its cap (an admin lowered it, or the other server grew): growth
// is refused though it fits CPU and memory, and a shrink still goes through.
mustExec(t, `UPDATE servers SET cached_storage_mb = 700 WHERE name = $1`, b)
if _, err := repo.ResizeServer(ctx, a, 1600, 4096); !errors.Is(err, api.ErrQuotaExceeded) {
t.Fatalf("growth with storage over its cap: err = %v, want ErrQuotaExceeded", err)
}
if _, err := repo.ResizeServer(ctx, a, 1000, 2048); err != nil {
t.Fatalf("shrink with storage over its cap: %v", err)
}
if got, want := cachedSize(t, a), (api.ResourceSpec{CPUMilli: 1000, MemoryMB: 2048, StorageMB: 400}); got != want {
t.Fatalf("cache after the shrink = %+v, want %+v", got, want)
}
free := "rsf-" + sfx
seedSized(t, free, "", api.ResourceSpec{CPUMilli: 500, MemoryMB: 1024, StorageMB: 400})
if prev, err := repo.ResizeServer(ctx, free, 8000, 16384); err != nil || prev.CPUMilli != 500 {
t.Fatalf("resize of an unowned server = %+v, %v; want prev cpu 500, nil", prev, err)
}
if got, want := cachedSize(t, free), (api.ResourceSpec{CPUMilli: 8000, MemoryMB: 16384, StorageMB: 400}); got != want {
t.Fatalf("unowned cache = %+v, want %+v", got, want)
}
if prev, err := repo.ResizeServer(ctx, "rsnone-"+sfx, 1000, 1024); err != nil || prev != (api.ResourceSpec{}) {
t.Fatalf("resize of no server = %+v, %v; want zeroes, nil", prev, err)
}
if rowExists(t, `SELECT 1 FROM servers WHERE name = $1`, "rsnone-"+sfx) {
t.Fatal("a resize of no server made a row")
}
}
// A resize decides in its owner's claim lane: a server the owner claims while the
// resize waits is counted, and here tips the owner over the CPU cap.
func TestResizeServerWaitsForClaims(t *testing.T) {
ctx := context.Background()
n := func(v int) *int { return &v }
u := newUser(t, "user", "rszw")
sfx := suffix(t)
a, claimed := "rswa-"+sfx, "rswc-"+sfx
seedSized(t, a, u.ID, api.ResourceSpec{CPUMilli: 1000, MemoryMB: 1024, StorageMB: 1})
seedSized(t, claimed, "", api.ResourceSpec{CPUMilli: 1000, MemoryMB: 1024, StorageMB: 1})
if _, err := repo.SetQuotas(ctx, u.ID, api.QuotaInput{MaxCPUMilli: n(2000)}, "pgint"); err != nil {
t.Fatalf("SetQuotas: %v", err)
}
hold := holdLane(t, u.ID)
defer hold.Rollback() //nolint:errcheck // no-op after commit
resized := make(chan error, 1)
go func() {
_, err := repo.ResizeServer(ctx, a, 1500, 1024)
resized <- err
}()
waitForLockWait(t, "SELECT pg_advisory_xact_lock")
if _, err := hold.Exec(`UPDATE servers SET owner_id = $2 WHERE name = $1`, claimed, u.ID); err != nil {
t.Fatalf("claim: %v", err)
}
if err := hold.Commit(); err != nil {
t.Fatalf("claim commit: %v", err)
}
if err := <-resized; !errors.Is(err, api.ErrQuotaExceeded) {
t.Fatalf("resize behind a claim = %v, want ErrQuotaExceeded", err)
}
if got := cachedSize(t, a); got.CPUMilli != 1000 {
t.Fatalf("cache = %+v, want cpu still 1000", got)
}
}
// A server that changes hands while its resize waits on the old owner's lane is
// judged against the new owner's caps, under the new owner's lane.
func TestResizeServerFollowsNewOwner(t *testing.T) {
ctx := context.Background()
n := func(v int) *int { return &v }
was := newUser(t, "user", "rszo-was")
now := newUser(t, "user", "rszo-now")
a := "rsoa-" + suffix(t)
seedSized(t, a, was.ID, api.ResourceSpec{CPUMilli: 1000, MemoryMB: 1024, StorageMB: 1})
if _, err := repo.SetQuotas(ctx, now.ID, api.QuotaInput{MaxCPUMilli: n(1000)}, "pgint"); err != nil {
t.Fatalf("SetQuotas: %v", err)
}
hold := holdLane(t, was.ID)
defer hold.Rollback() //nolint:errcheck // no-op after commit
resized := make(chan error, 1)
go func() {
_, err := repo.ResizeServer(ctx, a, 1500, 1024)
resized <- err
}()
waitForLockWait(t, "SELECT pg_advisory_xact_lock")
mustExec(t, `UPDATE servers SET owner_id = $2 WHERE name = $1`, a, now.ID)
if err := hold.Commit(); err != nil {
t.Fatalf("release lane: %v", err)
}
if err := <-resized; !errors.Is(err, api.ErrQuotaExceeded) {
t.Fatalf("resize of a server that changed hands = %v, want ErrQuotaExceeded from the new owner's cap", err)
}
if got := cachedSize(t, a); got.CPUMilli != 1000 {
t.Fatalf("cache = %+v, want cpu still 1000", got)
}
}
// A server nobody owns has no lane to take, so its row lock is what orders a resize
// against a claim: a resize that reaches the row while a claim holds it waits, finds
// the claimer's name on it, and is judged against the claimer's caps.
func TestResizeServerWaitsForAClaimOfAFreeServer(t *testing.T) {
ctx := context.Background()
n := func(v int) *int { return &v }
claimer := newUser(t, "user", "rszf")
free := "rsfc-" + suffix(t)
seedSized(t, free, "", api.ResourceSpec{CPUMilli: 1000, MemoryMB: 1024, StorageMB: 1})
if _, err := repo.SetQuotas(ctx, claimer.ID, api.QuotaInput{MaxCPUMilli: n(1000)}, "pgint"); err != nil {
t.Fatalf("SetQuotas: %v", err)
}
hold, err := db.BeginTx(ctx, nil)
if err != nil {
t.Fatalf("begin: %v", err)
}
defer hold.Rollback() //nolint:errcheck // no-op after commit
if _, err := hold.Exec(`SELECT 1 FROM servers WHERE name = $1 FOR UPDATE`, free); err != nil {
t.Fatalf("hold server: %v", err)
}
if _, err := hold.Exec(`UPDATE servers SET owner_id = $2 WHERE name = $1`, free, claimer.ID); err != nil {
t.Fatalf("claim: %v", err)
}
resized := make(chan error, 1)
go func() {
_, err := repo.ResizeServer(ctx, free, 1500, 1024)
resized <- err
}()
waitForLockWait(t, "SELECT owner_id, cached_cpu_milli")
if err := hold.Commit(); err != nil {
t.Fatalf("claim commit: %v", err)
}
if err := <-resized; !errors.Is(err, api.ErrQuotaExceeded) {
t.Fatalf("resize behind a claim of a free server = %v, want ErrQuotaExceeded", err)
}
if got := cachedSize(t, free); got.CPUMilli != 1000 {
t.Fatalf("cache = %+v, want cpu still 1000", got)
}
}