feat(httpapi): harden control plane and speed up prefix refresh
Очередь job получила lease и fencing, телеметрия и прогресс deploy сохраняются, ревизия пишет manifest входов. Обновление префиксов не переписывает неизменённый снимок, большие наборы идут через COPY, а устаревшая ревизия не попадает в deploy. Бандл ноды кэшируется, prune и постановка job больше не держат горячий путь. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
924f111e48
commit
f8e0520817
197 files changed
+18655
-1416
No files matched your search
@@ -0,0 +1,122 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user