Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
700 lines
25 KiB
Go
700 lines
25 KiB
Go
package approval
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/keys"
|
|
"git.novox.be/novox/mesh-sdk/go/operatorask"
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
)
|
|
|
|
// A marked verb a module's process serves is one call with a gate before its running (design 47 G §677): the holder keeps
|
|
// the call, asks under its own module's name, and on the Approve runs that same call, saying its end on the seat's event
|
|
// `gated-ended`. These are the holder's half of the gate; the caller's is process.Conn's call.
|
|
|
|
// WaitHeader is the bus header a call carries to say it waits for its end on `gated-ended` (lead answer A-244): a header,
|
|
// never an argument, so no verb's input and no act's digest changes. WaitHeld is its one value.
|
|
const (
|
|
WaitHeader = "x-wait"
|
|
WaitHeld = "held"
|
|
)
|
|
|
|
// Held is the holder's answer to a call that waits (§677): the ask it waits on and its own call's id, in the reply's field
|
|
// `held` beside result, error and node (A-244).
|
|
type Held struct {
|
|
Asked string `json:"asked"`
|
|
Call string `json:"call"`
|
|
}
|
|
|
|
// GatedEndedEvent is the seat's event a call's end is said on, kept last per subject for GatedEndedKeptFor (§677).
|
|
const (
|
|
GatedEndedEvent = seatdef.GatedEndedEvent
|
|
GatedEndedKeptFor = seatdef.GatedEndedKeptDays * 24 * time.Hour
|
|
)
|
|
|
|
// The outcomes of a held call (§677).
|
|
const (
|
|
OutcomeRan = "ran"
|
|
OutcomeDeclined = "declined"
|
|
OutcomeExpired = "expired"
|
|
OutcomeRefused = "refused"
|
|
)
|
|
|
|
// KindGateUnanswered is the holder's finding of a waiting call past its ask's 60 minutes with no decided heard (§677),
|
|
// its id the call's.
|
|
const KindGateUnanswered = "gate-unanswered"
|
|
|
|
// GatedEnded is a held call's end as the holder says it (§677): no argument is in it.
|
|
type GatedEnded struct {
|
|
Call string `json:"call"`
|
|
Ask string `json:"ask"`
|
|
Act string `json:"act"`
|
|
Verb string `json:"verb"`
|
|
Outcome string `json:"outcome"`
|
|
Answer any `json:"answer,omitempty"`
|
|
Approval string `json:"approval,omitempty"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|
|
|
|
// GatedEndedSubject is the subject one call's end is said on: `mesh.seat.<seat>.event.gated-ended.<caller>.<call>`, the
|
|
// caller the account the bus proved, escaped as a key's name is (§677, §73).
|
|
func GatedEndedSubject(seat, caller, call string) string {
|
|
return "mesh.seat." + seat + ".event." + GatedEndedEvent + "." + keys.EscapeName(caller) + "." + call
|
|
}
|
|
|
|
// WaitingCall is one call the holder keeps under `call.<id>` (§677): the call whole, the caller the bus proved, its ask,
|
|
// and once it ended its outcome, and whether that end was said.
|
|
type WaitingCall struct {
|
|
ID string `json:"id"`
|
|
Caller string `json:"caller"`
|
|
Ask string `json:"ask"`
|
|
Digest string `json:"digest"`
|
|
Act operatorask.Act `json:"act"`
|
|
Address string `json:"address"`
|
|
Seat string `json:"seat"`
|
|
Verb string `json:"verb"`
|
|
Arguments json.RawMessage `json:"arguments,omitempty"`
|
|
// Made is when the call was made; Asked when its ask (the newest, after an ask anew) was raised or taken.
|
|
Made time.Time `json:"made"`
|
|
Asked time.Time `json:"asked"`
|
|
// End is its end once it ended; Said says End was said on gated-ended.
|
|
End *GatedEnded `json:"end,omitempty"`
|
|
Said bool `json:"said,omitempty"`
|
|
}
|
|
|
|
func callKey(id string) string { return "call." + id }
|
|
func claimedKey(id string) string { return "claimed." + id }
|
|
|
|
// Serves names the handler a waiting call of seat's verb runs on its Approve (§677): process.Conn's AnswerSeat gives each
|
|
// marked verb's.
|
|
func (g *Gate) Serves(seat, verb string, h func(caller string, args json.RawMessage) (any, error)) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
if g.handlers == nil {
|
|
g.handlers = map[string]func(string, json.RawMessage) (any, error){}
|
|
}
|
|
g.handlers[seat+"."+verb] = h
|
|
}
|
|
|
|
func (g *Gate) handler(seat, verb string) func(string, json.RawMessage) (any, error) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
return g.handlers[seat+"."+verb]
|
|
}
|
|
|
|
// Hold is a call of a marked verb the holder serves (§677). A call whose arguments may not be asked, or that comes while
|
|
// the state approvals cannot be read or written, is refused at once and nothing is kept. One that waits (the header
|
|
// WaitHeader) is kept as a waiting call and answered at once with Held; one that does not takes ADR 0286 §37's path: it is
|
|
// refused naming its act's ask, and the same call made again after the Approve runs. A call of an act declined within 10
|
|
// minutes ends declined at once, unasked.
|
|
func (g *Gate) Hold(c Call, wait bool, run func() (any, error)) (any, *Held, error) {
|
|
if c.Fingerprint == "" {
|
|
c.Fingerprint = g.d.Build
|
|
}
|
|
if c.Fingerprint == "" {
|
|
return nil, nil, g.refused(c, "", "the holder's own fingerprint is unknown, so no warrant can bind its build")
|
|
}
|
|
if g.d.State == nil {
|
|
return nil, nil, g.refused(c, "", "its holder keeps no state "+StateName+" to keep its calls in")
|
|
}
|
|
if why := refusedUnasked(c.Args); why != "" {
|
|
return nil, nil, g.refused(c, "", why)
|
|
}
|
|
if why := g.held(); why != "" {
|
|
return nil, nil, g.refused(c, "", why)
|
|
}
|
|
act, err := ActOf(c, "") // a process holder's act names no node, so two instances raise one ask (A-255)
|
|
if err != nil {
|
|
return nil, nil, g.refused(c, "", err.Error())
|
|
}
|
|
digest, err := operatorask.ActDigest(act)
|
|
if err != nil {
|
|
return nil, nil, g.refused(c, "", err.Error())
|
|
}
|
|
if !wait {
|
|
answer, err := g.unheld(c, act, digest, run)
|
|
return answer, nil, err
|
|
}
|
|
id, err := g.askOf(c, act, digest)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
now := g.d.Now().UTC()
|
|
w := WaitingCall{ID: newID(), Caller: c.Caller, Ask: id, Digest: digest, Act: act, Address: c.Address, Seat: c.Seat,
|
|
Verb: c.Name, Arguments: c.Args, Made: now, Asked: now}
|
|
if _, err := g.d.State.Create(callKey(w.ID), w); err != nil {
|
|
g.stateFailed(err)
|
|
return nil, nil, g.refused(c, digest, "its call cannot be kept in "+StateName+": "+err.Error())
|
|
}
|
|
g.stateRead()
|
|
g.armUnanswered(w)
|
|
g.d.Go(func() { g.Decided(id) }) // a choice made before the call was kept is acted on now
|
|
return nil, &Held{Asked: id, Call: w.ID}, nil
|
|
}
|
|
|
|
// askOf is the ask a call of the act takes (§677): the one `act.<digest>` names while it is open, else a new one, written
|
|
// with the bus's create (so two instances raise one ask) or, over an ended one, by compare-and-set on its revision. A call
|
|
// of an act declined within 10 minutes is refused with the time it may be asked again.
|
|
func (g *Gate) askOf(c Call, act operatorask.Act, digest string) (string, error) {
|
|
key := g.actKey(digest)
|
|
for {
|
|
e, err := g.d.State.Get(key)
|
|
if err != nil {
|
|
g.stateFailed(err)
|
|
return "", g.refused(c, digest, "its state "+StateName+" cannot be read: "+err.Error())
|
|
}
|
|
g.stateRead()
|
|
id := newID()
|
|
if e == nil {
|
|
if _, err := g.d.State.Create(key, id); err != nil {
|
|
if isChanged(err) {
|
|
continue
|
|
}
|
|
g.stateFailed(err)
|
|
return "", g.refused(c, digest, "its act cannot be kept in "+StateName+": "+err.Error())
|
|
}
|
|
return id, g.raiseHeld(c, act, digest, id)
|
|
}
|
|
var named string
|
|
if err := json.Unmarshal(e.Value, &named); err != nil || named == "" {
|
|
return "", g.refused(c, digest, fmt.Sprintf("its act's key in %s names no ask", StateName))
|
|
}
|
|
open, until, err := g.openAsk(c.Module, named)
|
|
if err != nil {
|
|
return "", g.refused(c, digest, err.Error())
|
|
}
|
|
if !until.IsZero() {
|
|
return "", fmt.Errorf("Not run: the operator declined ask %s; the same call is not asked again until %s", named, formatTime(until))
|
|
}
|
|
if open {
|
|
return named, nil
|
|
}
|
|
if _, err := g.d.State.Update(key, id, e.Revision); err != nil {
|
|
if isChanged(err) {
|
|
continue
|
|
}
|
|
g.stateFailed(err)
|
|
return "", g.refused(c, digest, "its act cannot be kept in "+StateName+": "+err.Error())
|
|
}
|
|
return id, g.raiseHeld(c, act, digest, id)
|
|
}
|
|
}
|
|
|
|
// openAsk says whether an ask is open for another call of its act: raised and not yet ended by the router; and, when it was
|
|
// declined within 10 minutes, until when no call of its act is asked.
|
|
func (g *Gate) openAsk(module, id string) (bool, time.Time, error) {
|
|
rec, err := g.record(module, id)
|
|
if err != nil {
|
|
return false, time.Time{}, fmt.Errorf("the router's record of its ask %s cannot be read: %v", id, err)
|
|
}
|
|
if rec == nil {
|
|
k, _, err := g.kept(id)
|
|
return err == nil && k != nil && k.Ended == "", time.Time{}, nil // raised and not yet taken by the router
|
|
}
|
|
switch rec.State {
|
|
case "open", "held-back":
|
|
return true, time.Time{}, nil
|
|
case "chosen":
|
|
if w := rec.Warrant; w != nil && w.Option == OptionDecline {
|
|
at, _ := parseTime(w.At)
|
|
if until := at.Add(DeclinedFor); g.d.Now().Before(until) {
|
|
return false, until, nil
|
|
}
|
|
}
|
|
}
|
|
return false, time.Time{}, nil
|
|
}
|
|
|
|
// raiseHeld raises the ask the act's key names, under the holder's module's name, keeping the act and the arguments beside
|
|
// it (§677; design 46 7-approval-mark §10).
|
|
func (g *Gate) raiseHeld(c Call, act operatorask.Act, digest, id string) error {
|
|
return g.keepAskHeld(c, act, digest, g.askFor(c, digest, id))
|
|
}
|
|
|
|
// keepAskHeld is keepAsk without its write of the act's key, which askOf made first.
|
|
func (g *Gate) keepAskHeld(c Call, act operatorask.Act, digest string, a Ask) error {
|
|
if size := keptSize(a, act, c.Args); size > MostKeptValue {
|
|
return g.refused(c, digest, fmt.Sprintf("its ask would keep %d bytes, more than the %d a value of the state %s holds", size, MostKeptValue, StateName))
|
|
}
|
|
if err := g.d.Channel.Ask(c.Module, a); err != nil {
|
|
return g.refused(c, digest, "the ask could not be raised: "+err.Error())
|
|
}
|
|
if err := g.keepCall(a.ID, act, c.Args); err != nil {
|
|
g.stateFailed(err)
|
|
return g.refused(c, digest, "the ask was raised and its call could not be kept: "+err.Error())
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// unheld is a call that does not wait (ADR 0286 §37's path, §677): run when its act's ask is chosen Approve within 30
|
|
// minutes and unclaimed, claimed once; otherwise refused naming the ask, raising one when none is open.
|
|
func (g *Gate) unheld(c Call, act operatorask.Act, digest string, run func() (any, error)) (any, error) {
|
|
named, err := g.d.State.Get(g.actKey(digest))
|
|
if err != nil {
|
|
g.stateFailed(err)
|
|
return nil, g.refused(c, digest, "its state "+StateName+" cannot be read: "+err.Error())
|
|
}
|
|
g.stateRead()
|
|
if named != nil {
|
|
var id string
|
|
if json.Unmarshal(named.Value, &id) == nil && id != "" {
|
|
rec, err := g.record(c.Module, id)
|
|
if err != nil {
|
|
return nil, g.refused(c, digest, "the router's record of its ask "+id+" cannot be read: "+err.Error())
|
|
}
|
|
if rec != nil && rec.State == "chosen" && rec.Warrant != nil && rec.Warrant.Option == OptionApprove {
|
|
if at, _ := parseTime(rec.Warrant.At); !g.d.Now().After(at.Add(RunWithin)) {
|
|
out := g.claimRun(c.Module, id, *rec.Warrant, act, run)
|
|
switch out.outcome {
|
|
case OutcomeRan:
|
|
return g.ran(c, id, rec, out.answer, out.err)
|
|
case OutcomeRefused:
|
|
return nil, g.refused(c, digest, out.why)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
id, err := g.askOf(c, act, digest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return nil, g.firstAnswer(c, id, digest, "")
|
|
}
|
|
|
|
// claimed is what came of a claim of a warrant for one call.
|
|
type claimed struct {
|
|
outcome string // OutcomeRan, OutcomeRefused, or "" when another took it
|
|
answer any
|
|
err error
|
|
why string
|
|
}
|
|
|
|
// claimRun holds the warrant to the ask and the call's act, claims it once at `claimed.<ask id>` with the bus's create, and
|
|
// runs the call (§677; ADR 0286 §27, §28, §30, §31): of two instances, or two deliveries, one runs.
|
|
func (g *Gate) claimRun(module, id string, w Warrant, act operatorask.Act, run func() (any, error)) claimed {
|
|
var out claimed
|
|
var claimErr error
|
|
h, err := g.d.Channel.Claim(module, w, act, func() {
|
|
if _, err := g.d.State.Create(claimedKey(id), map[string]string{"at": formatTime(g.d.Now().UTC())}); err != nil {
|
|
claimErr = err
|
|
return
|
|
}
|
|
out.outcome = OutcomeRan
|
|
out.answer, out.err = run()
|
|
})
|
|
switch {
|
|
case h == Claimed && claimErr != nil && !isChanged(claimErr):
|
|
g.stateFailed(claimErr)
|
|
return claimed{outcome: OutcomeRefused, why: "its claim cannot be written in " + StateName + ": " + claimErr.Error()}
|
|
case h == Claimed:
|
|
return out
|
|
case h == ClaimDone:
|
|
return claimed{}
|
|
case h == "" && err != nil:
|
|
g.stateFailed(err)
|
|
return claimed{outcome: OutcomeRefused, why: "its state " + StateName + " cannot be written: " + err.Error()}
|
|
default: // refused, or the act about to run is not the one the option bound: nothing runs
|
|
return claimed{outcome: OutcomeRefused, why: "its warrant does not hold: " + errText(err)}
|
|
}
|
|
}
|
|
|
|
// Decided acts on the router's own record of one of the holder's asks (§677): heard on the holder's `decided`, never from
|
|
// its body, and at the start for each ask a waiting call waits on. On the Approve the oldest waiting call of the ask runs,
|
|
// within 30 minutes of the choice, else it ends expired; every other is asked anew. A Decline ends every one declined, an
|
|
// ask that ended without a choice every one expired, and one the router refused every one refused with its words.
|
|
func (g *Gate) Decided(id string) {
|
|
module := g.d.Runner
|
|
rec, err := g.record(module, id)
|
|
if err != nil {
|
|
g.d.Logf("[approval] the router's record of ask %s cannot be read (%v): read again at the next start", id, err)
|
|
return
|
|
}
|
|
if rec == nil || rec.State == "open" || rec.State == "held-back" {
|
|
return
|
|
}
|
|
calls, err := g.waiting(func(w WaitingCall) bool { return w.Ask == id })
|
|
if err != nil {
|
|
g.stateFailed(err)
|
|
g.d.Logf("[approval] the waiting calls of ask %s cannot be read (%v): read again at the next start", id, err)
|
|
return
|
|
}
|
|
if len(calls) == 0 {
|
|
return
|
|
}
|
|
end := func(w WaitingCall, outcome, why string) {
|
|
g.end(w, GatedEnded{Outcome: outcome, Error: why})
|
|
}
|
|
switch {
|
|
case rec.State == "refused":
|
|
for _, w := range calls {
|
|
end(w, OutcomeRefused, "the router refused its ask "+id+": "+orNone(rec.Answer))
|
|
}
|
|
return
|
|
case rec.State != "chosen":
|
|
for _, w := range calls {
|
|
end(w, OutcomeExpired, "its ask "+id+" ended "+rec.State+" with no choice; nothing ran")
|
|
}
|
|
return
|
|
case rec.Warrant == nil:
|
|
for _, w := range calls {
|
|
end(w, OutcomeRefused, "the router's record of its ask "+id+" is chosen and carries no warrant")
|
|
}
|
|
return
|
|
case rec.Warrant.Option == OptionDecline:
|
|
for _, w := range calls {
|
|
end(w, OutcomeDeclined, "the operator declined ask "+id+"; nothing ran")
|
|
}
|
|
return
|
|
}
|
|
first, rest := calls[0], calls[1:]
|
|
at, _ := parseTime(rec.Warrant.At)
|
|
if g.d.Now().After(at.Add(RunWithin)) {
|
|
end(first, OutcomeExpired, "the Approve of ask "+id+" was heard more than 30 minutes after it was given; nothing ran")
|
|
} else {
|
|
g.runWaiting(first, rec)
|
|
}
|
|
for _, w := range rest {
|
|
g.anew(w)
|
|
}
|
|
}
|
|
|
|
// runWaiting claims and runs one waiting call on its ask's warrant, saying its end.
|
|
func (g *Gate) runWaiting(w WaitingCall, rec *Record) {
|
|
h := g.handler(w.Seat, w.Verb)
|
|
c := Call{Address: w.Address, Module: g.d.Runner, Seat: w.Seat, Name: w.Verb, Caller: w.Caller, Args: w.Arguments}
|
|
if h == nil {
|
|
g.end(w, GatedEnded{Outcome: OutcomeRefused, Error: "the holder serves no handler of " + w.Address + " now; nothing ran"})
|
|
return
|
|
}
|
|
out := g.claimRun(g.d.Runner, w.Ask, *rec.Warrant, w.Act, func() (any, error) { return h(w.Caller, w.Arguments) })
|
|
switch out.outcome {
|
|
case "": // another instance, or another call of the same ask, took the warrant
|
|
return
|
|
case OutcomeRefused:
|
|
g.end(w, GatedEnded{Outcome: OutcomeRefused, Error: out.why})
|
|
return
|
|
}
|
|
answered, err := g.ran(c, w.Ask, rec, out.answer, out.err)
|
|
if err != nil {
|
|
g.end(w, GatedEnded{Outcome: OutcomeRan, Error: err.Error()})
|
|
return
|
|
}
|
|
a := answered.(Answered)
|
|
g.end(w, GatedEnded{Outcome: OutcomeRan, Answer: a.Answer, Approval: a.Approval})
|
|
}
|
|
|
|
// anew asks a waiting call's act anew, as a new ask (ADR 0286 §33: a second run takes a second approval); its end comes
|
|
// when its own ask ends.
|
|
func (g *Gate) anew(w WaitingCall) {
|
|
c := Call{Address: w.Address, Module: g.d.Runner, Seat: w.Seat, Name: w.Verb, Caller: w.Caller, Args: w.Arguments,
|
|
Fingerprint: w.Act["build"]}
|
|
id, err := g.askOf(c, w.Act, w.Digest)
|
|
if err != nil {
|
|
g.end(w, GatedEnded{Outcome: OutcomeRefused, Error: err.Error()})
|
|
return
|
|
}
|
|
if err := g.updateCall(w.ID, func(k *WaitingCall) { k.Ask, k.Asked = id, g.d.Now().UTC() }); err != nil {
|
|
g.d.Logf("[approval] the call %s, asked anew as ask %s, cannot be kept (%v)", w.ID, id, err)
|
|
return
|
|
}
|
|
w.Ask, w.Asked = id, g.d.Now().UTC()
|
|
g.rearmUnanswered(w)
|
|
g.d.Go(func() { g.Decided(id) })
|
|
}
|
|
|
|
// end keeps a waiting call's end, then says it on gated-ended (§677); an end not said is said again at the next start.
|
|
func (g *Gate) end(w WaitingCall, e GatedEnded) {
|
|
e.Call, e.Ask, e.Act, e.Verb = w.ID, w.Ask, w.Digest, w.Verb
|
|
ended := false
|
|
if err := g.updateCall(w.ID, func(k *WaitingCall) {
|
|
if k.End == nil {
|
|
k.End, ended = &e, true
|
|
}
|
|
}); err != nil {
|
|
g.d.Logf("[approval] the end %s of call %s cannot be kept (%v)", e.Outcome, w.ID, err)
|
|
return
|
|
}
|
|
if ended {
|
|
g.say(w.ID)
|
|
}
|
|
}
|
|
|
|
// say says a kept end on gated-ended and marks it said; its finding gate-unanswered is cleared.
|
|
func (g *Gate) say(id string) {
|
|
e, err := g.d.State.Get(callKey(id))
|
|
if err != nil || e == nil {
|
|
return
|
|
}
|
|
var w WaitingCall
|
|
if json.Unmarshal(e.Value, &w) != nil || w.End == nil || w.Said {
|
|
return
|
|
}
|
|
// Its bound key still kept says gate-unanswered was not raised, and the key goes; one the bus ended is cleared once the
|
|
// end is said (A-255).
|
|
b, err := g.d.State.Get(unansweredHeldKey(id))
|
|
raised := err == nil && b == nil
|
|
if b != nil && g.d.Bounds != nil {
|
|
_ = g.d.Bounds.Delete(unansweredHeldKey(id))
|
|
}
|
|
if g.d.Say == nil {
|
|
g.d.Logf("[approval] the end %s of call %s is not said: the holder says no gated-ended", w.End.Outcome, id)
|
|
return
|
|
}
|
|
if err := g.d.Say(GatedEndedSubject(w.Seat, w.Caller, w.ID), *w.End, GatedEndedEvent+"."+w.ID); err != nil {
|
|
g.d.Logf("[approval] the end %s of call %s is not said (%v): said again at the next start", w.End.Outcome, id, err)
|
|
return
|
|
}
|
|
g.d.Logf("[approval] %s by %s ended %s (call %s, ask %s)", w.Address, w.Caller, w.End.Outcome, id, w.Ask)
|
|
_ = g.updateCall(id, func(k *WaitingCall) { k.Said = true })
|
|
if raised && g.d.Findings != nil {
|
|
_ = g.d.Findings.Clear(nilCtx(), KindGateUnanswered, id, "the call's end, "+w.End.Outcome+", is said")
|
|
}
|
|
}
|
|
|
|
// updateCall changes a kept call by compare-and-set, again when another write came first.
|
|
func (g *Gate) updateCall(id string, change func(*WaitingCall)) error {
|
|
for {
|
|
e, err := g.d.State.Get(callKey(id))
|
|
if err != nil || e == nil {
|
|
return fmt.Errorf("the call %s cannot be read: %v", id, err)
|
|
}
|
|
var w WaitingCall
|
|
if err := json.Unmarshal(e.Value, &w); err != nil {
|
|
return err
|
|
}
|
|
change(&w)
|
|
if _, err := g.d.State.Update(callKey(id), w, e.Revision); err == nil {
|
|
return nil
|
|
} else if !isChanged(err) {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
// waiting is every kept call that has not ended and that keep takes, oldest first.
|
|
func (g *Gate) waiting(keep func(WaitingCall) bool) ([]WaitingCall, error) {
|
|
all, err := g.calls()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []WaitingCall
|
|
for _, w := range all {
|
|
if w.End == nil && keep(w) {
|
|
out = append(out, w)
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// calls is every kept call, oldest first.
|
|
func (g *Gate) calls() ([]WaitingCall, error) {
|
|
names, err := g.d.State.Keys()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var out []WaitingCall
|
|
for _, key := range names {
|
|
if !strings.HasPrefix(key, "call.") {
|
|
continue
|
|
}
|
|
e, err := g.d.State.Get(key)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var w WaitingCall
|
|
if e == nil || json.Unmarshal(e.Value, &w) != nil {
|
|
continue
|
|
}
|
|
out = append(out, w)
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Made.Before(out[j].Made) })
|
|
return out, nil
|
|
}
|
|
|
|
// unansweredHeldKey is a waiting call's bound key in the holder's state approvals (lead answer A-255): created with the
|
|
// ask's 60 minutes as its own time to live, its expiry heard as the bus's event raises gate-unanswered.
|
|
func unansweredHeldKey(call string) string { return "unanswered." + call }
|
|
|
|
// armUnanswered keeps the waiting call's bound key (§677; A-255): when its ask's 60 minutes pass with no end of it, the bus
|
|
// ends the key and the holder, hearing that, raises gate-unanswered; no clock waits in the process. A call already past its
|
|
// 60 minutes (its key ended while the holder was away) is judged at once.
|
|
func (g *Gate) armUnanswered(w WaitingCall) {
|
|
left := w.Asked.Add(AskLasts).Sub(g.d.Now())
|
|
if left <= 0 {
|
|
g.heldUnanswered(w.ID)
|
|
return
|
|
}
|
|
if g.d.Bounds == nil {
|
|
g.d.Logf("[approval] the call %s waits on ask %s with no bound kept: the holder's %s keeps no key with its own time to live",
|
|
w.ID, w.Ask, StateName)
|
|
return
|
|
}
|
|
if err := g.d.Bounds.CreateTTL(unansweredHeldKey(w.ID), map[string]string{"call": w.ID, "ask": w.Ask}, left); err != nil && !isChanged(err) {
|
|
g.d.Logf("[approval] the bound of call %s on ask %s is not kept: %v", w.ID, w.Ask, err)
|
|
}
|
|
}
|
|
|
|
// rearmUnanswered keeps a call asked anew under a new bound of its new ask's 60 minutes.
|
|
func (g *Gate) rearmUnanswered(w WaitingCall) {
|
|
if g.d.Bounds != nil {
|
|
_ = g.d.Bounds.Delete(unansweredHeldKey(w.ID))
|
|
}
|
|
g.armUnanswered(w)
|
|
}
|
|
|
|
// heldUnanswered is a waiting call's bound ended (A-255): gate-unanswered raised while the call has no end.
|
|
func (g *Gate) heldUnanswered(id string) {
|
|
e, err := g.d.State.Get(callKey(id))
|
|
if err != nil || e == nil {
|
|
return
|
|
}
|
|
var w WaitingCall
|
|
if json.Unmarshal(e.Value, &w) != nil || w.End != nil || g.d.Findings == nil {
|
|
return
|
|
}
|
|
if err := g.d.Findings.Raise(nilCtx(), KindGateUnanswered, w.ID, fmt.Sprintf("the call %s of %s by %s waits on ask %s, "+
|
|
"past its %s with no decided heard: nothing ran; the holder's %s.approvals reads it", w.ID, w.Address, w.Caller, w.Ask,
|
|
AskLasts, w.Seat), "a call waits on an unanswered ask"); err != nil {
|
|
g.d.Logf("[approval] the finding %s of call %s is not kept: %v", KindGateUnanswered, w.ID, err)
|
|
}
|
|
}
|
|
|
|
// watchHeld hears the waiting calls' bound keys end (A-255) for the life of the holder's connection.
|
|
func (g *Gate) watchHeld() error {
|
|
if g.d.Bounds == nil {
|
|
return nil
|
|
}
|
|
_, err := g.d.Bounds.Watch("unanswered.*", func(key string) { g.heldUnanswered(strings.TrimPrefix(key, "unanswered.")) })
|
|
return err
|
|
}
|
|
|
|
// StartHolding is the holder's read at its start (§677): every waiting call is read, each ask already decided acted on as
|
|
// when its decided is heard, each end not yet said said, each waiting call's 60 minutes armed, and every acted, unrecorded
|
|
// run recorded. A state that cannot be read leaves every marked call refused until it can.
|
|
func (g *Gate) StartHolding() error {
|
|
all, err := g.calls()
|
|
if err != nil {
|
|
g.stateFailed(err)
|
|
return fmt.Errorf("the waiting calls in %s cannot be read: %w", StateName, err)
|
|
}
|
|
g.stateRead()
|
|
if err := g.watchHeld(); err != nil {
|
|
return fmt.Errorf("the bound keys in %s cannot be watched: %w", StateName, err)
|
|
}
|
|
asks := map[string]bool{}
|
|
for _, w := range all {
|
|
switch {
|
|
case w.End != nil && !w.Said:
|
|
g.say(w.ID)
|
|
case w.End == nil:
|
|
g.armUnanswered(w)
|
|
asks[w.Ask] = true
|
|
}
|
|
}
|
|
ids := make([]string, 0, len(asks))
|
|
for id := range asks {
|
|
ids = append(ids, id)
|
|
}
|
|
sort.Strings(ids)
|
|
for _, id := range ids {
|
|
g.Decided(id)
|
|
}
|
|
g.Recover()
|
|
return nil
|
|
}
|
|
|
|
// Approvals is the seat's verb `approvals` (§677): the waiting and ended calls with their caller, ask and state, the
|
|
// `act.` keys and the claims; a call's arguments only to that call's caller.
|
|
type Approvals struct {
|
|
Calls []ListedCall `json:"calls"`
|
|
Acts []ListedAct `json:"acts"`
|
|
Claims []string `json:"claims"`
|
|
}
|
|
|
|
// ListedCall is one kept call as `approvals` lists it.
|
|
type ListedCall struct {
|
|
Call string `json:"call"`
|
|
Caller string `json:"caller"`
|
|
Ask string `json:"ask"`
|
|
Verb string `json:"verb"`
|
|
State string `json:"state"`
|
|
Made time.Time `json:"made"`
|
|
End *GatedEnded `json:"end,omitempty"`
|
|
Arguments json.RawMessage `json:"arguments,omitempty"`
|
|
}
|
|
|
|
// ListedAct is one `act.` key: its digest, escaped as kept, and the ask it names.
|
|
type ListedAct struct {
|
|
Act string `json:"act"`
|
|
Ask string `json:"ask"`
|
|
}
|
|
|
|
// Approvals answers the verb for caller, the account the bus proved.
|
|
func (g *Gate) Approvals(caller string) (Approvals, error) {
|
|
out := Approvals{Calls: []ListedCall{}, Acts: []ListedAct{}, Claims: []string{}}
|
|
if g.d.State == nil {
|
|
return out, fmt.Errorf("the holder keeps no state %s", StateName)
|
|
}
|
|
all, err := g.calls()
|
|
if err != nil {
|
|
return out, fmt.Errorf("the state %s cannot be read: %w", StateName, err)
|
|
}
|
|
for _, w := range all {
|
|
l := ListedCall{Call: w.ID, Caller: w.Caller, Ask: w.Ask, Verb: w.Verb, State: "waiting", Made: w.Made, End: w.End}
|
|
if w.End != nil {
|
|
l.State = w.End.Outcome
|
|
}
|
|
if caller != "" && caller == w.Caller {
|
|
l.Arguments = w.Arguments
|
|
}
|
|
out.Calls = append(out.Calls, l)
|
|
}
|
|
names, err := g.d.State.Keys()
|
|
if err != nil {
|
|
return out, fmt.Errorf("the state %s cannot be read: %w", StateName, err)
|
|
}
|
|
sort.Strings(names)
|
|
for _, key := range names {
|
|
switch {
|
|
case strings.HasPrefix(key, "act."):
|
|
e, err := g.d.State.Get(key)
|
|
var id string
|
|
if err == nil && e != nil {
|
|
_ = json.Unmarshal(e.Value, &id)
|
|
}
|
|
out.Acts = append(out.Acts, ListedAct{Act: strings.TrimPrefix(key, "act."), Ask: id})
|
|
case strings.HasPrefix(key, "claimed."):
|
|
out.Claims = append(out.Claims, strings.TrimPrefix(key, "claimed."))
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
func nilCtx() context.Context { return context.Background() }
|