Files
mesh-delivery/modules/mesh-delivery-assigner/internal/assign/ended.go
T
jschoubben 42c3db1d5b B2h AS17 (2a): the assigner's ask of a held revert has its kind and key
The asks state gains the kind revert, keyed revert.<node>.<module>.<hex>
after the reverted build's fingerprint; like the backup ask it needs no
proof of who answered, so the view may answer it, and an asking that ends
unanswered while the hold stands is written again.

Builds: design 47 G §710, §300

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 09:24:04 +01:00

140 lines
6.3 KiB
Go

package assign
import (
"encoding/json"
"fmt"
"slices"
"strings"
"time"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-contracts/go/clients/meshdeliveryclient"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-teller/tellerevents"
"mesh-delivery-assigner/internal/puts"
)
// AskLasts is how long an ask of the assigner's rules stands at the operator channel before it lapses unanswered: the hold,
// restore, record, backup, outside and revert asks.
const AskLasts = 7 * 24 * time.Hour
// writtenAgainOnEnd is, per kind of the assigner's asks, whether an asking that ended at the channel without an answer while
// its key stands is written again under a new asking (issue 1966, note 151), or why not. A rule's ask waits for a person
// as long as its key stands, so it is asked again; the authorising ask of a proposal ends the proposal when it lapses
// (ADR 0567 §5) and is asked again by the seat's verb `again` (§11).
var writtenAgainOnEnd = map[string]string{
asks.AskKindHold: "",
asks.AskKindRestore: "",
asks.AskKindBackup: "",
asks.AskKindOutside: "", // raised again at once while the wait stands (design 47 G §706); the wait's end takes the key away
asks.AskKindRecord: "",
asks.AskKindRevert: "", // raised again at once while the hold stands (design 47 G §710); one answered hold is not asked again
asks.AskKindAssignments: "a proposal's authorising ask that lapsed ends the proposal (ADR 0567 §5); the verb again asks it again (§11)",
}
// AskedAgain is the ask at key written again after the teller said its asking ended at the channel without an answer (the
// teller's event ask-ended): the value to write by compare-and-set over current, or why nothing is written. Nothing is written
// for an outcome other than a lapse or a cancellation, for a key that holds another asking now (an answer to it, or a newer
// asking, came since), for a kind its own rule ends (writtenAgainOnEnd), and for a record ask a put asked (put, nil for none:
// the put at the record ask's node and module), since its put is refused when it lapses (ADR 0546). The new asking is
// AskingSeconds(now), above the old one, under an id of its own, standing AskLasts from now.
func AskedAgain(e tellerevents.Ended, current []byte, put *puts.Put, now time.Time) ([]byte, string) {
if e.Outcome != occ.OutcomeExpired && e.Outcome != occ.OutcomeCancelled {
return nil, fmt.Sprintf("it ended %s, which asking again would not change", e.Outcome)
}
k, err := asks.ParseAnyAskKey(e.Key)
if err != nil {
return nil, err.Error()
}
why, known := writtenAgainOnEnd[k.Kind]
if !known {
return nil, "the assigner asks no " + k.Kind + " ask"
}
if why != "" {
return nil, why
}
if current == nil {
return nil, "the key is taken away: nothing is asked"
}
a, ask, err := DecodeAsk(e.Key, current)
if err != nil {
return nil, err.Error()
}
if ask.ID != e.Ask {
return nil, "the key holds the asking " + ask.ID + " now, not the one that ended"
}
if k.Kind == asks.AskKindRecord && put != nil && put.Ask == e.Key && PutAsking(*put) == a.Asking {
return nil, "a put asked it, and its put is refused when it lapses (ADR 0546)"
}
asking := max(AskingSeconds(now), a.Asking+1)
ask.ID, ask.Expires = meshdeliveryclient.AskingID(e.Key, asking), occ.FormatTime(now.Add(AskLasts))
value, err := EncodeAsk(e.Key, assignerAsk(asking, ask))
if err != nil {
return nil, err.Error()
}
return value, ""
}
// ChannelAsksBound is the most bytes of asks the assigner asks the operator-channel seat's verb asks for at its consumer's
// bind: every ended asking of its held asks fits.
const ChannelAsksBound = 1 << 20
// ChannelAsksQuery is the arguments of the operator-channel seat's verb asks that answer, at the assigner's consumer's bind,
// every asking of its held asks (key to value, as the asks state holds them) that ended at the channel: the ended asks
// opened since the earliest of them could have been opened, less a minute (issue 1966 note 155, ADR 0668 §4). ok is false
// when no held ask reads, and then nothing is asked.
func ChannelAsksQuery(held map[string][]byte) (query oc.AsksInput, ok bool) {
var since time.Time
for key, value := range held {
a, ask, err := DecodeAsk(key, value)
if err != nil {
continue
}
opened := ExpiresAt(ask).Add(-AskLasts)
if a.Asking >= 2 {
if at := time.Unix(int64(a.Asking), 0); at.Before(opened) {
opened = at
}
}
if !ok || opened.Before(since) {
since, ok = opened, true
}
}
if !ok {
return oc.AsksInput{}, false
}
return oc.AsksInput{State: "ended", Since: since.Add(-time.Minute).UTC().Format(time.RFC3339), Bound: ChannelAsksBound}, true
}
// EndedAtChannel is, of the operator-channel seat's answer of its verb asks, each held ask whose asking ended there
// expired or cancelled, as the teller says it in its event ask-ended, so the assigner writes it again by the rules of that
// event (AskedAgain), with its compare-and-set: an ended ask said before the assigner's consumer existed is not delivered
// to it (issue 1966 note 155, ADR 0668 §4). cut is the answer's own cut, said by the caller; an answer that does not read
// is the error.
func EndedAtChannel(held map[string][]byte, answer []byte) (ended []tellerevents.Ended, cut string, err error) {
var got oc.AsksAnswer // the seat's declared answer: each ask's key and its state, and the cut
if err := json.Unmarshal(answer, &got); err != nil {
return nil, "", fmt.Errorf("assign: the operator channel's asks do not read: %w", err)
}
byID := map[string]string{}
for key, value := range held {
if _, ask, err := DecodeAsk(key, value); err == nil {
byID[ask.ID] = key
}
}
for _, v := range got.Asks {
if v.State != occ.OutcomeExpired && v.State != occ.OutcomeCancelled {
continue
}
_, id, ok := oc.ParseAsksByAskerIDKey(v.Key)
if key, held := byID[id]; ok && held {
ended = append(ended, tellerevents.Ended{Key: key, Ask: id, Outcome: v.State})
}
}
slices.SortFunc(ended, func(a, b tellerevents.Ended) int { return strings.Compare(a.Key, b.Key) })
return ended, got.Cut, nil
}