CAT-RD8: minio asks the assigner's seat-holders once and follows its events, no reads, no cache on a timer

This commit is contained in:
2026-11-04 12:06:34 +01:00
parent 40e1e087e5
commit 3e3294cd47
6 changed files with 41 additions and 68 deletions
+13 -2
View File
@@ -10,10 +10,14 @@
package main
import (
"context"
"fmt"
"os"
stdio "git.novox.be/novox/mesh-sdk/go"
"git.novox.be/novox/mesh-sdk/go/delivery"
"git.novox.be/novox/mesh-delivery/sdk/views"
)
// Words the manifest gives this bundle (modules/minio/module.json).
@@ -24,17 +28,24 @@ const (
)
func main() {
s, err := newServer(os.Getenv(ConfigWord), os.Getenv(LinksWord), os.Getenv(ReceivesWord))
seats := &views.Reader{Module: "minio", Logf: func(f string, a ...any) { fmt.Fprintf(os.Stderr, "[object-store] "+f+"\n", a...) }}
seats.Follow(delivery.AssignerModule, delivery.SeatHoldersState, SeatHoldersTool)
s, err := newServer(os.Getenv(ConfigWord), os.Getenv(LinksWord), os.Getenv(ReceivesWord),
func() (map[string][]byte, error) { return seats.Values(delivery.SeatHoldersRead) })
if err != nil {
fmt.Fprintf(os.Stderr, "[object-store] %v\n", err)
os.Exit(1)
}
if err := stdio.Serve("", tools(s)); err != nil {
seats.Start(context.Background())
if err := stdio.Serve("", append(tools(s), seats.Tools("object_store")...)); err != nil {
fmt.Fprintf(os.Stderr, "[object-store] %v\n", err)
os.Exit(1)
}
}
// SeatHoldersTool is the assigner's read tool of who holds which seat, which the manifest invokes (ADR 0615).
const SeatHoldersTool = delivery.AssignerModule + ".assigner_read_seat_holders"
func tools(s *server) []stdio.Tool {
return []stdio.Tool{{
Name: "object-store.link",
@@ -38,9 +38,10 @@ func TestManifestClaimsTheSeat(t *testing.T) {
if c["name"] != "object-store" || c["scope"] != "mesh" || len(serves) != 1 || serves[0] != "link" {
t.Fatalf("claim: %v", c)
}
reads, _ := m["reads"].([]any)
if _, invokes := m["invokes"]; invokes || len(reads) != 1 || reads[0] != "mesh-delivery-assigner.seat-holders" {
t.Fatalf("reads %v and invokes %v: it reads the placement seat's holders and calls nothing", reads, m["invokes"])
invokes, _ := m["invokes"].([]any)
consumes, _ := m["consumes"].([]any)
if _, reads := m["reads"]; reads || len(invokes) != 1 || invokes[0] != SeatHoldersTool || len(consumes) != 1 || consumes[0] != "mesh-delivery-assigner.views.seat-holders" {
t.Fatalf("reads %v, invokes %v, consumes %v: it asks the assigner's seat-holders once and follows its events (ADR 0615)", m["reads"], invokes, consumes)
}
var goBundle map[string]any
for _, a := range m["build"].(map[string]any)["artifacts"].([]any) {
+9 -39
View File
@@ -9,7 +9,6 @@ import (
"os"
"path/filepath"
"strings"
"sync"
"time"
stdio "git.novox.be/novox/mesh-sdk/go"
@@ -21,7 +20,6 @@ import (
const (
DefaultSeconds = 300
MaxSeconds = 600
holdersFor = 60 * time.Second
)
// storeConfig is the store as this machine reaches it. The file is rendered by the mesh from files/object-store.json
@@ -60,41 +58,18 @@ func linkKeyID(bucket string) string {
type server struct {
configPath, linksDir, receives string
now func() time.Time
// seats reads the placement seat's state of who holds which seat, every key; a test replaces it.
// seats is the picture of the placement seat's state of who holds which seat, every key: asked of the assigner once
// and followed by its events (ADR 0615); a test replaces it.
seats func() (map[string][]byte, error)
mu sync.Mutex
keys map[string][]byte // seat-holders, as read
readAt time.Time
haveSet bool
}
func newServer(config, links, receives string) (*server, error) {
func newServer(config, links, receives string, seats func() (map[string][]byte, error)) (*server, error) {
for word, v := range map[string]string{ConfigWord: config, LinksWord: links, ReceivesWord: receives} {
if strings.TrimSpace(v) == "" {
return nil, fmt.Errorf("%s is not given: the manifest names it", word)
}
}
return &server{configPath: config, linksDir: links, receives: receives, now: time.Now, seats: askSeats}, nil
}
func askSeats() (map[string][]byte, error) {
state := stdio.State(delivery.SeatHoldersRead)
names, err := state.Keys()
if err != nil {
return nil, err
}
keys := map[string][]byte{}
for _, k := range names {
e, err := state.Get(k)
if err != nil {
return nil, err
}
if e != nil {
keys[k] = e.Value
}
}
return keys, nil
return &server{configPath: config, linksDir: links, receives: receives, now: time.Now, seats: seats}, nil
}
// link answers one ask.
@@ -266,18 +241,13 @@ func has(list []string, v string) bool {
}
// holds says whether node.module holds the seat, from the placement seat's state of who holds which seat (novox/hq ADR
// 0571 clause 4), read at most once a minute. A read that failed is not cached: the next ask reads again.
// 0571 clause 4), from the picture the assigner's first view and events keep. No first view yet is refused, saying so.
func (s *server) holds(seat, node, module string) (bool, error) {
s.mu.Lock()
defer s.mu.Unlock()
if !s.haveSet || s.now().Sub(s.readAt) >= holdersFor {
keys, err := s.seats()
if err != nil {
return false, err
}
s.keys, s.readAt, s.haveSet = keys, s.now(), true
keys, err := s.seats()
if err != nil {
return false, err
}
holders, err := delivery.HoldersOf(s.keys, seat, node)
holders, err := delivery.HoldersOf(keys, seat, node)
if err != nil {
return false, err
}
+10 -22
View File
@@ -59,7 +59,7 @@ func newRig(t *testing.T) *rig {
write("links/mesh-shanks-brother.secret", "brother-link-secret\n")
write("links/mesh-novox-nextcloud.secret", "nextcloud-link-secret\n")
grants := write("grants/mesh.json", grantFile)
s, err := newServer(cfg, filepath.Join(dir, "links"), grants)
s, err := newServer(cfg, filepath.Join(dir, "links"), grants, nil)
if err != nil {
t.Fatal(err)
}
@@ -165,36 +165,24 @@ func TestRefusals(t *testing.T) {
}
}
// 1516/AC4: with the controller unreadable a seat share is refused naming why; a good read serves a second ask within
// 60 s from the cache and is read again after.
func TestSeatsUnreadableAndCached(t *testing.T) {
// 1516/AC4 and ADR 0615: with no first view of the seat-holders yet a seat share is refused naming why; once it is
// answered every ask is served from the picture, with no read of the owner's state and none on a timer.
func TestSeatsUnreadableAndFollowed(t *testing.T) {
r := newRig(t)
r.seatsErr = errors.New("the controller did not answer")
r.seatsErr = errors.New("no first view of mesh-delivery-assigner.seat-holders yet")
_, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "put", nil))
if err == nil || !strings.Contains(err.Error(), "the controller did not answer") || !strings.Contains(err.Error(), "node-files") {
if err == nil || !strings.Contains(err.Error(), "no first view") || !strings.Contains(err.Error(), "node-files") {
t.Fatalf("not refused naming why: %v", err)
}
r.seatsErr = nil
put := func() {
t.Helper()
for range 3 {
if _, err := r.s.link("g14.mesh-files", ask("mesh-shanks-brother", "put", nil)); err != nil {
t.Fatal(err)
}
r.now = r.now.Add(10 * time.Minute) // time changes nothing: the picture is current by its events
}
put() // a failed read is not cached: this reads again
put() // served from the cache
if r.seatsCalls != 2 {
t.Fatalf("the controller was asked %d times, want 2 (one failure, one good read)", r.seatsCalls)
}
r.now = r.now.Add(59 * time.Second)
put()
if r.seatsCalls != 2 {
t.Fatalf("an ask within 60 s read the seats again (%d calls)", r.seatsCalls)
}
r.now = r.now.Add(2 * time.Second)
put()
if r.seatsCalls != 3 {
t.Fatalf("the seats were not read again after 60 s (%d calls)", r.seatsCalls)
if r.seatsCalls != 4 {
t.Fatalf("the picture was asked %d times, want 4 (one refused, three served)", r.seatsCalls)
}
}
+5 -2
View File
@@ -39,8 +39,11 @@
]
}
],
"reads": [
"mesh-delivery-assigner.seat-holders"
"invokes": [
"mesh-delivery-assigner.assigner_read_seat_holders"
],
"consumes": [
"mesh-delivery-assigner.views.seat-holders"
],
"contributes": {
"route": {
BIN
View File
Binary file not shown.