package inventory import ( "context" "encoding/json" "errors" "fmt" "time" "github.com/jackc/pgx/v5" "github.com/novox/mesh-controller/internal/catalogue" ) // The gate's verdicts (novox/hq ADR 0235, to-be 45 §8): what a build did on its first machine, and, // for one that failed there, how it was put back. One row per build, written by the plan that rolled it // out, under the lease. // The gate's verdicts and a failed build's rollback. const ( GatePassed = "passed" GateFailed = "failed" RollingBack = "rolling-back" RolledBack = "rolled-back" NotRolledBack = "not-rolled-back" ) // GateVerdict is one build's verdict at its gate. type GateVerdict struct { Build string Module string Commit string Previous string Plan string Machines []string Verdict string Rollback string Why string Component string // JudgingFrom is when the first machine reported the build applied and the judging began. JudgingFrom *time.Time JudgedAt time.Time Epoch uint64 } // RecordGate writes a build's verdict. **A failed build's row is written once**: a second failure for // the same build is refused with ErrGateKept, which is what keeps a rollback to one per build — the row // is written before the rollback's send, and a controller replaced in between finds it. func (i *Inventory) RecordGate(ctx context.Context, v GateVerdict) error { epoch, err := i.actingEpoch(ctx) if err != nil { return fmt.Errorf("the gate's verdict on %s is not written: %w", v.Build, err) } if v.Machines == nil { v.Machines = []string{} } tag, err := i.store.Pool().Exec(ctx, `insert into build_gate (build, module, commit_hash, previous, plan, machines, verdict, rollback, why, component, judging_from, judged_at, epoch) values ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, now(), $12) on conflict (build) do update set verdict = excluded.verdict, rollback = excluded.rollback, why = excluded.why, previous = excluded.previous, machines = excluded.machines, judged_at = now(), epoch = excluded.epoch where build_gate.verdict = 'passed' and excluded.verdict = 'passed'`, v.Build, v.Module, v.Commit, v.Previous, v.Plan, v.Machines, v.Verdict, v.Rollback, v.Why, v.Component, v.JudgingFrom, epoch) if err != nil { return err } if tag.RowsAffected() == 0 { return fmt.Errorf("%w: %s", ErrGateKept, v.Build) } return nil } // ErrGateKept is a verdict already kept for the build, which is not written over. var ErrGateKept = errors.New("this build's verdict at its gate is already kept") // SetRollback records how a failed build's rollback went. func (i *Inventory) SetRollback(ctx context.Context, build, rollback, why string) error { if _, err := i.actingEpoch(ctx); err != nil { return err } _, err := i.store.Pool().Exec(ctx, `update build_gate set rollback = $2, why = $3, judged_at = now() where build = $1 and verdict = 'failed'`, build, rollback, why) return err } // GateOf is a build's verdict, and whether it has one. func (i *Inventory) GateOf(ctx context.Context, build string) (GateVerdict, bool, error) { rows, err := i.store.Pool().Query(ctx, gateSelect+` where build = $1`, build) if err != nil { return GateVerdict{}, false, err } list, err := scanGates(rows) if err != nil || len(list) == 0 { return GateVerdict{}, false, err } return list[0], true, nil } // GateFailed is whether a build failed its gate: one the mesh never registers or sends again on its own. func (i *Inventory) GateFailed(ctx context.Context, build string) (bool, error) { v, found, err := i.GateOf(ctx, build) return found && v.Verdict == GateFailed, err } // LatestGates is the newest verdict of every module that has one: what the gate probe (DG) reads. func (i *Inventory) LatestGates(ctx context.Context) ([]GateVerdict, error) { rows, err := i.store.Pool().Query(ctx, `select distinct on (module) build, module, commit_hash, previous, plan, machines, verdict, rollback, why, component, judging_from, judged_at, coalesce(epoch, 0) from build_gate order by module, judged_at desc`) if err != nil { return nil, err } return scanGates(rows) } // Gates is the newest verdicts, newest first: what `plans gates` lists. func (i *Inventory) Gates(ctx context.Context, limit int) ([]GateVerdict, error) { rows, err := i.store.Pool().Query(ctx, gateSelect+` order by judged_at desc limit $1`, limit) if err != nil { return nil, err } return scanGates(rows) } const gateSelect = `select build, module, commit_hash, previous, plan, machines, verdict, rollback, why, component, judging_from, judged_at, coalesce(epoch, 0) from build_gate` func scanGates(rows pgx.Rows) ([]GateVerdict, error) { defer rows.Close() var out []GateVerdict for rows.Next() { var v GateVerdict var epoch int64 if err := rows.Scan(&v.Build, &v.Module, &v.Commit, &v.Previous, &v.Plan, &v.Machines, &v.Verdict, &v.Rollback, &v.Why, &v.Component, &v.JudgingFrom, &v.JudgedAt, &epoch); err != nil { return nil, err } v.Epoch = uint64(epoch) out = append(out, v) } return out, rows.Err() } // PreviousBuild is the build a module goes back to when a build of it fails its gate: the newest build // that worked, made from the commit the first machine ran before, asked before the failed one, and not // itself failed at a gate. Among the builds whose artifacts the mesh keeps (KeptBuilds): an older one // may already be gone from the artifact store. False when there is none to go back to. func (i *Inventory) PreviousBuild(ctx context.Context, module, commit string, failed Build) (Build, bool, error) { builds, err := i.Builds(ctx, module, 50) if err != nil { return Build{}, false, err } kept := 0 for _, b := range builds { if !b.Worked() { continue } kept++ if kept > KeptBuilds { break } if b.ID == failed.ID || (commit != "" && b.Commit != commit) { continue } if !failed.AskedOrAt().IsZero() && !b.AskedOrAt().Before(failed.AskedOrAt()) { continue } if bad, err := i.GateFailed(ctx, b.ID); err != nil { return Build{}, false, err } else if bad { continue } var manifest []byte if err := i.store.Pool().QueryRow(ctx, `select manifest from build where id = $1`, b.ID).Scan(&manifest); err != nil { return Build{}, false, err } if len(manifest) == 0 || string(manifest) == "null" { continue } b.Manifest = manifest return b, true, nil } return Build{}, false, nil } // RestoreModule puts a module's registered build back to an earlier one: its manifest, the commit it // was built from, and when it was asked — as now, so the build that failed its gate, asked before, can // never register over it again (issue 219's order). The source's head is left where the merge moved it: // the module IS behind its source, and `status` says so. func (i *Inventory) RestoreModule(ctx context.Context, b Build) error { if _, err := i.actingEpoch(ctx); err != nil { return fmt.Errorf("%s is not put back: %w", b.Module, err) } m, err := catalogue.ParseManifest(b.Manifest) if err != nil { return fmt.Errorf("%s's build %s is not a manifest the mesh can register again: %w", b.Module, b.ID, err) } raw, err := json.Marshal(m) if err != nil { return err } tag, err := i.store.Pool().Exec(ctx, `update module set manifest = $2, version = nullif($3, ''), built_from = nullif($4, ''), built_asked = now(), registered = now() where name = $1`, b.Module, raw, m.Version, b.Commit) if err != nil { return err } if tag.RowsAffected() == 0 { return fmt.Errorf("%w: %s", ErrNoSuchModule, b.Module) } return nil }