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,114 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"evobgp/internal/jobs"
|
||||
"evobgp/internal/store"
|
||||
)
|
||||
|
||||
func TestJobReportDurableProgressAndTenantScope(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
m := store.NewMemory()
|
||||
m.SeedDemo()
|
||||
tenant, _, _, revision, speaker := m.DemoIDs()
|
||||
j, _, err := m.EnqueueJob(ctx, store.JobEnqueue{TenantID: tenant, Kind: jobs.KindDeployApply})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
claimed, err := m.ClaimJobs(ctx, "worker", 1, time.Minute)
|
||||
if err != nil || len(claimed) != 1 {
|
||||
t.Fatalf("claim: %v %v", claimed, err)
|
||||
}
|
||||
f := store.JobFence{JobID: j.ID, TenantID: tenant, OwnerID: "worker", Token: claimed[0].FencingToken}
|
||||
d, err := m.BeginDeploymentSpeaker(ctx, f, speaker, revision)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.RecordDeploymentSpeaker(ctx, f, d, "unknown", "unconfirmed", "response lost"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// A fresh registry has no in-process job or progress metadata.
|
||||
r := jobs.NewRegistry(nil)
|
||||
r.UseDurableQueue(m)
|
||||
s := &Server{store: m, jobs: r}
|
||||
request := func(tenantID, jobID string) *httptest.ResponseRecorder {
|
||||
t.Helper()
|
||||
req := httptest.NewRequest(http.MethodGet, "/v1/jobs/"+jobID+"/report", nil)
|
||||
req.SetPathValue("job_id", jobID)
|
||||
req = req.WithContext(context.WithValue(req.Context(), authCtxKey, Auth{TenantID: tenantID, Role: "operator"}))
|
||||
w := httptest.NewRecorder()
|
||||
s.handleGetJobReport(w, req)
|
||||
return w
|
||||
}
|
||||
check := func(id string) {
|
||||
t.Helper()
|
||||
w := request(tenant, id)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("report: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
var body struct {
|
||||
Progress []struct {
|
||||
State, Confirmation string
|
||||
JobID string `json:"job_id"`
|
||||
} `json:"deployment_progress"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(body.Progress) != 1 || body.Progress[0].JobID != j.ID || body.Progress[0].State != "unknown" || body.Progress[0].Confirmation != "unconfirmed" {
|
||||
t.Fatalf("lost persisted progress: %s", w.Body.String())
|
||||
}
|
||||
}
|
||||
check(j.ID)
|
||||
recovery, _, err := r.EnqueueContext(ctx, tenant, jobs.KindDeployApply, nil, nil, map[string]any{"source_deployment_job_id": j.ID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
check(recovery.ID)
|
||||
// A later explicit completion releases the delivery reservation. Once recovery
|
||||
// has its own persisted row, its report must not show the parent's old state.
|
||||
if err := m.RecordDeploymentSpeaker(ctx, f, d, "unconfirmed", "unconfirmed", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
recoveryRows, err := m.ClaimJobs(ctx, "recovery-worker", 1, time.Minute)
|
||||
if err != nil || len(recoveryRows) != 1 || recoveryRows[0].ID != recovery.ID {
|
||||
t.Fatalf("recovery claim: %v %v", recoveryRows, err)
|
||||
}
|
||||
rf := store.JobFence{JobID: recovery.ID, TenantID: tenant, OwnerID: "recovery-worker", Token: recoveryRows[0].FencingToken}
|
||||
rd, err := m.BeginDeploymentSpeaker(ctx, rf, speaker, revision)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := m.RecordDeploymentSpeaker(ctx, rf, rd, "applied", "agent_reported", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
w := request(tenant, recovery.ID)
|
||||
var report struct {
|
||||
Progress []struct {
|
||||
State string
|
||||
JobID string `json:"job_id"`
|
||||
} `json:"deployment_progress"`
|
||||
}
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &report); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w.Code != http.StatusOK || len(report.Progress) != 1 || report.Progress[0].JobID != recovery.ID || report.Progress[0].State != "applied" {
|
||||
t.Fatalf("recovery own progress hidden: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
if w := request("other-tenant", j.ID); w.Code != http.StatusNotFound {
|
||||
t.Fatalf("cross-tenant report: %d", w.Code)
|
||||
}
|
||||
foreign, _, err := r.EnqueueContext(ctx, "other-tenant", jobs.KindDeployApply, nil, nil, map[string]any{"source_deployment_job_id": j.ID})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if w := request("other-tenant", foreign.ID); w.Code != http.StatusNotFound {
|
||||
t.Fatalf("cross-tenant source: %d %s", w.Code, w.Body.String())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user