Merge branch 'feat/47-G-fix9-catalog' into feat/47-G-join9
This commit is contained in:
@@ -52,6 +52,9 @@ func (w processWiring) Drop(owner, state string) error { return w.r.Drop(owner,
|
||||
// ReaderWiring wires Branches on a bundle's reader.
|
||||
func ReaderWiring(r *views.Reader) Wiring { return readerWiring{r} }
|
||||
|
||||
// Rows adds the list to the reader's `<module>_first_views`.
|
||||
func (w readerWiring) Rows(name string, rows func() any) { w.r.Rows(name, rows) }
|
||||
|
||||
// ProcessWiring wires Branches on a process's reader.
|
||||
func ProcessWiring(r *views.ProcessReader) Wiring { return processWiring{r} }
|
||||
|
||||
@@ -185,6 +188,11 @@ func NewBranches(w Wiring, call Call) (*Branches, error) {
|
||||
if err := w.Adopt(b.source.Follower, delivery.Seat, delivery.EventSourceMoved, b.source.Hear); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if rw, ok := w.(interface {
|
||||
Rows(name string, rows func() any)
|
||||
}); ok { // the entries by fingerprint are said in the part's `<module>_first_views` (design 47 G A-103)
|
||||
rw.Rows("entries", func() any { return b.EntryStatuses() })
|
||||
}
|
||||
return b, nil
|
||||
}
|
||||
|
||||
@@ -386,6 +394,8 @@ type Entries struct {
|
||||
// Changed, when not nil, is told of each entry that was answered or found missing, so the part starts a new pass at
|
||||
// once, or writes the finding `entry-not-found`.
|
||||
Changed func(row EntryRow)
|
||||
// Released, when not nil, is told of each entry removed by Release.
|
||||
Released func(fingerprint string)
|
||||
// Now is the clock, time.Now when nil.
|
||||
Now func() time.Time
|
||||
|
||||
@@ -442,6 +452,9 @@ func (e *Entries) Release(fingerprint string) {
|
||||
e.mu.Lock()
|
||||
delete(e.rows, fingerprint)
|
||||
e.mu.Unlock()
|
||||
if e.Released != nil {
|
||||
e.Released(fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
// Rows lists the entries kept, by fingerprint (the part's tool `<module>_first_views` lists them with their state and
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
package meshcatalogclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// Ready is nil once every watched branch has its first view (`none-yet` counting as one), else says what it waits for
|
||||
// (design 47 G §614, A-103): a part acts on nothing until then.
|
||||
func (b *Branches) Ready() error {
|
||||
followed := b.Followed()
|
||||
if len(followed) == 0 {
|
||||
return fmt.Errorf("the catalog half names no watched branch yet (the registrations of the seat %s are asked until they answer)", seat.Seat)
|
||||
}
|
||||
var waiting []string
|
||||
for _, branch := range followed {
|
||||
if c, ok := b.Catalog(branch); ok {
|
||||
select {
|
||||
case <-c.Ready():
|
||||
default:
|
||||
waiting = append(waiting, branch)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(waiting) > 0 {
|
||||
return fmt.Errorf("the catalog half has no first view of %s yet", strings.Join(waiting, ", "))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Entry is a module's entry at the trunk: the module's own registration decides, so the branch followed first, which lists
|
||||
// the module, is the one read (design 47 G A-103). An error says a watched branch has no first view yet, never "no module".
|
||||
func (b *Branches) Entry(module string) (seat.CatalogEntry, bool, error) {
|
||||
if err := b.Ready(); err != nil {
|
||||
return seat.CatalogEntry{}, false, err
|
||||
}
|
||||
for _, branch := range b.Followed() {
|
||||
if c, ok := b.Catalog(branch); ok {
|
||||
if e, ok := c.Entry(module); ok {
|
||||
return e, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return seat.CatalogEntry{}, false, nil
|
||||
}
|
||||
|
||||
// EntryStatus is one entry kept by fingerprint as a part's `<module>_first_views` says it: its state and since when.
|
||||
type EntryStatus struct {
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
State EntryState `json:"state"`
|
||||
Since time.Time `json:"since"`
|
||||
Detail string `json:"detail,omitempty"`
|
||||
}
|
||||
|
||||
// EntryStatuses are the entries kept by fingerprint, as `<module>_first_views` lists them (design 47 G §614, A-103).
|
||||
func (b *Branches) EntryStatuses() []EntryStatus {
|
||||
rows := b.Entries.Rows()
|
||||
out := make([]EntryStatus, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
out = append(out, EntryStatus{Fingerprint: r.Fingerprint, State: r.State, Since: r.Since, Detail: r.Detail})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// WriteEntryFindings makes the part write the finding `entry-not-found` through w for each entry the holder answered it does not
|
||||
// hold, and clear it when a later ask answers it or the part releases the entry (design 47 G §614, A-15, A-103). then, when not
|
||||
// nil, is told of each change after, so the part starts a new pass. The kind `entry-not-found` is one of w's kinds.
|
||||
func (b *Branches) WriteEntryFindings(w views.WireWriter, then func(EntryRow)) {
|
||||
b.Entries.Changed = func(row EntryRow) {
|
||||
switch row.State {
|
||||
case EntryNotFound:
|
||||
if err := views.WriteEntryNotFound(w, row.Fingerprint, row.Since, time.Now()); err != nil && b.Logf != nil {
|
||||
b.Logf("the finding entry-not-found of %s is not kept: %v", row.Fingerprint, err)
|
||||
}
|
||||
case EntryAnswered:
|
||||
_ = views.ClearEntryNotFound(w, row.Fingerprint)
|
||||
}
|
||||
if then != nil {
|
||||
then(row)
|
||||
}
|
||||
}
|
||||
b.Entries.Released = func(fingerprint string) { _ = views.ClearEntryNotFound(w, fingerprint) }
|
||||
}
|
||||
|
||||
// EntryAgainTool is the part's tool `<module>_entry_again` (design 47 G A-103; class act): it asks one entry by fingerprint
|
||||
// again now, as a hand act recorded first through handAct (what, why, cause), and answers where the entry stands.
|
||||
func (b *Branches) EntryAgainTool(module string, handAct func(what, why, cause string) error) stdio.Tool {
|
||||
name := module + "_entry_again"
|
||||
return stdio.Tool{Name: name,
|
||||
Description: "Ask the catalog's entry of one build, by its fingerprint, again now, whatever its state: a hand act, for an entry the part holds as asked or not found. Answers where it stands after. (a)",
|
||||
Input: map[string]any{"type": "object", "required": []string{"fingerprint", "why"}, "properties": map[string]any{
|
||||
"fingerprint": map[string]any{"type": "string"}, "why": map[string]any{"type": "string"}}},
|
||||
Run: func(a map[string]any) (any, error) {
|
||||
fingerprint, _ := a["fingerprint"].(string)
|
||||
why, _ := a["why"].(string)
|
||||
if fingerprint = strings.TrimSpace(fingerprint); fingerprint == "" {
|
||||
return nil, fmt.Errorf("name the fingerprint of the entry")
|
||||
}
|
||||
if strings.TrimSpace(why) == "" {
|
||||
return nil, fmt.Errorf("say why: a hand act keeps its why")
|
||||
}
|
||||
if err := handAct(name+" "+fingerprint, strings.TrimSpace(why), name); err != nil {
|
||||
return nil, fmt.Errorf("the hand-act log does not hold it, so nothing is asked: %w", err)
|
||||
}
|
||||
if err := b.Entries.Again(context.Background(), fingerprint); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return map[string]any{"entries": b.EntryStatuses()}, nil
|
||||
}}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package meshcatalogclient
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
seat "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-sdk/go/keys"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
|
||||
// wireFindings is a part's finding writer in memory: the findings by `<kind>/<id>` it holds.
|
||||
type wireFindings struct {
|
||||
mu sync.Mutex
|
||||
held map[string]bool
|
||||
}
|
||||
|
||||
func (w *wireFindings) WriteWire(_ context.Context, f map[string]any) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
w.held[f["kind"].(string)+"/"+f["id"].(string)] = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *wireFindings) Clear(_ context.Context, _, id, kind, _ string) error {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
delete(w.held, kind+"/"+id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *wireFindings) ModuleNode() (string, string) { return "part", "n1" }
|
||||
|
||||
func (w *wireFindings) has(key string) bool {
|
||||
w.mu.Lock()
|
||||
defer w.mu.Unlock()
|
||||
return w.held[key]
|
||||
}
|
||||
|
||||
// A-103 (design 47 G §614): the helpers every part uses over the branch follower: Ready and Entry before a first view, the
|
||||
// entries by fingerprint as `first_views` rows, the finding entry-not-found written and cleared, and `<module>_entry_again`.
|
||||
func TestTheBranchFollowerGivesEveryPartItsEntryHelpers(t *testing.T) {
|
||||
seats := &fakeSeats{entries: map[string]seat.CatalogEntry{"fp:a": {Module: "m", Fp: "fp:a"}}}
|
||||
b, err := NewBranches(&fakeWiring{adopted: map[string]*views.Follower{}, hears: map[string]func(context.Context, []byte) error{}}, seats.call)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := b.Ready(); err == nil {
|
||||
t.Error("Ready said ready with no watched branch")
|
||||
}
|
||||
if _, _, err := b.Entry("m"); err == nil {
|
||||
t.Error("Entry answered with no first view: that is an error, never no module")
|
||||
}
|
||||
|
||||
told := make(chan EntryRow, 4)
|
||||
w := &wireFindings{held: map[string]bool{}}
|
||||
b.WriteEntryFindings(w, func(r EntryRow) { told <- r })
|
||||
b.Entries.Need(context.Background(), "fp:none")
|
||||
<-told
|
||||
if !w.has(views.EntryNotFoundKind + "/" + keys.EscapeName("fp:none")) {
|
||||
t.Errorf("entry-not-found not written: %v", w.held)
|
||||
}
|
||||
if st := b.EntryStatuses(); len(st) != 1 || st[0].State != EntryNotFound {
|
||||
t.Errorf("entry statuses = %+v", st)
|
||||
}
|
||||
seats.mu.Lock()
|
||||
seats.entries["fp:none"] = seat.CatalogEntry{Module: "late", Fp: "fp:none"}
|
||||
seats.mu.Unlock()
|
||||
var acts []string
|
||||
tool := b.EntryAgainTool("part", func(what, why, cause string) error { acts = append(acts, what+"|"+why+"|"+cause); return nil })
|
||||
if tool.Name != "part_entry_again" {
|
||||
t.Errorf("tool name %s", tool.Name)
|
||||
}
|
||||
if _, err := tool.Run(map[string]any{"fingerprint": "fp:none"}); err == nil {
|
||||
t.Error("entry_again ran without a why")
|
||||
}
|
||||
if _, err := tool.Run(map[string]any{"fingerprint": "fp:none", "why": "late"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(acts) != 1 || w.has(views.EntryNotFoundKind+"/"+keys.EscapeName("fp:none")) {
|
||||
t.Errorf("hand acts %v, findings %v", acts, w.held)
|
||||
}
|
||||
select {
|
||||
case <-told:
|
||||
case <-time.After(5 * time.Second):
|
||||
t.Error("the part was not told of the answer")
|
||||
}
|
||||
b.Entries.Need(context.Background(), "fp:gone")
|
||||
<-told
|
||||
b.Entries.Release("fp:gone")
|
||||
if w.has(views.EntryNotFoundKind + "/" + keys.EscapeName("fp:gone")) {
|
||||
t.Error("a released entry kept its finding")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package nodebuildimage
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The seat's `build` acting rule matches an artifact by its kind, an image having no language (design 47 G §324, §397).
|
||||
func TestTheSeatsBuildRuleActsOnTheKindImage(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../../seats/node-build-image/seat.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var def struct {
|
||||
Acts []struct{ Step, On, Bound string } `json:"acts"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &def); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(def.Acts) != 1 || def.Acts[0].Step != "build" || def.Acts[0].On != "kind:image" {
|
||||
t.Errorf("acts = %+v; wanted the one rule {build, kind:image}", def.Acts)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,13 @@
|
||||
"decision": "novox/hq ADR 0525, ADR 0536, ADR 0553",
|
||||
"pulled-by-kind": true,
|
||||
"yields": true,
|
||||
"acts": [
|
||||
{
|
||||
"step": "build",
|
||||
"on": "kind:image",
|
||||
"bound": "60m"
|
||||
}
|
||||
],
|
||||
"accepts": [
|
||||
{
|
||||
"name": "delivery",
|
||||
|
||||
Reference in New Issue
Block a user