215 lines
8.5 KiB
Go
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)
|
|
}
|
|
}
|