fix(server): cache health probes, surface SSE marshal errors, fail closed on missing duties
Health checks reuse a two-second snapshot and bound the schema query. Task SSE writes an error frame when an event cannot be encoded. ResolveDocTask errors when the frozen plan has no duty row, while explicit none-duties stay zero. Drop the unwired semantic-summary replace env, record the intentional dev versus release model split, document the test/main cutover path, and archive pre-goose SQL. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
# Pre-goose SQL archive
|
||||
|
||||
These files predate Goose and are not applied by env-service or any script.
|
||||
Live schema is deploy/postgres/goose/. Kept for history only.
|
||||
@@ -41,6 +41,7 @@ services:
|
||||
DATA_DIR: /app/data/workspaces
|
||||
MINIMAX_ENDPOINT_URL: ${MINIMAX_ENDPOINT_URL:-https://api.minimaxi.com/v1}
|
||||
MINIMAX_API_KEY: ${MINIMAX_API_KEY:-}
|
||||
# dev iterates on highspeed; test/main pin MiniMax-M3. Do not align without a promotion decision.
|
||||
MINIMAX_MODEL_ID: ${MINIMAX_MODEL_ID:-MiniMax-M2.7-highspeed}
|
||||
MINIMAX_API_TYPE: ${MINIMAX_API_TYPE:-openai-compatible}
|
||||
MINIMAX_PROVIDER: minimax
|
||||
@@ -114,9 +115,8 @@ services:
|
||||
MULTI_INTENT_U10_MUTATE: ${MULTI_INTENT_U10_MUTATE:-false}
|
||||
MULTI_INTENT_U11_DOCGOV: ${MULTI_INTENT_U11_DOCGOV:-false}
|
||||
MULTI_INTENT_U12_RUN_CONTROL: ${MULTI_INTENT_U12_RUN_CONTROL:-false}
|
||||
# W2 PU6: shadow semantic summary; REPLACE stays unwired even if env is true
|
||||
# W2 PU6: shadow semantic summary only. REPLACE stays unwired and is not declared here.
|
||||
SEMANTIC_SUMMARY_SHADOW: ${SEMANTIC_SUMMARY_SHADOW:-false}
|
||||
SEMANTIC_SUMMARY_REPLACE: ${SEMANTIC_SUMMARY_REPLACE:-false}
|
||||
PI_CHANGE_REPORT_NARRATIVE: ${PI_CHANGE_REPORT_NARRATIVE:-false}
|
||||
# FAV U7: keep degraded read + in-process publisher off until separate approval
|
||||
FAV_DEGRADED_READ: ${FAV_DEGRADED_READ:-false}
|
||||
|
||||
@@ -41,6 +41,7 @@ services:
|
||||
DATA_DIR: /app/data/workspaces
|
||||
MINIMAX_ENDPOINT_URL: ${MINIMAX_ENDPOINT_URL:-https://api.minimaxi.com/v1}
|
||||
MINIMAX_API_KEY: ${MINIMAX_API_KEY:-}
|
||||
# Release pin. dev uses MiniMax-M2.7-highspeed on purpose.
|
||||
MINIMAX_MODEL_ID: ${MINIMAX_MODEL_ID:-MiniMax-M3}
|
||||
MINIMAX_API_TYPE: ${MINIMAX_API_TYPE:-openai-compatible}
|
||||
MINIMAX_PROVIDER: minimax
|
||||
|
||||
@@ -41,6 +41,7 @@ services:
|
||||
DATA_DIR: /app/data/workspaces
|
||||
MINIMAX_ENDPOINT_URL: ${MINIMAX_ENDPOINT_URL:-https://api.minimaxi.com/v1}
|
||||
MINIMAX_API_KEY: ${MINIMAX_API_KEY:-}
|
||||
# Release pin. dev uses MiniMax-M2.7-highspeed on purpose.
|
||||
MINIMAX_MODEL_ID: ${MINIMAX_MODEL_ID:-MiniMax-M3}
|
||||
MINIMAX_API_TYPE: ${MINIMAX_API_TYPE:-openai-compatible}
|
||||
MINIMAX_PROVIDER: minimax
|
||||
|
||||
@@ -12,6 +12,12 @@ import (
|
||||
|
||||
const OpsCutoverEnvironment = "dev"
|
||||
|
||||
// OpsCutoverPromotionPath is how test and main change consumer modes.
|
||||
// ApplyOpsCutover stays dev-only. After a dev observation window, copy the
|
||||
// reviewed mode with an authorized promotion receipt (SQL or migration), first
|
||||
// on test, then on main. Do not widen ValidateOpsCutover to accept those envs.
|
||||
const OpsCutoverPromotionPath = "dev-observe -> promotion-receipt on test -> same receipt on main; ApplyOpsCutover rejects non-dev"
|
||||
|
||||
// ValidateOpsCutover is the shared-dev gate for cluster mode changes.
|
||||
// test/main, FAV, and unknown consumers are rejected before any write.
|
||||
func ValidateOpsCutover(environment, consumer, mode string) error {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package events
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestValidateOpsCutoverRejectsFavAndNonDev(t *testing.T) {
|
||||
if err := ValidateOpsCutover("dev", ConsumerNotify, ModeShadow); err != nil {
|
||||
@@ -12,6 +15,9 @@ func TestValidateOpsCutoverRejectsFavAndNonDev(t *testing.T) {
|
||||
if err := ValidateOpsCutover("main", ConsumerNotify, ModeLive); err == nil {
|
||||
t.Fatal("main env must fail")
|
||||
}
|
||||
if !strings.Contains(OpsCutoverPromotionPath, "test") || !strings.Contains(OpsCutoverPromotionPath, "main") {
|
||||
t.Fatalf("promotion path must name test and main: %s", OpsCutoverPromotionPath)
|
||||
}
|
||||
if err := ValidateOpsCutover("dev", ConsumerFAV, ModeOff); err == nil {
|
||||
t.Fatal("fav must fail even at off")
|
||||
}
|
||||
|
||||
@@ -86,27 +86,47 @@ type TaskRunRequestDTO struct {
|
||||
}
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
resp, code := s.buildHealthResponse(false)
|
||||
resp, code := s.buildHealthResponse(r.Context(), false)
|
||||
writeJSONResponse(w, code, resp)
|
||||
}
|
||||
|
||||
func (s *Server) handleReadiness(w http.ResponseWriter, r *http.Request) {
|
||||
resp, code := s.buildHealthResponse(true)
|
||||
resp, code := s.buildHealthResponse(r.Context(), true)
|
||||
writeJSONResponse(w, code, resp)
|
||||
}
|
||||
|
||||
func (s *Server) buildHealthResponse(strict bool) (HealthResponse, int) {
|
||||
type healthSnapshot struct {
|
||||
at time.Time
|
||||
strict bool
|
||||
resp HealthResponse
|
||||
code int
|
||||
ok bool
|
||||
}
|
||||
|
||||
func (s *Server) buildHealthResponse(ctx context.Context, strict bool) (HealthResponse, int) {
|
||||
const healthCacheTTL = 2 * time.Second
|
||||
s.healthMu.Lock()
|
||||
if s.healthCache.ok && s.healthCache.strict == strict && time.Since(s.healthCache.at) < healthCacheTTL {
|
||||
resp, code := s.healthCache.resp, s.healthCache.code
|
||||
s.healthMu.Unlock()
|
||||
return resp, code
|
||||
}
|
||||
s.healthMu.Unlock()
|
||||
|
||||
dbCtx, cancel := context.WithTimeout(ctx, 2*time.Second)
|
||||
defer cancel()
|
||||
|
||||
dbReady := s.svc != nil && s.svc.DatabaseReady()
|
||||
schemaVersion := int64(0)
|
||||
if s.svc != nil {
|
||||
if dbReady {
|
||||
schemaVersion = s.svc.LiveSchemaVersion(context.Background())
|
||||
schemaVersion = s.svc.LiveSchemaVersion(dbCtx)
|
||||
} else {
|
||||
schemaVersion = s.svc.SchemaVersion()
|
||||
}
|
||||
}
|
||||
schemaReady := schemaVersion > 0 || (s.cfg != nil && s.cfg.AllowInMemory)
|
||||
taskStoreReady := taskrun.TablesReady(context.Background(), s.pool)
|
||||
taskStoreReady := taskrun.TablesReady(dbCtx, s.pool)
|
||||
|
||||
status := "ok"
|
||||
code := http.StatusOK
|
||||
@@ -122,7 +142,7 @@ func (s *Server) buildHealthResponse(strict bool) (HealthResponse, int) {
|
||||
|
||||
kbReady := s.kb != nil && s.kb.Online()
|
||||
|
||||
return HealthResponse{
|
||||
resp := HealthResponse{
|
||||
Status: status,
|
||||
Env: s.cfg.Env,
|
||||
AgentBackend: s.cfg.AgentBackend,
|
||||
@@ -133,7 +153,11 @@ func (s *Server) buildHealthResponse(strict bool) (HealthResponse, int) {
|
||||
SchemaVersion: schemaVersion,
|
||||
SchemaReady: schemaReady,
|
||||
TaskStoreReady: taskStoreReady,
|
||||
}, code
|
||||
}
|
||||
s.healthMu.Lock()
|
||||
s.healthCache = healthSnapshot{at: time.Now(), strict: strict, resp: resp, code: code, ok: true}
|
||||
s.healthMu.Unlock()
|
||||
return resp, code
|
||||
}
|
||||
|
||||
func (s *Server) handleGetProject(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -80,6 +80,8 @@ type Server struct {
|
||||
vaultStop chan struct{}
|
||||
reminderStop chan struct{}
|
||||
loginLimiter *loginRateLimiter
|
||||
healthMu sync.Mutex
|
||||
healthCache healthSnapshot
|
||||
}
|
||||
|
||||
// NewFromRuntime attaches the HTTP adapter to an already-wired composition root.
|
||||
|
||||
@@ -433,7 +433,13 @@ func (s *Server) handleTaskRunEvents(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
writeStoredEvents := func(events []taskrun.StoredEvent) error {
|
||||
for _, stored := range events {
|
||||
data, _ := json.Marshal(stored.Event)
|
||||
data, err := json.Marshal(stored.Event)
|
||||
if err != nil {
|
||||
if _, werr := fmt.Fprintf(w, "event: error\ndata: {\"error\":\"event_marshal_failed\",\"seq\":%d}\n\n", stored.Seq); werr != nil {
|
||||
return werr
|
||||
}
|
||||
return fmt.Errorf("marshal stored event seq %d: %w", stored.Seq, err)
|
||||
}
|
||||
if _, err := fmt.Fprintf(w, "id: %d\n", stored.Seq); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -66,6 +66,7 @@ func (o *Orchestrator) ResolveDocTask(ctx context.Context, projectID, docTaskIns
|
||||
res.TemplateVersion = def.TemplateVersion
|
||||
}
|
||||
}
|
||||
matchedDuty := false
|
||||
for _, stPlan := range plan.Stages {
|
||||
if stPlan.StageKey != st.StageKey {
|
||||
continue
|
||||
@@ -77,8 +78,12 @@ func (o *Orchestrator) ResolveDocTask(ctx context.Context, projectID, docTaskIns
|
||||
res.AuthorUserID = dtPlan.Duties.AuthorUserID
|
||||
res.ReviewerUserID = dtPlan.Duties.ReviewerUserID
|
||||
res.ApproverUserID = dtPlan.Duties.ApproverUserID
|
||||
matchedDuty = true
|
||||
}
|
||||
}
|
||||
if !matchedDuty {
|
||||
return nil, fmt.Errorf("orchestrator: frozen plan has no duties for stage %s task %s", st.StageKey, dt.TaskKey)
|
||||
}
|
||||
return res, nil
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user