diff --git a/modules/minio/cmd/object-store/main.go b/modules/minio/cmd/object-store/main.go index c789f03c..3ff38861 100644 --- a/modules/minio/cmd/object-store/main.go +++ b/modules/minio/cmd/object-store/main.go @@ -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", diff --git a/modules/minio/cmd/object-store/manifest_test.go b/modules/minio/cmd/object-store/manifest_test.go index 58494c7f..acf2d470 100644 --- a/modules/minio/cmd/object-store/manifest_test.go +++ b/modules/minio/cmd/object-store/manifest_test.go @@ -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) { diff --git a/modules/minio/cmd/object-store/server.go b/modules/minio/cmd/object-store/server.go index 789873b8..14684beb 100644 --- a/modules/minio/cmd/object-store/server.go +++ b/modules/minio/cmd/object-store/server.go @@ -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 } diff --git a/modules/minio/cmd/object-store/server_test.go b/modules/minio/cmd/object-store/server_test.go index ba7bc956..53e72b61 100644 --- a/modules/minio/cmd/object-store/server_test.go +++ b/modules/minio/cmd/object-store/server_test.go @@ -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) } } diff --git a/modules/minio/module.json b/modules/minio/module.json index 60b40a4b..b52fba28 100644 --- a/modules/minio/module.json +++ b/modules/minio/module.json @@ -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": { diff --git a/modules/minio/object-store b/modules/minio/object-store new file mode 100755 index 00000000..94048c5f Binary files /dev/null and b/modules/minio/object-store differ