531b36924c
- job stop: real systemctl stop via SSH (was DB-only soft stop) resolves node from alloc_history or --peer flag - doctor db-retention: row count check for jobs/tasks/audit_log warns at 100k rows, suggests backup + cleanup - logs --lines: cap at 50000 (default 1000); --since upper bound 7d prevents OOM from unbounded journalctl - cache DB mode 0600 (was 0644; matches store.Open) - upgrade cutover: backup file + atomic rename (was sed -i) rollback restores from backup on failure Tests: job stop SSH, DB retention warning, logs lines cap, cache mode, cutover backup-restore + atomic rename. ---ci--- project: orca phase: 9 milestone: v0.13 status: complete requirements: covered: [158] ---/ci---
275 lines
7.0 KiB
Go
275 lines
7.0 KiB
Go
package cache
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func openTestCache(t *testing.T) (*Cache, func()) {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "orca_cache.db")
|
|
c, err := Open(path)
|
|
if err != nil {
|
|
t.Fatalf("open cache: %v", err)
|
|
}
|
|
return c, func() { _ = c.Close() }
|
|
}
|
|
|
|
func TestCache_Hit(t *testing.T) {
|
|
c, cleanup := openTestCache(t)
|
|
defer cleanup()
|
|
|
|
want := []byte("hello-orca")
|
|
if err := c.Set("nodes", "list", want, 30*time.Second); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
got, inserted, err := c.Get("nodes", "list")
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if string(got) != string(want) {
|
|
t.Errorf("get value = %q, want %q", got, want)
|
|
}
|
|
if inserted.IsZero() {
|
|
t.Errorf("inserted time is zero")
|
|
}
|
|
}
|
|
|
|
func TestCache_Miss(t *testing.T) {
|
|
c, cleanup := openTestCache(t)
|
|
defer cleanup()
|
|
|
|
got, _, err := c.Get("nodes", "missing")
|
|
if !errors.Is(err, ErrCacheMiss) {
|
|
t.Fatalf("get miss: err = %v, want ErrCacheMiss", err)
|
|
}
|
|
if got != nil {
|
|
t.Errorf("get miss value = %v, want nil", got)
|
|
}
|
|
}
|
|
|
|
func TestCache_Invalidate(t *testing.T) {
|
|
c, cleanup := openTestCache(t)
|
|
defer cleanup()
|
|
|
|
if err := c.Set("nodes", "list", []byte("a"), 30*time.Second); err != nil {
|
|
t.Fatalf("set a: %v", err)
|
|
}
|
|
if err := c.Set("nodes", "other", []byte("b"), 30*time.Second); err != nil {
|
|
t.Fatalf("set b: %v", err)
|
|
}
|
|
if err := c.Set("jobs", "list", []byte("c"), 30*time.Second); err != nil {
|
|
t.Fatalf("set c: %v", err)
|
|
}
|
|
if err := c.Invalidate("nodes"); err != nil {
|
|
t.Fatalf("invalidate: %v", err)
|
|
}
|
|
if _, _, err := c.Get("nodes", "list"); !errors.Is(err, ErrCacheMiss) {
|
|
t.Errorf("nodes/list after invalidate: err = %v, want ErrCacheMiss", err)
|
|
}
|
|
if _, _, err := c.Get("nodes", "other"); !errors.Is(err, ErrCacheMiss) {
|
|
t.Errorf("nodes/other after invalidate: err = %v, want ErrCacheMiss", err)
|
|
}
|
|
if _, _, err := c.Get("jobs", "list"); err != nil {
|
|
t.Errorf("jobs/list after nodes invalidate: err = %v, want nil", err)
|
|
}
|
|
}
|
|
|
|
func TestCache_InvalidateKey(t *testing.T) {
|
|
c, cleanup := openTestCache(t)
|
|
defer cleanup()
|
|
|
|
if err := c.Set("nodes", "list", []byte("a"), 30*time.Second); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
if err := c.InvalidateKey("nodes", "list"); err != nil {
|
|
t.Fatalf("invalidate key: %v", err)
|
|
}
|
|
if _, _, err := c.Get("nodes", "list"); !errors.Is(err, ErrCacheMiss) {
|
|
t.Errorf("get after invalidate key: err = %v, want ErrCacheMiss", err)
|
|
}
|
|
}
|
|
|
|
func TestCache_TTLExpiry(t *testing.T) {
|
|
c, cleanup := openTestCache(t)
|
|
defer cleanup()
|
|
|
|
if err := c.Set("jobs", "list", []byte("stale"), 1*time.Millisecond); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
time.Sleep(10 * time.Millisecond)
|
|
if _, _, err := c.Get("jobs", "list"); !errors.Is(err, ErrCacheMiss) {
|
|
t.Errorf("get after ttl expiry: err = %v, want ErrCacheMiss", err)
|
|
}
|
|
}
|
|
|
|
func TestCache_TTLZeroNeverExpires(t *testing.T) {
|
|
c, cleanup := openTestCache(t)
|
|
defer cleanup()
|
|
|
|
if err := c.Set("namespaces", "list", []byte("forever"), 0); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
got, _, err := c.Get("namespaces", "list")
|
|
if err != nil {
|
|
t.Fatalf("get ttl=0: %v", err)
|
|
}
|
|
if string(got) != "forever" {
|
|
t.Errorf("get ttl=0 value = %q, want %q", got, "forever")
|
|
}
|
|
}
|
|
|
|
func TestCache_Overwrite(t *testing.T) {
|
|
c, cleanup := openTestCache(t)
|
|
defer cleanup()
|
|
|
|
if err := c.Set("nodes", "list", []byte("v1"), 30*time.Second); err != nil {
|
|
t.Fatalf("set v1: %v", err)
|
|
}
|
|
if err := c.Set("nodes", "list", []byte("v2"), 30*time.Second); err != nil {
|
|
t.Fatalf("set v2: %v", err)
|
|
}
|
|
got, _, err := c.Get("nodes", "list")
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if string(got) != "v2" {
|
|
t.Errorf("get after overwrite = %q, want %q", got, "v2")
|
|
}
|
|
}
|
|
|
|
func TestCache_InvalidateAll(t *testing.T) {
|
|
c, cleanup := openTestCache(t)
|
|
defer cleanup()
|
|
|
|
_ = c.Set("nodes", "list", []byte("a"), 30*time.Second)
|
|
_ = c.Set("jobs", "list", []byte("b"), 30*time.Second)
|
|
if err := c.InvalidateAll(); err != nil {
|
|
t.Fatalf("invalidate all: %v", err)
|
|
}
|
|
if _, _, err := c.Get("nodes", "list"); !errors.Is(err, ErrCacheMiss) {
|
|
t.Errorf("nodes/list after invalidate-all: err = %v, want ErrCacheMiss", err)
|
|
}
|
|
if _, _, err := c.Get("jobs", "list"); !errors.Is(err, ErrCacheMiss) {
|
|
t.Errorf("jobs/list after invalidate-all: err = %v, want ErrCacheMiss", err)
|
|
}
|
|
}
|
|
|
|
func TestCache_Stats(t *testing.T) {
|
|
c, cleanup := openTestCache(t)
|
|
defer cleanup()
|
|
|
|
_ = c.Set("nodes", "list", []byte("aaaa"), 30*time.Second)
|
|
_ = c.Set("jobs", "list", []byte("bb"), 30*time.Second)
|
|
stats, err := c.Stats()
|
|
if err != nil {
|
|
t.Fatalf("stats: %v", err)
|
|
}
|
|
if len(stats) != 2 {
|
|
t.Fatalf("stats len = %d, want 2", len(stats))
|
|
}
|
|
var nodes, jobs *ClassStats
|
|
for i := range stats {
|
|
switch stats[i].Class {
|
|
case "nodes":
|
|
nodes = &stats[i]
|
|
case "jobs":
|
|
jobs = &stats[i]
|
|
}
|
|
}
|
|
if nodes == nil || nodes.Count != 1 || nodes.Bytes != 4 {
|
|
t.Errorf("nodes stats = %+v, want count=1 bytes=4", nodes)
|
|
}
|
|
if jobs == nil || jobs.Count != 1 || jobs.Bytes != 2 {
|
|
t.Errorf("jobs stats = %+v, want count=1 bytes=2", jobs)
|
|
}
|
|
}
|
|
|
|
func TestCache_OpenDefaultPath(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("ORCA_HOME", dir)
|
|
c, err := Open("")
|
|
if err != nil {
|
|
t.Fatalf("open default path: %v", err)
|
|
}
|
|
defer c.Close()
|
|
if err := c.Set("nodes", "list", []byte("ok"), 0); err != nil {
|
|
t.Fatalf("set: %v", err)
|
|
}
|
|
got, _, err := c.Get("nodes", "list")
|
|
if err != nil {
|
|
t.Fatalf("get: %v", err)
|
|
}
|
|
if string(got) != "ok" {
|
|
t.Errorf("get = %q, want %q", got, "ok")
|
|
}
|
|
}
|
|
|
|
func BenchmarkCacheHit(b *testing.B) {
|
|
path := filepath.Join(b.TempDir(), "orca_cache.db")
|
|
c, err := Open(path)
|
|
if err != nil {
|
|
b.Fatalf("open: %v", err)
|
|
}
|
|
defer c.Close()
|
|
if err := c.Set("nodes", "list", []byte("bench"), 0); err != nil {
|
|
b.Fatalf("set: %v", err)
|
|
}
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
if _, _, err := c.Get("nodes", "list"); err != nil {
|
|
b.Fatalf("get: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
// TestCache_FileMode0600 verifies that the cache DB file is created
|
|
// with mode 0600 (not the default umask 0644) (REQ-158, P09 T4).
|
|
func TestCache_FileMode0600(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "orca_cache.db")
|
|
c, err := Open(path)
|
|
if err != nil {
|
|
t.Fatalf("open: %v", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
t.Fatalf("stat cache db: %v", err)
|
|
}
|
|
got := info.Mode().Perm()
|
|
if got != 0o600 {
|
|
t.Errorf("cache db mode = %04o, want 0600", got)
|
|
}
|
|
}
|
|
|
|
// TestCache_FileMode0600DefaultPath verifies that the cache DB at the
|
|
// default path (ORCA_HOME) also gets 0600 (REQ-158, P09 T4).
|
|
func TestCache_FileMode0600DefaultPath(t *testing.T) {
|
|
dir := t.TempDir()
|
|
t.Setenv("ORCA_HOME", dir)
|
|
c, err := Open("")
|
|
if err != nil {
|
|
t.Fatalf("open default path: %v", err)
|
|
}
|
|
defer c.Close()
|
|
|
|
// The default path is paths.CacheDB() which is under ORCA_HOME.
|
|
// Find the db file.
|
|
dbPath := filepath.Join(dir, "orca_cache.db")
|
|
info, err := os.Stat(dbPath)
|
|
if err != nil {
|
|
t.Fatalf("stat cache db at %s: %v", dbPath, err)
|
|
}
|
|
got := info.Mode().Perm()
|
|
if got != 0o600 {
|
|
t.Errorf("cache db mode = %04o, want 0600", got)
|
|
}
|
|
}
|