diff --git a/modules/nats/cmd/nats-tools/requests.go b/modules/nats/cmd/nats-tools/requests.go index 3384b064e..3b4a879db 100644 --- a/modules/nats/cmd/nats-tools/requests.go +++ b/modules/nats/cmd/nats-tools/requests.go @@ -111,21 +111,47 @@ func requestTools(rs requestStore) []stdio.Tool { if err := rs.Put(r.ID, r); err != nil { return nil, fmt.Errorf("the request cannot be handed to the keeper: %w", err) } - // The keeper acts on its watch of requests at once; its end is read back for at most half a minute. - for wait := 100 * time.Millisecond; wait < 20*time.Second; wait *= 2 { - rs.Wait(wait) - if got, err := rs.Get(r.ID); err == nil && got != nil && !got.Done.IsZero() { - return got, nil - } - } - return map[string]any{"request": r.ID, "state": "handed to the keeper, not ended yet", - "read": fmt.Sprintf("nats_%s with request %s", verb, r.ID)}, nil + return awaitEnd(rs, r, "nats_"+verb), nil }} } + firstView := stdio.Tool{Name: "nats_first_view_again", + Description: "Ask the bus module's keeper to ask the indexer's indexer_bus_state, its first view of every accepted manifest's bus state, at once (design 25 § 12.2): by hand, beside its own backoff, recorded as a hand act; answers what the first view answered and its revision.", + Input: map[string]any{"why": str("why, in plain words: kept with the act"), "request": str("a request made before: its end is read")}, + Run: func(a map[string]any) (any, error) { + if id := arg(a, "request"); id != "" { + r, err := rs.Get(id) + if err != nil || r == nil { + return nil, fmt.Errorf("no request %s is held (%v)", id, err) + } + return r, nil + } + r := keeper.Request{Verb: keeper.VerbFirstViewAgain, Why: strings.TrimSpace(arg(a, "why")), Caller: stdio.CallerOf(a), At: rs.Now()} + if r.Why == "" { + return nil, fmt.Errorf("nats_first_view_again says why") + } + r.ID = requestID(r.At) + if err := rs.Put(r.ID, r); err != nil { + return nil, fmt.Errorf("the request cannot be handed to the keeper: %w", err) + } + return awaitEnd(rs, r, "nats_first_view_again"), nil + }} return []stdio.Tool{ + firstView, tool(keeper.VerbCreate, "Create one entry of a module's kept data on the bus now, as its accepted manifest asks for it (design 25 § 12.3): handed to the bus module's keeper, which alone may create, and recorded as a hand act."), tool(keeper.VerbApply, "Apply what a module's manifest asks of one entry where the bus holds less (a larger cap, a longer age): only a difference that grows; handed to the keeper and recorded as a hand act."), tool(keeper.VerbRecreate, "Rebuild one entry whose settings drifted in a way that does not grow: the operator is asked first, and the keeper acts only on their warrant, after a snapshot of the entry. Its data is lost from the bus (the snapshot keeps it). Dangerous."), tool(keeper.VerbDrop, "Remove one orphan, an entry no accepted manifest asks for: the operator is asked first, and the keeper acts only on their warrant, after a snapshot of the entry. Dangerous."), } } + +// awaitEnd reads a request's end back: the keeper acts on its watch of requests at once, so the end is read for at most +// half a minute, and otherwise how to read it later is answered. +func awaitEnd(rs requestStore, r keeper.Request, tool string) any { + for wait := 100 * time.Millisecond; wait < 20*time.Second; wait *= 2 { + rs.Wait(wait) + if got, err := rs.Get(r.ID); err == nil && got != nil && !got.Done.IsZero() { + return got + } + } + return map[string]any{"request": r.ID, "state": "handed to the keeper, not ended yet", "read": fmt.Sprintf("%s with request %s", tool, r.ID)} +} diff --git a/modules/nats/internal/keeper/keeper_test.go b/modules/nats/internal/keeper/keeper_test.go index 88eafc6f3..f26ee9539 100644 --- a/modules/nats/internal/keeper/keeper_test.go +++ b/modules/nats/internal/keeper/keeper_test.go @@ -510,6 +510,25 @@ func TestTheKeeperMakesWhatAVerdictAsksForAndSaysIt(t *testing.T) { t.Errorf("held %+v, said %v %v: rebuilt as asked, the drift cleared", bus.held["m_log"], out.events, out.conds) } }}, + // CAT-N2b: nats_first_view_again asks the first view at once, by hand, recorded as a hand act. + {"the first view asked again by hand", func(t *testing.T, k *Keeper, bus *fakeBus, out *said) { + store := k.Store.(*memStore) + var acts []string + k.Act = func(what, why, cause string) error { acts = append(acts, cause+": "+what); return nil } + k.View = func() (FirstView, error) { + return FirstView{Revision: 8, Modules: []Accepted{{Module: "m", Revision: 8, BusState: []catalogue.BusStateEntry{drafts}}}}, nil + } + raw, _ := json.Marshal(Request{ID: "r9", Verb: VerbFirstViewAgain, Why: "a verdict thought missed", Caller: "person.jo"}) + _, _ = store.Put(StateRequests, "r9", raw) + k.ActOnRequests() + raw, _, _, _ = store.Get(StateRequests, "r9") + var got Request + _ = json.Unmarshal(raw, &got) + if got.Outcome != "the first view answered at revision 8" || fmt.Sprint(bus.created) != "[KV_m_drafts]" || + len(acts) != 1 || !strings.Contains(acts[0], "nats_first_view_again: asked mesh-delivery-indexer.indexer_bus_state again by hand") { + t.Errorf("ended %+v, created %v, recorded %v", got, bus.created, acts) + } + }}, } { t.Run(row.name, func(t *testing.T) { k, bus, _, out := newKeeper(FirstView{Revision: 1}) diff --git a/modules/nats/internal/keeper/requests.go b/modules/nats/internal/keeper/requests.go index a686c6bbe..b36900026 100644 --- a/modules/nats/internal/keeper/requests.go +++ b/modules/nats/internal/keeper/requests.go @@ -5,6 +5,7 @@ import ( "encoding/json" "errors" "fmt" + "strings" "time" "git.novox.be/novox/mesh-sdk/go/asks" @@ -22,6 +23,8 @@ const ( VerbApply = "apply" VerbRecreate = "recreate" VerbDrop = "drop" + // VerbFirstViewAgain asks the indexer's first view at once, by hand (design 25 § 12.2), beside the backoff. + VerbFirstViewAgain = "first-view-again" ) // Request is one person's request, the value of its key in StateRequests. @@ -83,6 +86,15 @@ func (k *Keeper) ActOnRequests() { } func (k *Keeper) act(r *Request) (string, error) { + if r.Verb == VerbFirstViewAgain { + _, err := k.AskFirstView(0) + s := k.Status().FirstView + k.record(r, fmt.Sprintf("asked %s.%s again by hand: answered %v at revision %d", s.Owner, s.Verb, err == nil, s.Revision)) + if err != nil { + return "asked, not answered", err + } + return fmt.Sprintf("the first view answered at revision %d", s.Revision), nil + } if !k.Viewed() { return "", fmt.Errorf("%w: the keeper holds no first view yet, so it acts on nothing", ErrNoWarrant) } @@ -199,7 +211,7 @@ func (k *Keeper) record(r *Request, what string) { if k.Act == nil { return } - if err := k.Act(what+", asked by "+r.Caller, r.Why, "nats_"+r.Verb); err != nil { + if err := k.Act(what+", asked by "+r.Caller, r.Why, "nats_"+strings.ReplaceAll(r.Verb, "-", "_")); err != nil { k.logf("the hand act %q could not be recorded: %v", what, err) } } diff --git a/modules/nats/module.json b/modules/nats/module.json index cb4e5f191..b704efbe9 100644 --- a/modules/nats/module.json +++ b/modules/nats/module.json @@ -120,7 +120,8 @@ "nats_apply", "nats_recreate", "nats_drop", - "nats_keeper_health" + "nats_keeper_health", + "nats_first_view_again" ], "guards": [ 8222