package approval import ( "encoding/json" "fmt" "time" assignerseat "git.novox.be/novox/mesh-contracts/go/seats/meshmoduleassigner" ) // AskedOn is the seat an ask is raised on, whose holder must be there for a marked call to be asked (§19). const AskedOn = "operator-channel" // UnreadableAfter is how long the state approvals fails before the runner's finding says so (§19). const UnreadableAfter = 5 * time.Minute // unreadableKey is the bound key whose expiry raises `approvals-unreadable` (lead answer A-157). func (g *Gate) unreadableKey() string { return g.d.Node + ".unreadable." + StateName } // held says why no marked call can be asked now, or "" (§19): no module holds operator-channel, as the assigner's view // of the seat's holders says, or that view is not read. func (g *Gate) held() string { if g.d.Holder == nil { return "the tool runner reads no view of the seats' holders, so it cannot tell that a module holds " + AskedOn } ok, err := g.d.Holder(AskedOn) switch { case err != nil: return fmt.Sprintf("whether a module holds %s cannot be read: %v", AskedOn, err) case !ok: return "no module holds " + AskedOn + ", so nobody can be asked" } return "" } // stateFailed notes that the state approvals could not be read or written: no marked call runs on this machine until it // can, the failed read or write is made again at the next marked call and at the next membership change (A-157), and // five minutes on the runner's finding says it (§19). func (g *Gate) stateFailed(err error) { g.mu.Lock() first := g.failingSince.IsZero() if first { g.failingSince = g.d.Now() } g.mu.Unlock() if first { g.d.Logf("[mesh-tools] the state %s on %s cannot be read or written (%v): no marked call runs here until it can", StateName, g.d.Node, err) if g.d.Bounds != nil { if e := g.d.Bounds.CreateTTL(g.unreadableKey(), map[string]string{"since": g.d.Now().UTC().Format(time.RFC3339)}, UnreadableAfter); e != nil { g.d.Logf("[mesh-tools] the bound of the state %s's failure is not kept: %v", StateName, e) } } } } // stateRead notes that the state approvals was read or written: a failure noted before ends, its finding cleared. func (g *Gate) stateRead() { g.mu.Lock() was := !g.failingSince.IsZero() g.failingSince = time.Time{} g.mu.Unlock() if !was { return } if g.d.Bounds != nil { _ = g.d.Bounds.Delete(g.unreadableKey()) } g.finding(StateName, KindApprovalsUnreadable, false, "the state "+StateName+" is read again", "") } // failing says the state approvals is failing now. func (g *Gate) failing() bool { g.mu.Lock() defer g.mu.Unlock() return !g.failingSince.IsZero() } // StateAgain makes the failed read of the state approvals again: at the next membership change the runner hears (A-157). func (g *Gate) StateAgain() { if g.d.State == nil || !g.failing() { return } if _, err := g.d.State.Keys(); err != nil { return } g.stateRead() } // unreadableExpired is the bound key of a failing state expiring: the finding raised while the state still fails. func (g *Gate) unreadableExpired() { g.mu.Lock() since := g.failingSince g.mu.Unlock() if since.IsZero() { return } g.finding(StateName, KindApprovalsUnreadable, true, fmt.Sprintf("the state %s on %s cannot be read or written since %s: no marked call "+ "runs here until it can", StateName, g.d.Node, since.UTC().Format(time.RFC3339)), "approvals unreadable on "+g.d.Node) } // HoldersIn is Holder over the assigner's view of the seats' holders as the runner holds it (§19): view answers the view's // values by key, an error while its first view is not answered; a seat is held when its key names a holder. func HoldersIn(view func() (map[string][]byte, error)) func(seat string) (bool, error) { return func(seat string) (bool, error) { values, err := view() if err != nil { return false, err } raw, ok := values[assignerseat.SeatHoldersBySeatKey(seat)] if !ok { return false, nil } var h assignerseat.SeatHolders if err := json.Unmarshal(raw, &h); err != nil { return false, fmt.Errorf("the holders of %s cannot be read: %w", seat, err) } return len(h.Holders) > 0, nil } }