fetch_ml/tests/unit/worker/prewarm_v1_test.go
Jeremie Fraeys fc2459977c
refactor(worker): update worker tests and native bridge
**Worker Refactoring:**
- Update internal/worker/factory.go, worker.go, snapshot_store.go
- Update native_bridge.go and native_bridge_nocgo.go for native library integration

**Test Updates:**
- Update all worker unit tests for new interfaces
- Update chaos tests
- Update container/podman_test.go
- Add internal/workertest/worker.go for shared test utilities

**Documentation:**
- Update native/README.md
2026-02-23 18:04:22 -05:00

296 lines
8.6 KiB
Go

package worker_test
import (
"context"
"os"
"path/filepath"
"testing"
"time"
"github.com/alicebob/miniredis/v2"
"github.com/jfraeys/fetch_ml/internal/queue"
"github.com/jfraeys/fetch_ml/internal/worker"
"github.com/jfraeys/fetch_ml/internal/workertest"
)
func TestPrewarmNextOnce_Snapshot_WritesPrewarmDir(t *testing.T) {
s, err := miniredis.Run()
if err != nil {
t.Fatalf("miniredis: %v", err)
}
t.Cleanup(s.Close)
base := t.TempDir()
dataDir := filepath.Join(base, "data")
// Create a snapshot directory and compute its overall SHA.
srcSnapshot := filepath.Join(base, "snapshot-src")
if err := os.MkdirAll(srcSnapshot, 0750); err != nil {
t.Fatalf("mkdir src snapshot: %v", err)
}
if err := os.WriteFile(filepath.Join(srcSnapshot, "file.txt"), []byte("ok"), 0600); err != nil {
t.Fatalf("write snapshot file: %v", err)
}
sha, err := worker.DirOverallSHA256Hex(srcSnapshot)
if err != nil {
t.Fatalf("DirOverallSHA256Hex: %v", err)
}
cacheDir := filepath.Join(dataDir, "snapshots", "sha256", sha)
if err := os.MkdirAll(filepath.Dir(cacheDir), 0750); err != nil {
t.Fatalf("mkdir cache parent: %v", err)
}
if err := os.Rename(srcSnapshot, cacheDir); err != nil {
t.Fatalf("rename into cache: %v", err)
}
// Queue has one task; prewarm should stage it into base/.prewarm/snapshots/<taskID>.
tq, err := queue.NewTaskQueue(queue.Config{RedisAddr: s.Addr(), MetricsFlushInterval: 5 * time.Millisecond})
if err != nil {
t.Fatalf("NewTaskQueue: %v", err)
}
t.Cleanup(func() { _ = tq.Close() })
task := &queue.Task{
ID: "task-1",
JobName: "job-1",
Status: "queued",
Priority: 10,
CreatedAt: time.Now().UTC(),
SnapshotID: "snap-1",
Metadata: map[string]string{
"snapshot_sha256": "sha256:" + sha,
},
}
if err := tq.AddTask(task); err != nil {
t.Fatalf("AddTask: %v", err)
}
cfg := &worker.Config{
WorkerID: "worker-1",
BasePath: base,
DataDir: dataDir,
PrewarmEnabled: true,
AutoFetchData: false,
LocalMode: true,
PollInterval: 1,
MaxWorkers: 1,
DatasetCacheTTL: 30 * time.Minute,
}
w := workertest.NewTestWorkerWithQueue(cfg, tq)
ok, err := w.PrewarmNextOnce(context.Background())
if err != nil {
t.Fatalf("PrewarmNextOnce: %v", err)
}
if !ok {
t.Fatalf("expected ok=true")
}
prewarmed := filepath.Join(base, ".prewarm", "snapshots", task.ID, "file.txt")
if _, err := os.Stat(prewarmed); err != nil {
t.Fatalf("expected prewarmed file to exist: %v", err)
}
}
func TestPrewarmNextOnce_Disabled_NoOp(t *testing.T) {
s, err := miniredis.Run()
if err != nil {
t.Fatalf("miniredis: %v", err)
}
t.Cleanup(s.Close)
base := t.TempDir()
dataDir := filepath.Join(base, "data")
tq, err := queue.NewTaskQueue(queue.Config{RedisAddr: s.Addr(), MetricsFlushInterval: 5 * time.Millisecond})
if err != nil {
t.Fatalf("NewTaskQueue: %v", err)
}
t.Cleanup(func() { _ = tq.Close() })
task := &queue.Task{ID: "task-1", JobName: "job-1", Status: "queued", Priority: 10, CreatedAt: time.Now().UTC()}
if err := tq.AddTask(task); err != nil {
t.Fatalf("AddTask: %v", err)
}
cfg := &worker.Config{WorkerID: "worker-1", BasePath: base, DataDir: dataDir, PrewarmEnabled: false}
w := workertest.NewTestWorkerWithQueue(cfg, tq)
ok, err := w.PrewarmNextOnce(context.Background())
if err != nil {
t.Fatalf("PrewarmNextOnce: %v", err)
}
if ok {
t.Fatalf("expected ok=false")
}
if _, err := os.Stat(filepath.Join(base, ".prewarm")); err == nil {
t.Fatalf("expected no .prewarm dir when disabled")
}
}
func TestPrewarmNextOnce_QueueEmpty_DoesNotDeleteState(t *testing.T) {
s, err := miniredis.Run()
if err != nil {
t.Fatalf("miniredis: %v", err)
}
t.Cleanup(s.Close)
base := t.TempDir()
dataDir := filepath.Join(base, "data")
// Create a snapshot directory and compute its overall SHA.
srcSnapshot := filepath.Join(base, "snapshot-src")
if err := os.MkdirAll(srcSnapshot, 0750); err != nil {
t.Fatalf("mkdir src snapshot: %v", err)
}
if err := os.WriteFile(filepath.Join(srcSnapshot, "file.txt"), []byte("ok"), 0600); err != nil {
t.Fatalf("write snapshot file: %v", err)
}
sha, err := worker.DirOverallSHA256Hex(srcSnapshot)
if err != nil {
t.Fatalf("DirOverallSHA256Hex: %v", err)
}
cacheDir := filepath.Join(dataDir, "snapshots", "sha256", sha)
if err := os.MkdirAll(filepath.Dir(cacheDir), 0750); err != nil {
t.Fatalf("mkdir cache parent: %v", err)
}
if err := os.Rename(srcSnapshot, cacheDir); err != nil {
t.Fatalf("rename into cache: %v", err)
}
tq, err := queue.NewTaskQueue(queue.Config{RedisAddr: s.Addr(), MetricsFlushInterval: 5 * time.Millisecond})
if err != nil {
t.Fatalf("NewTaskQueue: %v", err)
}
t.Cleanup(func() { _ = tq.Close() })
task := &queue.Task{
ID: "task-1",
JobName: "job-1",
Status: "queued",
Priority: 10,
CreatedAt: time.Now().UTC(),
SnapshotID: "snap-1",
Metadata: map[string]string{
"snapshot_sha256": "sha256:" + sha,
},
}
if err := tq.AddTask(task); err != nil {
t.Fatalf("AddTask: %v", err)
}
cfg := &worker.Config{
WorkerID: "worker-1",
BasePath: base,
DataDir: dataDir,
PrewarmEnabled: true,
AutoFetchData: false,
LocalMode: true,
PollInterval: 1,
MaxWorkers: 1,
DatasetCacheTTL: 30 * time.Minute,
}
w := workertest.NewTestWorkerWithQueue(cfg, tq)
ok, err := w.PrewarmNextOnce(context.Background())
if err != nil {
t.Fatalf("PrewarmNextOnce: %v", err)
}
if !ok {
t.Fatalf("expected ok=true")
}
// Empty the queue and run again. This should not delete the prewarm state; it should
// simply cancel its internal state and let the Redis TTL expire naturally.
_, _ = tq.GetNextTask() // drain the only queued task
_, _ = w.PrewarmNextOnce(context.Background())
state, err := tq.GetWorkerPrewarmState(cfg.WorkerID)
if err != nil {
t.Fatalf("GetWorkerPrewarmState: %v", err)
}
if state == nil {
t.Fatalf("expected prewarm state to remain present when queue is empty")
}
}
func TestStageSnapshotFromPath_UsesPrewarm(t *testing.T) {
base := t.TempDir()
taskID := "task-1"
jobDir := filepath.Join(base, "pending", "job-1", "run")
if err := os.MkdirAll(jobDir, 0750); err != nil {
t.Fatalf("mkdir jobDir: %v", err)
}
// Create a prewarmed snapshot directory.
prewarmSrc := filepath.Join(base, ".prewarm", "snapshots", taskID)
if err := os.MkdirAll(prewarmSrc, 0750); err != nil {
t.Fatalf("mkdir prewarm parent: %v", err)
}
if err := os.WriteFile(filepath.Join(prewarmSrc, "file.txt"), []byte("prewarmed"), 0600); err != nil {
t.Fatalf("write prewarm file: %v", err)
}
// Call stageSnapshotFromPath with a dummy srcPath; it should prefer the prewarmed dir.
dummySrc := filepath.Join(base, "unused")
if err := worker.StageSnapshotFromPath(base, taskID, dummySrc, jobDir); err != nil {
t.Fatalf("StageSnapshotFromPath: %v", err)
}
// Verify the prewarmed content was renamed into the job snapshot dir.
dstFile := filepath.Join(jobDir, "snapshot", "file.txt")
if _, err := os.Stat(dstFile); err != nil {
t.Fatalf("expected prewarmed file in job snapshot dir: %v", err)
}
if _, err := os.Stat(prewarmSrc); err == nil {
t.Fatalf("expected prewarm src to be moved (rename) not copied")
}
// Verify the content is correct.
got, err := os.ReadFile(dstFile)
if err != nil {
t.Fatalf("read dst file: %v", err)
}
if string(got) != "prewarmed" {
t.Fatalf("expected content 'prewarmed', got %q", string(got))
}
}
func TestStageSnapshotFromPath_FallsBackToCopy_WhenNoPrewarm(t *testing.T) {
base := t.TempDir()
taskID := "task-1"
jobDir := filepath.Join(base, "pending", "job-1", "run")
if err := os.MkdirAll(jobDir, 0750); err != nil {
t.Fatalf("mkdir jobDir: %v", err)
}
// Create a source snapshot dir (simulating ResolveSnapshot result).
src := filepath.Join(base, "src")
if err := os.MkdirAll(src, 0750); err != nil {
t.Fatalf("mkdir src: %v", err)
}
if err := os.WriteFile(filepath.Join(src, "file.txt"), []byte("source"), 0600); err != nil {
t.Fatalf("write src file: %v", err)
}
// No prewarm dir exists; should copy from src.
if err := worker.StageSnapshotFromPath(base, taskID, src, jobDir); err != nil {
t.Fatalf("StageSnapshotFromPath: %v", err)
}
dstFile := filepath.Join(jobDir, "snapshot", "file.txt")
if _, err := os.Stat(dstFile); err != nil {
t.Fatalf("expected file in job snapshot dir: %v", err)
}
got, err := os.ReadFile(dstFile)
if err != nil {
t.Fatalf("read dst file: %v", err)
}
if string(got) != "source" {
t.Fatalf("expected content 'source', got %q", string(got))
}
// Verify src is still present (copy, not move).
if _, err := os.Stat(filepath.Join(src, "file.txt")); err != nil {
t.Fatalf("expected src file to remain after copy")
}
}