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>
177 lines
4.9 KiB
Go
177 lines
4.9 KiB
Go
package events
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"chat-agent-system/internal/app/uow"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
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 {
|
|
if strings.TrimSpace(environment) != OpsCutoverEnvironment {
|
|
return fmt.Errorf("%w: environment %q", ErrOpsCutover, environment)
|
|
}
|
|
switch consumer {
|
|
case ConsumerNotify, ConsumerWorkflowAdvance, ConsumerProjection:
|
|
default:
|
|
return fmt.Errorf("%w: consumer %q", ErrOpsCutover, consumer)
|
|
}
|
|
switch mode {
|
|
case ModeOff, ModeShadow, ModeLive:
|
|
default:
|
|
return fmt.Errorf("%w: mode %q", ErrOpsCutover, mode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ApplyOpsCutover(ctx context.Context, pool *pgxpool.Pool, environment, consumer, mode string) error {
|
|
if err := ValidateOpsCutover(environment, consumer, mode); err != nil {
|
|
return err
|
|
}
|
|
return SetMode(ctx, pool, environment, consumer, mode)
|
|
}
|
|
|
|
type ModeSnapshot struct {
|
|
Environment string `json:"environment"`
|
|
Consumer string `json:"consumer"`
|
|
Mode string `json:"mode"`
|
|
Revision int64 `json:"revision"`
|
|
}
|
|
|
|
type BacklogRow struct {
|
|
Consumer string `json:"consumer"`
|
|
Status string `json:"status"`
|
|
Count int64 `json:"count"`
|
|
}
|
|
|
|
func ListModes(ctx context.Context, pool *pgxpool.Pool) ([]ModeSnapshot, error) {
|
|
rows, err := pool.Query(ctx, `
|
|
SELECT environment, consumer, mode, revision
|
|
FROM event_consumer_modes
|
|
ORDER BY environment, consumer
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []ModeSnapshot
|
|
for rows.Next() {
|
|
var m ModeSnapshot
|
|
if err := rows.Scan(&m.Environment, &m.Consumer, &m.Mode, &m.Revision); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, m)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func ListBacklog(ctx context.Context, pool *pgxpool.Pool) ([]BacklogRow, error) {
|
|
rows, err := pool.Query(ctx, `
|
|
SELECT consumer, status, COUNT(*)
|
|
FROM event_deliveries
|
|
GROUP BY consumer, status
|
|
ORDER BY consumer, status
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []BacklogRow
|
|
for rows.Next() {
|
|
var r BacklogRow
|
|
if err := rows.Scan(&r.Consumer, &r.Status, &r.Count); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
type pendingDelivery struct {
|
|
EventID string
|
|
Consumer string
|
|
EventName string
|
|
Environment string
|
|
ProjectID int64
|
|
SchemaVersion int
|
|
PayloadDigest string
|
|
}
|
|
|
|
func loadPending(ctx context.Context, pool *pgxpool.Pool) ([]pendingDelivery, error) {
|
|
rows, err := pool.Query(ctx, `
|
|
SELECT d.event_id, d.consumer, e.event_name, e.environment, e.project_id, e.schema_version, COALESCE(e.payload_digest, '')
|
|
FROM event_deliveries d
|
|
INNER JOIN event_envelopes e ON e.event_id = d.event_id
|
|
WHERE d.status IN ('pending','leased')
|
|
`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []pendingDelivery
|
|
for rows.Next() {
|
|
var p pendingDelivery
|
|
if err := rows.Scan(&p.EventID, &p.Consumer, &p.EventName, &p.Environment, &p.ProjectID, &p.SchemaVersion, &p.PayloadDigest); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, p)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
func ClassifyPending(ctx context.Context, pool *pgxpool.Pool) ([]InventoryRow, error) {
|
|
pending, err := loadPending(ctx, pool)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make([]InventoryRow, 0, len(pending))
|
|
for _, p := range pending {
|
|
env := Envelope{
|
|
EventID: p.EventID, EventName: p.EventName, SchemaVersion: p.SchemaVersion,
|
|
Environment: p.Environment, ProjectID: p.ProjectID, PayloadDigest: p.PayloadDigest,
|
|
}
|
|
out = append(out, ClassifyEnvelope(env, p.Consumer))
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// QuarantineIllegalPending persists quarantine for pending deliveries whose
|
|
// consumer is not on the contract (including FAV). Safe envelopes are left untouched.
|
|
func QuarantineIllegalPending(ctx context.Context, pool *pgxpool.Pool, operator, approval string) (int, error) {
|
|
if operator == "" || approval == "" {
|
|
return 0, ErrIntegrityFailure
|
|
}
|
|
rows, err := ClassifyPending(ctx, pool)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
n := 0
|
|
unit := uow.NewPG(pool)
|
|
for _, row := range rows {
|
|
if row.Classification != ClassificationQuarantine {
|
|
continue
|
|
}
|
|
err := unit.Within(ctx, func(scope uow.TxScope) error {
|
|
return PersistInventory(uow.WithScope(ctx, scope), row, operator, approval)
|
|
})
|
|
if err != nil {
|
|
return n, err
|
|
}
|
|
n++
|
|
}
|
|
return n, nil
|
|
}
|