Files
EvoBGP/internal/store/memory_job_queue_test.go
T
DenozordecandCursor f8e0520817 feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь.

Co-authored-by: Cursor <[email protected]>
2026-11-02 17:23:59 +07:00

123 lines
3.1 KiB
Go

package store
import (
"context"
"errors"
"sync"
"testing"
"time"
)
func TestEnqueueJobTenantsDoNotShareLock(t *testing.T) {
m := NewMemory()
ctx := context.Background()
unlock := m.lockJobEnqueueTenant("tenant-a")
var once sync.Once
release := func() { once.Do(unlock) }
t.Cleanup(release)
otherDone := make(chan error, 1)
go func() {
_, _, err := m.EnqueueJob(ctx, JobEnqueue{TenantID: "tenant-b", Kind: "test"})
otherDone <- err
}()
select {
case err := <-otherDone:
if err != nil {
t.Fatal(err)
}
case <-time.After(2 * time.Second):
t.Fatal("other tenant waited on the held tenant lock")
}
sameDone := make(chan struct{})
go func() {
_, _, err := m.EnqueueJob(ctx, JobEnqueue{TenantID: "tenant-a", Kind: "test"})
if err != nil {
t.Error(err)
}
close(sameDone)
}()
select {
case <-sameDone:
t.Fatal("same tenant did not wait on its lock")
case <-time.After(200 * time.Millisecond):
}
release()
select {
case <-sameDone:
case <-time.After(2 * time.Second):
t.Fatal("same tenant did not finish after unlock")
}
}
func TestEnqueueJobIdempotencyKeyConflicts(t *testing.T) {
m := NewMemory()
ctx := context.Background()
key := "same-key"
in := JobEnqueue{TenantID: "tenant-a", Kind: "refresh", IdempotencyKey: &key, Meta: map[string]any{"module": "a"}}
first, created, err := m.EnqueueJob(ctx, in)
if err != nil || !created || first.ID == "" {
t.Fatalf("first=%+v created=%v err=%v", first, created, err)
}
in.Meta = map[string]any{"module": "b"}
if _, _, err = m.EnqueueJob(ctx, in); !errors.Is(err, ErrJobConflict) {
t.Fatalf("same key accepted: %v", err)
}
other := JobEnqueue{TenantID: "tenant-b", Kind: "refresh", IdempotencyKey: &key, Meta: map[string]any{"module": "b"}}
if _, created, err = m.EnqueueJob(ctx, other); err != nil || !created {
t.Fatalf("other tenant created=%v err=%v", created, err)
}
}
func TestMemoryJobQueueEqualTimestampOrdering(t *testing.T) {
m := NewMemory()
ctx := context.Background()
stamp := time.Date(2026, 10, 26, 0, 0, 0, 0, time.UTC)
var ids []string
for range 32 {
j, _, err := m.EnqueueJob(ctx, JobEnqueue{TenantID: "tenant", Kind: "test"})
if err != nil {
t.Fatal(err)
}
ids = append(ids, j.ID)
m.jobMu.Lock()
saved := m.jobs[j.ID]
saved.CreatedAt = stamp
m.jobs[j.ID] = saved
m.jobMu.Unlock()
}
// Repeated offset pages must use the same total order despite map iteration.
for range 5 {
cursor := ""
var listed []string
for {
page, next, more, err := m.ListJobs(ctx, "tenant", "", "", cursor, 3)
if err != nil {
t.Fatal(err)
}
for _, j := range page {
listed = append(listed, j.ID)
}
if !more {
break
}
cursor = next
}
if len(listed) != len(ids) {
t.Fatalf("listed %d jobs", len(listed))
}
for i, id := range listed {
if id != ids[len(ids)-1-i] {
t.Fatalf("list[%d]=%s expected reverse enqueue sequence %s", i, id, ids[len(ids)-1-i])
}
}
}
for i, want := range ids {
claimed, err := m.ClaimJobs(ctx, "owner", 1, time.Minute)
if err != nil || len(claimed) != 1 || claimed[0].ID != want {
t.Fatalf("claim[%d]=%+v %v want %s", i, claimed, err, want)
}
}
}