1090/1092: the move tests pick each report by kind and expect three, all already, on the second run. 1090-1093: under the share lock the switch reads the rows again and writes every key whose value differs from its row (and ends an assignment the store dropped) before recording the kind, so an update the old controller made after the first pass is not lost. 1093: route-proxy is pinned at mesh-sdk f185aedb and vendored; the controller uses only the SDK's asks package. KeepAssignmentPlans decides each key by keepStep, which never makes or revives an assignment. AssignmentsMirrorToWrite refuses once the assignments are moved.
652 lines
22 KiB
Go
652 lines
22 KiB
Go
package inventory
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/novox/mesh-controller/internal/broker"
|
|
"github.com/novox/mesh-controller/internal/chain"
|
|
)
|
|
|
|
// **The move** (novox/hq ADR 0408 §8, issue 981 note 7 Q2): the serving controller writes the rows of a kind into its
|
|
// bucket at its start, holding the lease and after the buckets exist, and records the kind in chain_moved. Idempotent: a
|
|
// key that exists is already moved and is never overwritten, so a bucket value written since is not laid over by the row
|
|
// it came from. It counts the rows against the keys, logs the largest value, and **refuses** (a Refusal, which the
|
|
// controller raises as an urgent condition) on a count that differs or a value over the bus's largest, with nothing
|
|
// switched: a kind is recorded moved only once all of it is in the bucket.
|
|
|
|
// Refusal is a move that stopped without switching a kind, and why. It is permanent until the cause is put right; any
|
|
// other error of a move (the bus away, a write not granted) is tried again.
|
|
type Refusal struct {
|
|
Kind, Why string
|
|
}
|
|
|
|
func (r *Refusal) Error() string {
|
|
return fmt.Sprintf("the move of %s to the bus is refused: %s", r.Kind, r.Why)
|
|
}
|
|
|
|
// MoveReport is what a move of one kind did.
|
|
type MoveReport struct {
|
|
Kind string
|
|
// Rows is how many records the tables held, Written how many keys this move made, and Present how many already
|
|
// held a value (moved by an earlier start).
|
|
Rows, Written, Present int
|
|
// TableRows is how many rows of the tables those records came from: a request and its outcome are two rows and one
|
|
// record.
|
|
TableRows int
|
|
// Largest is the biggest value, in bytes.
|
|
Largest int
|
|
// Already is a kind an earlier start moved: nothing was done.
|
|
Already bool
|
|
}
|
|
|
|
// mover is one kind of the chain: its rows, read from its tables, as the keys and values they become.
|
|
type mover struct {
|
|
kind string
|
|
// rows reads the tables: each record's key and its value, whole, and how many rows of the tables they came from.
|
|
rows func(ctx context.Context) ([]moveRow, int, error)
|
|
// create writes one record under its key; chain.ErrExists when the key holds a value.
|
|
create func(ctx context.Context, c *chain.Chain, row moveRow) error
|
|
// keys are the keys the bucket holds.
|
|
keys func(ctx context.Context, c *chain.Chain) ([]string, error)
|
|
// count is how many rows the tables hold now, counted as rows reports them.
|
|
count func(ctx context.Context, tx pgx.Tx) (int, error)
|
|
// lock keeps the tables from taking a row while the switch is recorded.
|
|
lock string
|
|
// settle runs under that lock, with the rows read again: it makes the bucket hold what the rows say now. A row the old
|
|
// controller changed after the first pass is in the bucket before the kind is recorded as moved, for before the switch
|
|
// the table is the only truth.
|
|
settle func(ctx context.Context, c *chain.Chain, rows []moveRow) error
|
|
}
|
|
|
|
type moveRow struct {
|
|
key string
|
|
value any
|
|
size int
|
|
}
|
|
|
|
func (i *Inventory) movers() []mover {
|
|
return []mover{{
|
|
kind: KindBuilds,
|
|
rows: func(ctx context.Context) ([]moveRow, int, error) {
|
|
builds, err := i.allBuildRows(ctx)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
requests, err := i.allRequestRows(ctx)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
// A build request is the build's record in state asked, and the request stays beside the outcome that
|
|
// answered it: one record under one id (issue 1091).
|
|
byID := make(map[string]*chain.Build, len(builds))
|
|
var order []string
|
|
for _, b := range builds {
|
|
v := toChainBuild(b)
|
|
byID[b.ID] = &v
|
|
order = append(order, b.ID)
|
|
}
|
|
for _, r := range requests {
|
|
if v, answered := byID[r.ID]; answered {
|
|
v.Request = toRequest(r)
|
|
continue
|
|
}
|
|
v := askedRecord(r)
|
|
if r.NotAsked != "" {
|
|
// Never handed over: asked again once, after the move (askAgain).
|
|
v.Request.Reask = chain.ReaskPending
|
|
}
|
|
byID[r.ID] = &v
|
|
order = append(order, r.ID)
|
|
}
|
|
out := make([]moveRow, 0, len(order))
|
|
for _, id := range order {
|
|
body, err := json.Marshal(byID[id])
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
out = append(out, moveRow{key: chainBuildKey(id), value: *byID[id], size: len(body)})
|
|
}
|
|
return out, len(builds) + len(requests), nil
|
|
},
|
|
create: func(ctx context.Context, c *chain.Chain, row moveRow) error {
|
|
_, err := c.Builds.Create(ctx, row.key, row.value.(chain.Build))
|
|
return err
|
|
},
|
|
keys: func(ctx context.Context, c *chain.Chain) ([]string, error) { return c.Builds.Keys(ctx) },
|
|
count: func(ctx context.Context, tx pgx.Tx) (int, error) {
|
|
var n int
|
|
err := tx.QueryRow(ctx, `select (select count(*) from build) + (select count(*) from build_request)`).Scan(&n)
|
|
return n, err
|
|
},
|
|
lock: `lock table build, build_request in share mode`,
|
|
settle: func(ctx context.Context, c *chain.Chain, rows []moveRow) error {
|
|
return settleBucket(ctx, c.Builds, rows)
|
|
},
|
|
}, {
|
|
kind: KindRollouts,
|
|
rows: func(ctx context.Context) ([]moveRow, int, error) {
|
|
plans, err := i.plans(ctx, `order by created`)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
merges, err := i.merges(ctx, ``)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
tablePlans := len(plans)
|
|
if plans, err = holdMerges(plans, merges); err != nil {
|
|
return nil, 0, err
|
|
}
|
|
out := make([]moveRow, 0, len(plans))
|
|
for _, p := range plans {
|
|
body, err := json.Marshal(p)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
out = append(out, moveRow{key: p.ID, value: p, size: len(body)})
|
|
}
|
|
return out, tablePlans + len(merges), nil
|
|
},
|
|
create: func(ctx context.Context, c *chain.Chain, row moveRow) error {
|
|
_, err := chain.RolloutsOf[Plan](c).Create(ctx, row.key, row.value.(Plan))
|
|
return err
|
|
},
|
|
keys: func(ctx context.Context, c *chain.Chain) ([]string, error) {
|
|
return chain.RolloutsOf[Plan](c).Keys(ctx)
|
|
},
|
|
count: func(ctx context.Context, tx pgx.Tx) (int, error) {
|
|
var n int
|
|
err := tx.QueryRow(ctx, `select (select count(*) from release_plan) + (select count(*) from batched_merge)`).Scan(&n)
|
|
return n, err
|
|
},
|
|
lock: `lock table release_plan, batched_merge in share mode`,
|
|
settle: func(ctx context.Context, c *chain.Chain, rows []moveRow) error {
|
|
return settleBucket(ctx, chain.RolloutsOf[Plan](c), rows)
|
|
},
|
|
}, {
|
|
kind: KindAssignments,
|
|
rows: func(ctx context.Context) ([]moveRow, int, error) {
|
|
assigned, err := i.assignedWithSends(ctx)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
out := make([]moveRow, 0, len(assigned))
|
|
for _, a := range assigned {
|
|
body, err := json.Marshal(a)
|
|
if err != nil {
|
|
return nil, 0, err
|
|
}
|
|
out = append(out, moveRow{key: assignmentKey(a.Node, a.Module), value: a, size: len(body)})
|
|
}
|
|
return out, len(out), nil
|
|
},
|
|
create: createAssignment,
|
|
keys: func(ctx context.Context, c *chain.Chain) ([]string, error) { return c.Assignments.Keys(ctx) },
|
|
count: func(ctx context.Context, tx pgx.Tx) (int, error) {
|
|
var n int
|
|
err := tx.QueryRow(ctx, `select count(*) from assignment`).Scan(&n)
|
|
return n, err
|
|
},
|
|
lock: `lock table assignment in share mode`,
|
|
settle: func(ctx context.Context, c *chain.Chain, rows []moveRow) error {
|
|
have := make(map[string]bool, len(rows))
|
|
for _, r := range rows {
|
|
have[r.key] = true
|
|
if err := settleAssignment(ctx, c, r); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// An assignment the store no longer holds ended since the first pass: the bucket says so.
|
|
all, err := c.Assignments.All(ctx)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for key, k := range all {
|
|
if have[key] || k.Value.Assigned.IsZero() || !k.Value.Stands() {
|
|
continue
|
|
}
|
|
k.Value.Ended = time.Now().UTC()
|
|
if _, err := c.Assignments.Update(ctx, key, k.Value, k.Revision); err != nil && !errors.Is(err, chain.ErrMoved) {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
},
|
|
}}
|
|
}
|
|
|
|
// assignedWithSends is every assignment of the store as the chain keeps it: with the commit its machine was last sent for
|
|
// the module (node.sent_builds), the build that is of it and what the build published, and since when (when the machine was
|
|
// last sent). An assignment of a module the machine was never sent has none of those.
|
|
func (i *Inventory) assignedWithSends(ctx context.Context) ([]chain.Assignment, error) {
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select n.name, a.module, a.assigned, n.sent_at, n.sent_builds
|
|
from assignment a join node n on n.id = a.node order by n.name, a.module`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []chain.Assignment
|
|
for rows.Next() {
|
|
var a chain.Assignment
|
|
var sentAt *time.Time
|
|
var raw []byte
|
|
if err := rows.Scan(&a.Node, &a.Module, &a.Assigned, &sentAt, &raw); err != nil {
|
|
rows.Close()
|
|
return nil, err
|
|
}
|
|
a.Assigned = a.Assigned.UTC()
|
|
var sent map[string]string
|
|
if len(raw) > 0 {
|
|
_ = json.Unmarshal(raw, &sent)
|
|
}
|
|
if commit := sent[a.Module]; commit != "" && sentAt != nil {
|
|
a.Commit, a.Since = commit, sentAt.UTC()
|
|
}
|
|
out = append(out, a)
|
|
}
|
|
rows.Close()
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
// The build of each commit, read once: the newest successful build of the module at it.
|
|
type at struct{ module, commit string }
|
|
var all []Build
|
|
if _, moved, err := i.ChainReads(ctx, KindBuilds); err != nil {
|
|
return nil, err
|
|
} else if moved {
|
|
if all, err = chainBuilds(ctx, i.attached()); err != nil {
|
|
return nil, err
|
|
}
|
|
} else if all, err = i.allBuildRows(ctx); err != nil {
|
|
return nil, err
|
|
}
|
|
newest := map[at]Build{}
|
|
for _, b := range all {
|
|
k := at{b.Module, b.Commit}
|
|
if b.Failed != "" || b.Module == "" {
|
|
continue
|
|
}
|
|
if had, has := newest[k]; !has || b.At.After(had.At) || (b.At.Equal(had.At) && b.ID > had.ID) {
|
|
newest[k] = b
|
|
}
|
|
}
|
|
for k := range out {
|
|
if out[k].Commit == "" {
|
|
continue
|
|
}
|
|
b, found := newest[at{out[k].Module, out[k].Commit}]
|
|
if !found {
|
|
continue
|
|
}
|
|
out[k].Build = b.ID
|
|
if published := publishedOf(b.Made); published != nil {
|
|
out[k].Artifact, out[k].Package = published.Artifact, published.Package
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// holdMerges puts each batched merge into the record of the plan that answers it. A merge no plan answers — the plan_id
|
|
// of a merge a failed walk carried is null, or names a plan the store never kept — is held, flagged waiting, by the
|
|
// newest plan that lists it among its merges, else the record kept for the merges no plan answers: the record that held it
|
|
// last.
|
|
func holdMerges(plans []Plan, merges []BatchedMerge) ([]Plan, error) {
|
|
waitingAt := -1
|
|
at := make(map[string]int, len(plans))
|
|
for k, p := range plans {
|
|
at[p.ID] = k
|
|
}
|
|
newest := func(match func(Plan) bool) int {
|
|
best := -1
|
|
for k, p := range plans {
|
|
if match(p) && (best < 0 || p.Created.After(plans[best].Created)) {
|
|
best = k
|
|
}
|
|
}
|
|
return best
|
|
}
|
|
for _, m := range merges {
|
|
held := heldOf(m, m.Heard)
|
|
holder, answered := at[m.Plan]
|
|
if m.Plan == "" || !answered {
|
|
held.Waiting = true
|
|
holder = newest(func(p Plan) bool {
|
|
if p.Delivery == nil {
|
|
return false
|
|
}
|
|
for _, listed := range p.Delivery.Merges {
|
|
if strings.EqualFold(listed.Repository, m.Repository) && listed.Commit == m.Commit {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
})
|
|
if holder < 0 {
|
|
// Held by the record kept for the merges no plan answers, made with the others.
|
|
if waitingAt < 0 {
|
|
plans = append(plans, Plan{ID: WaitingRecord, State: "merges", Created: time.Unix(0, 0).UTC(), Revision: 1,
|
|
Tiers: [][]string{}, Modules: map[string]*PlanModule{}, Note: "the merges no plan answers"})
|
|
waitingAt = len(plans) - 1
|
|
}
|
|
holder = waitingAt
|
|
}
|
|
}
|
|
plans[holder].Held = append(plans[holder].Held, held)
|
|
}
|
|
return plans, nil
|
|
}
|
|
|
|
// MoveChain moves every kind not moved yet, in order, and says what it did. It needs the chain attached and the lease
|
|
// held (the chain's writes ask). It stops at the first kind that fails.
|
|
func (i *Inventory) MoveChain(ctx context.Context, say func(format string, args ...any)) ([]MoveReport, error) {
|
|
if say == nil {
|
|
say = func(string, ...any) {}
|
|
}
|
|
if i.attached() == nil {
|
|
return nil, ErrNoChain
|
|
}
|
|
var out []MoveReport
|
|
for _, m := range i.movers() {
|
|
r, err := i.moveKind(ctx, m, say)
|
|
if err != nil {
|
|
return out, err
|
|
}
|
|
out = append(out, r)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// movePasses is how often a move reads the tables again when they took a row while it wrote: the controller before this
|
|
// one may still have been writing as the lease changed hands.
|
|
const movePasses = 5
|
|
|
|
func (i *Inventory) moveKind(ctx context.Context, m mover, say func(string, ...any)) (MoveReport, error) {
|
|
report := MoveReport{Kind: m.kind}
|
|
moved, err := i.ChainMoved(ctx)
|
|
if err != nil {
|
|
return report, err
|
|
}
|
|
if _, done := moved[m.kind]; done {
|
|
report.Already = true
|
|
return report, nil
|
|
}
|
|
for pass := 1; pass <= movePasses; pass++ {
|
|
rows, tableRows, err := m.rows(ctx)
|
|
if err != nil {
|
|
return report, fmt.Errorf("the %s cannot be read from the tables: %w", m.kind, err)
|
|
}
|
|
report.Rows, report.TableRows, report.Largest = len(rows), tableRows, 0
|
|
// Every value measured before any is written: a value over the bus's largest stops the move with nothing
|
|
// written (ADR 0408 §6), never cut.
|
|
for _, r := range rows {
|
|
report.Largest = max(report.Largest, r.size)
|
|
if r.size > broker.ChainValueBytes {
|
|
return report, &Refusal{m.kind, fmt.Sprintf("the value of %s is %d bytes and the bus holds at most %d; "+
|
|
"nothing is cut, so the record must be made smaller first", r.key, r.size, broker.ChainValueBytes)}
|
|
}
|
|
}
|
|
report.Written, report.Present = 0, 0
|
|
for _, r := range rows {
|
|
switch err := m.create(ctx, i.attached(), r); {
|
|
case err == nil:
|
|
report.Written++
|
|
case errors.Is(err, chain.ErrExists):
|
|
report.Present++
|
|
case errors.Is(err, chain.ErrBadKey):
|
|
return report, &Refusal{m.kind, fmt.Sprintf("%s cannot be a key of the bus: %v", r.key, err)}
|
|
default:
|
|
return report, fmt.Errorf("%s %s is not written to the bus: %w", m.kind, r.key, err)
|
|
}
|
|
}
|
|
// Rows against keys: every row's key is in the bucket.
|
|
keys, err := m.keys(ctx, i.attached())
|
|
if err != nil {
|
|
return report, fmt.Errorf("the keys of the %s cannot be read back: %w", m.kind, err)
|
|
}
|
|
have := make(map[string]bool, len(keys))
|
|
for _, k := range keys {
|
|
have[k] = true
|
|
}
|
|
for _, r := range rows {
|
|
if !have[r.key] {
|
|
return report, &Refusal{m.kind, fmt.Sprintf("the tables hold %d records and the bucket does not hold %s", len(rows), r.key)}
|
|
}
|
|
}
|
|
settled, err := i.switchKind(ctx, m, report)
|
|
if err != nil {
|
|
return report, err
|
|
}
|
|
if settled {
|
|
say("moved the %s to the bus: %d records, %d written now and %d already there, the largest %d bytes",
|
|
m.kind, report.Rows, report.Written, report.Present, report.Largest)
|
|
return report, nil
|
|
}
|
|
say("the %s took a row while they were moved, so they are read again (pass %d of %d)", m.kind, pass, movePasses)
|
|
}
|
|
return report, &Refusal{m.kind, fmt.Sprintf("the tables kept taking rows through %d passes; another controller is still writing them", movePasses)}
|
|
}
|
|
|
|
// switchKind records the kind as moved, in one transaction that holds the tables against writes and finds their count
|
|
// as the move read it: false when they took a row since.
|
|
func (i *Inventory) switchKind(ctx context.Context, m mover, r MoveReport) (bool, error) {
|
|
tx, err := i.store.Pool().Begin(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
defer func() { _ = tx.Rollback(ctx) }()
|
|
if _, err := tx.Exec(ctx, m.lock); err != nil {
|
|
return false, err
|
|
}
|
|
n, err := m.count(ctx, tx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if n != r.TableRows {
|
|
return false, nil
|
|
}
|
|
// The rows read again under the lock: nothing writes them now, so what the bucket holds is made what they say.
|
|
rows, tableRows, err := m.rows(ctx)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if tableRows != r.TableRows {
|
|
return false, nil
|
|
}
|
|
if err := m.settle(ctx, i.attached(), rows); err != nil {
|
|
return false, fmt.Errorf("the %s changed since they were first written, and the bucket cannot be brought to them: %w", m.kind, err)
|
|
}
|
|
counts, _ := json.Marshal(map[string]int{m.kind: r.Rows})
|
|
largest, _ := json.Marshal(map[string]int{m.kind: r.Largest})
|
|
if _, err := tx.Exec(ctx,
|
|
`insert into chain_moved (counts, largest) values ($1, $2)
|
|
on conflict (only_one) do update
|
|
set counts = chain_moved.counts || excluded.counts, largest = chain_moved.largest || excluded.largest,
|
|
moved_at = now()`, counts, largest); err != nil {
|
|
return false, err
|
|
}
|
|
if err := tx.Commit(ctx); err != nil {
|
|
return false, err
|
|
}
|
|
i.moved.mu.Lock()
|
|
if i.moved.kind == nil {
|
|
i.moved.kind = map[string]bool{}
|
|
}
|
|
i.moved.kind[m.kind] = true
|
|
i.moved.mu.Unlock()
|
|
return true, nil
|
|
}
|
|
|
|
// allBuildRows is every build of the table, whole.
|
|
func (i *Inventory) allBuildRows(ctx context.Context) ([]Build, error) {
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select id, repository, ref, coalesce(module, ''), commit_hash, built_on, failed, made, source_path, manifest,
|
|
built_against, built_contexts, build_sources, source_fingerprint, asked, at
|
|
from build order by at, id`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []Build
|
|
for rows.Next() {
|
|
var b Build
|
|
var made, manifest, against, read, sources []byte
|
|
var asked *time.Time
|
|
if err := rows.Scan(&b.ID, &b.Repository, &b.Ref, &b.Module, &b.Commit, &b.On, &b.Failed, &made, &b.Path,
|
|
&manifest, &against, &read, &sources, &b.SourceFingerprint, &asked, &b.At); err != nil {
|
|
return nil, err
|
|
}
|
|
if asked != nil {
|
|
b.Asked = *asked
|
|
}
|
|
b.Manifest = manifest
|
|
for _, part := range []struct {
|
|
raw []byte
|
|
to any
|
|
who string
|
|
}{{made, &b.Made, "made"}, {against, &b.Against, "built_against"}, {read, &b.Read, "built_contexts"},
|
|
{sources, &b.Sources, "build_sources"}} {
|
|
if len(part.raw) == 0 {
|
|
continue
|
|
}
|
|
if err := json.Unmarshal(part.raw, part.to); err != nil {
|
|
return nil, fmt.Errorf("build %s: %s is not readable: %w", b.ID, part.who, err)
|
|
}
|
|
}
|
|
out = append(out, b)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// allRequestRows is every build request of the table.
|
|
func (i *Inventory) allRequestRows(ctx context.Context) ([]BuildRequest, error) {
|
|
rows, err := i.store.Pool().Query(ctx,
|
|
`select id, repository, seat, source_path, ref, commit_hash, asked_for, coalesce(not_asked, ''),
|
|
coalesce(outcome_unknown, ''), asked_at, at_terminal
|
|
from build_request order by asked_at, id`)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var out []BuildRequest
|
|
for rows.Next() {
|
|
var a BuildRequest
|
|
if err := rows.Scan(&a.ID, &a.Repository, &a.Seat, &a.Path, &a.Ref, &a.Commit, &a.For, &a.NotAsked,
|
|
&a.OutcomeUnknown, &a.At, &a.AtTerminal); err != nil {
|
|
return nil, err
|
|
}
|
|
out = append(out, a)
|
|
}
|
|
return out, rows.Err()
|
|
}
|
|
|
|
// createAssignment writes an assignment's row under its key; chain.ErrExists when the key holds a value that stays.
|
|
func createAssignment(ctx context.Context, c *chain.Chain, row moveRow) error {
|
|
want := row.value.(chain.Assignment)
|
|
// The mirror of the store kept a value under this key until now, with the provisioner's plan beside it: the
|
|
// plan stays, the record's links are laid in. A value that names its build already is a later word, and stays.
|
|
for try := 0; try < casTries; try++ {
|
|
cur, found, err := c.Assignments.Get(ctx, row.key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found {
|
|
if _, err := c.Assignments.Create(ctx, row.key, want); !errors.Is(err, chain.ErrExists) {
|
|
return err
|
|
}
|
|
continue
|
|
}
|
|
if cur.Value.Build != "" || cur.Value.Commit != "" || !cur.Value.Since.IsZero() || (want.Build == "" && want.Commit == "") {
|
|
return chain.ErrExists
|
|
}
|
|
merged := want
|
|
merged.RequiredBy, merged.Provisioning = cur.Value.RequiredBy, cur.Value.Provisioning
|
|
if _, err := c.Assignments.Update(ctx, row.key, merged, cur.Revision); errors.Is(err, chain.ErrMoved) {
|
|
continue
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
return fmt.Errorf("the assignment %s kept moving under the write", row.key)
|
|
}
|
|
|
|
// settleBucket makes a bucket of whole values hold each row's value, by compare-and-set: a key that is absent is made, one
|
|
// whose bytes differ from the row's is written over, and one that is the same is left.
|
|
func settleBucket[T any](ctx context.Context, b chain.Bucket[T], rows []moveRow) error {
|
|
for _, r := range rows {
|
|
want, err := json.Marshal(r.value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
settled := false
|
|
for try := 0; try < casTries && !settled; try++ {
|
|
e, found, err := b.GetRaw(ctx, r.key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
switch {
|
|
case !found:
|
|
_, err = b.CreateRaw(ctx, r.key, want)
|
|
if errors.Is(err, chain.ErrExists) {
|
|
continue
|
|
}
|
|
case bytes.Equal(e.Value, want):
|
|
settled = true
|
|
continue
|
|
default:
|
|
_, err = b.UpdateRaw(ctx, r.key, want, e.Revision)
|
|
if errors.Is(err, chain.ErrMoved) {
|
|
continue
|
|
}
|
|
}
|
|
if err != nil {
|
|
return err
|
|
}
|
|
settled = true
|
|
}
|
|
if !settled {
|
|
return fmt.Errorf("%s kept moving under the write", r.key)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// settleAssignment makes the value under an assignment's key what its row says, keeping the plan the mirror put beside it:
|
|
// before the switch the store is the only truth of the links, so a commit it changed since the first pass is written over.
|
|
func settleAssignment(ctx context.Context, c *chain.Chain, row moveRow) error {
|
|
want := row.value.(chain.Assignment)
|
|
for try := 0; try < casTries; try++ {
|
|
cur, found, err := c.Assignments.Get(ctx, row.key)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !found {
|
|
if _, err := c.Assignments.Create(ctx, row.key, want); errors.Is(err, chain.ErrExists) {
|
|
continue
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
merged := want
|
|
merged.RequiredBy, merged.Provisioning = cur.Value.RequiredBy, cur.Value.Provisioning
|
|
a, _ := json.Marshal(merged)
|
|
b, _ := json.Marshal(cur.Value)
|
|
if bytes.Equal(a, b) {
|
|
return nil
|
|
}
|
|
if _, err := c.Assignments.Update(ctx, row.key, merged, cur.Revision); errors.Is(err, chain.ErrMoved) {
|
|
continue
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
return fmt.Errorf("the assignment %s kept moving under the write", row.key)
|
|
}
|