Keep assignments in the assignments bucket and move the rows there at start (issue 1093)

Once chain_moved records the kind, Assign, Unassign and Assigned write and read the keys <node>.<module>, and every join of the store that read assignments is made from the bucket: where a module runs, what is behind, what a machine awaits, a given value due for replacement, taking a module, and whether a module may be forgotten (refused while a standing assignment names it). An unassignment ends the key with a marker and keeps its earlier values, as it removed the row; no ended or pending mark is written yet. The generation rises with each change, and a seat held by an assignment goes with it: the foreign key from seat_holding is dropped by migration 0104 and the controller says both. The provisioner's plan is kept in the values by a pass of the tick that writes that one field by compare-and-set; the assignment-changed event is said from what the bucket holds, whoever wrote it. The start moves the rows with what each machine was last sent, keeping a plan the mirror put in a value.
This commit is contained in:
2026-10-18 03:15:44 +02:00
parent 275ddd76e8
commit 0e4feb0d3a
24 changed files with 1138 additions and 162 deletions
+89
View File
@@ -2,12 +2,15 @@ package main
import (
"context"
"encoding/json"
"fmt"
"sort"
"time"
"github.com/nats-io/nats.go"
"github.com/nats-io/nats.go/jetstream"
"github.com/novox/mesh-controller/internal/broker"
"github.com/novox/mesh-controller/internal/inventory"
"github.com/novox/mesh-controller/internal/recordmirror"
)
@@ -49,9 +52,17 @@ func mirroringRecords(ctx context.Context, inv *inventory.Inventory, conn *nats.
failing := map[string]string{}
tick := time.NewTicker(mirrorEvery)
defer tick.Stop()
events := &assignmentEvents{}
for {
if holds() {
// The assignments, once they are keys of the bus, are written by the verbs that change them (novox/hq ADR
// 0408): this tick keeps the provisioner's plan in their values and says what changed; the mirror of the store
// writes them only until the move.
onTheBus := keepingAssignments(ctx, inv, announce, epoch, events, failing)
for _, m := range recordmirror.Mirrors() {
if m.Name == recordmirror.Assignments.Name && onTheBus {
continue
}
why := mirrorOnce(ctx, api, m, writers, sources[m.Name], announce, epoch, holds)
if why != failing[m.Name] {
if why != "" {
@@ -104,3 +115,81 @@ func mirrorOnce(ctx context.Context, api jetstream.JetStream, m recordmirror.Mir
}
return ""
}
// assignmentEvents is the assignments as the bucket held them at the last look, to say what changed since.
type assignmentEvents struct {
known map[string]inventory.KeptValue
}
// keepingAssignments is the tick's work for the assignments once they are on the bus: the provisioner's plan written into
// the values it belongs to, and one `assignment-changed` event for each key that is new, changed or ended since the last
// look — whoever wrote it, a verb in this process or a command in another. False when they are not on the bus yet.
func keepingAssignments(ctx context.Context, inv *inventory.Inventory, announce recordmirror.Announcer, epoch func() uint64,
events *assignmentEvents, failing map[string]string) bool {
reading, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel()
const name = "assignments"
if _, on, err := inv.KeptAssignments(reading); err != nil || !on {
if err != nil {
// Not known to be off the bus: the store's mirror is not written over what the bus may hold.
if failing[name] == "" {
failing[name] = err.Error()
fmt.Printf("the assignments cannot be read from the bus: %v\n", err)
}
return true
}
return false
}
if _, _, err := inv.KeepAssignmentPlans(reading); err != nil {
if failing[name] != err.Error() {
failing[name] = err.Error()
fmt.Printf("the provisioner's plans are not kept in the assignments: %v\n", err)
}
} else if failing[name] != "" {
fmt.Printf("the provisioner's plans are kept in the assignments again\n")
failing[name] = ""
}
now, _, err := inv.KeptAssignments(reading)
if err != nil {
return true
}
if events.known != nil {
keys := make([]string, 0, len(now)+len(events.known))
for key := range now {
keys = append(keys, key)
}
for key := range events.known {
if _, still := now[key]; !still {
keys = append(keys, key)
}
}
sort.Strings(keys)
for _, key := range keys {
was, had := events.known[key]
is, has := now[key]
e := recordmirror.Event{Mirror: recordmirror.Assignments.Name, Key: key, Revision: is.Revision, Epoch: epoch(),
At: time.Now().UTC()}
switch {
case has && !had:
e.Change = recordmirror.Added
case has && had && was.Revision != is.Revision:
e.Change, e.Fields = recordmirror.Changed, recordmirror.ChangedFields(was.Value, is.Value)
case !has && had:
e.Change, e.Revision = recordmirror.Removed, 0
default:
continue
}
body, err := json.Marshal(e)
if err != nil {
continue
}
for try := 0; try < 2; try++ {
if err = announce.PublishSeatEvent(reading, broker.ControllerSeat, recordmirror.Assignments.Event, body); err == nil {
break
}
}
}
}
events.known = now
return true
}