RTR4: the router keeps a refused ask as a closed record, the oldest removed past refused-kept
Builds: design 47 G §570
This commit is contained in:
@@ -5,6 +5,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
@@ -71,6 +73,13 @@ func (m *memAsks) UpdateAsk(k string, r AskRecord, rev uint64) (uint64, error) {
|
||||
m.asks[k], m.revs[k] = clone(r), m.rev
|
||||
return m.rev, nil
|
||||
}
|
||||
func (m *memAsks) DeleteAsk(k string) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
delete(m.asks, k)
|
||||
delete(m.revs, k)
|
||||
return nil
|
||||
}
|
||||
func (m *memAsks) AskKeys() ([]string, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -601,8 +610,17 @@ func TestAnAskIsRefusedInWordsToItsAsker(t *testing.T) {
|
||||
if strings.Contains(all, "d-6") || store.asks["mesh-delivery.d-6"].State != StateOpen || store.asks["mesh-delivery.d-3"].State != StateHeldBack {
|
||||
t.Errorf("the fourth ask was refused or the oldest not held back: d-6 %s, d-3 %s", store.asks["mesh-delivery.d-6"].State, store.asks["mesh-delivery.d-3"].State)
|
||||
}
|
||||
if _, kept := store.asks["mesh-delivery.d-1"]; kept {
|
||||
t.Error("a refused ask was recorded")
|
||||
// A refused ask is kept as a closed record with its outcome and words (design 47 G §570), and `asks` lists it refused.
|
||||
if rec, kept := store.asks["mesh-delivery.d-1"]; !kept || rec.State != string(occ.OutcomeRefused) || rec.Warrant == nil ||
|
||||
!strings.Contains(rec.Warrant.Words, "an ask that destroys lasts at most") || liveState(rec.State) {
|
||||
t.Errorf("a refused ask was not kept as a closed record: %+v", rec)
|
||||
}
|
||||
listed := false
|
||||
for _, v := range h.AsksNow() {
|
||||
listed = listed || (v.Key == "mesh-delivery.d-1" && v.State == "refused" && strings.HasPrefix(v.Answer, "refused: "))
|
||||
}
|
||||
if !listed {
|
||||
t.Error("asks does not list the refused ask")
|
||||
}
|
||||
// A node's engine is an asker under node-engine-<node> (novox/hq ADR 0563 §3, changing ADR 0259 §2): its ask is
|
||||
// recorded under that name, with its own bound of open asks; a name that is no asker's is dropped.
|
||||
@@ -1073,3 +1091,38 @@ func TestAnAsksMessageWithOneAnswerIsNotFoldedIntoADigest(t *testing.T) {
|
||||
t.Errorf("the ask's message was folded with another: %q", asked.Body)
|
||||
}
|
||||
}
|
||||
|
||||
// Design 47 G §570: a refusal is written by a create only, so an open ask of the same id keeps its record; the twenty-first
|
||||
// refusal of one asker removes its oldest, as the setting refused-kept says, each removal written in the journal.
|
||||
func TestRefusedAsksAreKeptByACreateOnlyAndTheOldestGoesPastTheSetting(t *testing.T) {
|
||||
h, _, _, c, store, _ := newRouter(t)
|
||||
h.RefusedKept = func() int { return 3 }
|
||||
open := heldAsk(c, "open-1")
|
||||
submit(t, h, "mesh-delivery", open)
|
||||
before := store.asks["mesh-delivery.open-1"]
|
||||
same := heldAsk(c, "open-1")
|
||||
same.Options[0].Level = occ.Destroy
|
||||
submit(t, h, "mesh-delivery", same)
|
||||
if got := store.asks["mesh-delivery.open-1"]; got.State != before.State || got.Warrant != nil {
|
||||
t.Fatalf("a refusal of an open ask's id overwrote it: %+v", got)
|
||||
}
|
||||
for i := 1; i <= 5; i++ {
|
||||
bad := heldAsk(c, fmt.Sprintf("bad-%d", i))
|
||||
bad.Options[0].Level = occ.Destroy
|
||||
submit(t, h, "mesh-delivery", bad)
|
||||
c.pass(time.Minute)
|
||||
}
|
||||
var kept []string
|
||||
for k, r := range store.asks {
|
||||
if r.State == string(occ.OutcomeRefused) {
|
||||
kept = append(kept, k)
|
||||
}
|
||||
}
|
||||
sort.Strings(kept)
|
||||
if !reflect.DeepEqual(kept, []string{"mesh-delivery.bad-3", "mesh-delivery.bad-4", "mesh-delivery.bad-5"}) {
|
||||
t.Fatalf("kept %v: the three newest refused asks of the asker", kept)
|
||||
}
|
||||
if h.askOf("mesh-delivery.bad-1") != nil {
|
||||
t.Error("the router's own view still holds the removed refusal")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,6 +95,8 @@ type Identity struct {
|
||||
type AskStore interface {
|
||||
GetAsk(key string) (*AskRecord, uint64, error)
|
||||
CreateAsk(key string, r AskRecord) (uint64, error)
|
||||
// DeleteAsk removes one ask's record: the oldest refused one past the setting refused-kept (design 47 G §570).
|
||||
DeleteAsk(key string) error
|
||||
UpdateAsk(key string, r AskRecord, revision uint64) (uint64, error)
|
||||
AskKeys() ([]string, error)
|
||||
GetIdentity(key string) (*Identity, error)
|
||||
@@ -864,10 +866,70 @@ func (h *Holder) offerAsk(r *Record) {
|
||||
h.flush(now, false)
|
||||
}
|
||||
|
||||
// refuseAsk says to its asker that an ask was not taken, and why.
|
||||
// DefaultRefusedKept is how many refused asks of one asker the router keeps when the setting refused-kept gives none.
|
||||
const DefaultRefusedKept = 20
|
||||
|
||||
// refuseAsk says to its asker that an ask was not taken, and why, and keeps it under `<asker>.<id>` as a closed record with the
|
||||
// outcome refused (design 47 G §570), by a create only: an id that holds a record already, an open ask with its tickets, is
|
||||
// left as it is and the refusal is said on `decided` alone.
|
||||
func (h *Holder) refuseAsk(asker string, a oc.Ask, why string) {
|
||||
h.Logf("[messenger] refused an ask from %s (%s): %s", asker, a.ID, why)
|
||||
h.tell(oc.Warrant{Ask: a.ID, Asker: asker, About: a.About, Outcome: occ.OutcomeRefused, Words: why, At: occ.FormatTime(h.Now())})
|
||||
now := h.Now()
|
||||
w := oc.Warrant{Ask: a.ID, Asker: asker, About: a.About, Outcome: occ.OutcomeRefused, Words: why, At: occ.FormatTime(now)}
|
||||
told := h.tell(w) == nil
|
||||
if h.Asks == nil || !occ.UsableID(a.ID) {
|
||||
return
|
||||
}
|
||||
key := askKey(asker, a.ID)
|
||||
if h.askOf(key) != nil {
|
||||
return
|
||||
}
|
||||
// Only its id and subject are kept: a refused ask's words may carry what may not leave the mesh (a secret's shape), so
|
||||
// none of them is written to the record; its warrant says why in the router's words.
|
||||
rec := AskRecord{Asker: asker, Ask: oc.Ask{ID: a.ID, About: a.About}, State: string(occ.OutcomeRefused), Opened: now, Closed: now, Warrant: &w, Told: told}
|
||||
rev, err := h.Asks.CreateAsk(key, rec)
|
||||
switch {
|
||||
case errors.Is(err, ErrChanged):
|
||||
return
|
||||
case err != nil:
|
||||
h.Logf("[messenger] the refusal of %s could not be kept: %v", key, err)
|
||||
return
|
||||
}
|
||||
h.keepAsk(key, rec, rev)
|
||||
h.pruneRefused(asker)
|
||||
}
|
||||
|
||||
// pruneRefused removes the oldest refused records of an asker past the setting refused-kept, each removal written in the
|
||||
// journal (design 47 G §570).
|
||||
func (h *Holder) pruneRefused(asker string) {
|
||||
most := DefaultRefusedKept
|
||||
if h.RefusedKept != nil {
|
||||
most = h.RefusedKept()
|
||||
}
|
||||
h.mu.Lock()
|
||||
var keys []string
|
||||
for k, e := range h.asks {
|
||||
if e.rec.Asker == asker && e.rec.State == string(occ.OutcomeRefused) {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
}
|
||||
sort.Slice(keys, func(i, j int) bool {
|
||||
a, b := h.asks[keys[i]].rec.Closed, h.asks[keys[j]].rec.Closed
|
||||
return a.Before(b) || (a.Equal(b) && keys[i] < keys[j])
|
||||
})
|
||||
h.mu.Unlock()
|
||||
for len(keys) > most {
|
||||
oldest := keys[0]
|
||||
keys = keys[1:]
|
||||
if err := h.Asks.DeleteAsk(oldest); err != nil {
|
||||
h.Logf("[messenger] the refused ask %s could not be removed: %v", oldest, err)
|
||||
continue
|
||||
}
|
||||
h.mu.Lock()
|
||||
delete(h.asks, oldest)
|
||||
h.mu.Unlock()
|
||||
h.Logf("[messenger] removed the oldest refused ask of %s, %s: %d are kept at most (refused-kept)", asker, oldest, most)
|
||||
}
|
||||
}
|
||||
|
||||
// tell publishes a warrant, or an ask's end, to its asker alone, deduplicated by the ask.
|
||||
|
||||
@@ -330,6 +330,9 @@ type Holder struct {
|
||||
// CapPerHour is the most messages one channel gets an hour (the setting messages-per-hour, issue 1476);
|
||||
// nil, or a value below zero, is DefaultCapPerHour; 0 is no cap at all. TellCapPerHour is its own and stays fixed.
|
||||
CapPerHour func() int
|
||||
// RefusedKept is how many refused asks of one asker are kept (the setting refused-kept, design 47 G §570); nil is
|
||||
// DefaultRefusedKept.
|
||||
RefusedKept func() int
|
||||
// Settle is how long a delivered walk holds what it touched after its end (the setting settle-seconds, novox/hq
|
||||
// ADR 0505 §3); nil is DefaultSettle.
|
||||
Settle func() time.Duration
|
||||
|
||||
@@ -39,6 +39,8 @@ type Settings struct {
|
||||
// SettleSeconds is how long a delivered walk still holds what it touched after its end (novox/hq ADR 0505 §3):
|
||||
// nil, not given, is DefaultSettle; 0 holds nothing after the end.
|
||||
SettleSeconds *int `json:"settle-seconds"`
|
||||
// RefusedKept is how many refused asks of one asker the router keeps (design 47 G §570): nil, not given, is DefaultRefusedKept.
|
||||
RefusedKept *int `json:"refused-kept"`
|
||||
}
|
||||
|
||||
// zoneOf is the location a time-zone setting names, or this machine's own when it names none or one
|
||||
@@ -105,6 +107,17 @@ func readSettings(path string) (Settings, error) {
|
||||
default:
|
||||
return s, fmt.Errorf("settle-seconds is not a number")
|
||||
}
|
||||
switch v := loose["refused-kept"].(type) {
|
||||
case nil:
|
||||
case float64:
|
||||
n := int(v)
|
||||
if v < 1 || v != float64(n) {
|
||||
return s, fmt.Errorf("refused-kept is not a whole number of one or more")
|
||||
}
|
||||
s.RefusedKept = &n
|
||||
default:
|
||||
return s, fmt.Errorf("refused-kept is not a number")
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
@@ -242,6 +255,7 @@ func (askStore) UpdateAsk(key string, r AskRecord, revision uint64) (uint64, err
|
||||
rev, err := stdio.State("asks").Update(key, r, revision)
|
||||
return rev, changed(err)
|
||||
}
|
||||
func (askStore) DeleteAsk(key string) error { return stdio.State("asks").Delete(key) }
|
||||
func (askStore) AskKeys() ([]string, error) { return stdio.State("asks").Keys() }
|
||||
func (askStore) GetIdentity(key string) (*Identity, error) {
|
||||
e, err := stdio.State("identities").Get(key)
|
||||
@@ -366,6 +380,12 @@ func main() {
|
||||
}
|
||||
return DefaultCapPerHour
|
||||
},
|
||||
RefusedKept: func() int {
|
||||
if n := settings().RefusedKept; n != nil {
|
||||
return *n
|
||||
}
|
||||
return DefaultRefusedKept
|
||||
},
|
||||
Settle: func() time.Duration {
|
||||
if n := settings().SettleSeconds; n != nil && *n >= 0 {
|
||||
return time.Duration(*n) * time.Second
|
||||
|
||||
@@ -149,8 +149,9 @@ func TestAnAskWhoseWholeWordsCarryASecretIsRefused(t *testing.T) {
|
||||
if len(tg.sends) != 0 || len(got) != 1 || got[0].Outcome != occ.OutcomeRefused || !strings.Contains(got[0].Words, "whole words carry secret") {
|
||||
t.Errorf("%q: sent %d, told %+v", secret, len(tg.sends), got)
|
||||
}
|
||||
if _, kept := store.asks["mesh-controller.s-4"]; kept {
|
||||
t.Errorf("%q: the refused ask was recorded", secret)
|
||||
if rec, kept := store.asks["mesh-controller.s-4"]; !kept || rec.State != "refused" || rec.Ask.Whole != "" || rec.Ask.Headline != "" ||
|
||||
strings.Contains(fmt.Sprint(rec), "ghp_") || strings.Contains(fmt.Sprint(rec), "hunter2") {
|
||||
t.Errorf("%q: the refused ask was not kept as a closed record without its words: %+v", secret, rec)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -204,7 +205,7 @@ func TestEveryKeyAndItsWholeValueIsInTheNoteWhereTheSenderIsProven(t *testing.T)
|
||||
b.WholeDetails = "+ token: ghp_abcdefghijklmnopqrstuvwxyz0123456789"
|
||||
submit(t, h, "mesh-controller", b)
|
||||
settle(h, c)
|
||||
if _, kept := store.asks["mesh-controller.s-10"]; kept || len(tg.sends) != 1 {
|
||||
if rec, kept := store.asks["mesh-controller.s-10"]; (kept && rec.State != "refused") || rec.Ask.WholeDetails != "" || len(tg.sends) != 1 {
|
||||
t.Errorf("an ask whose whole details carry a secret was taken")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
"only-what-needs-me": [],
|
||||
"messages-per-hour": 100,
|
||||
"settle-seconds": 120,
|
||||
"refused-kept": 20,
|
||||
"readers": {"offered": ["mesh-knowledge"], "closed": ["mesh-knowledge"]}
|
||||
}
|
||||
|
||||
@@ -164,6 +164,14 @@
|
||||
"trusted": false,
|
||||
"default": 100
|
||||
},
|
||||
"refused-kept": {
|
||||
"type": "number",
|
||||
"description": "how many refused asks of one asker the router keeps as closed records, a whole number from 1 to 1000: the twenty-first refusal of an asker removes its oldest (design 47 G section 570).",
|
||||
"minimum": 1,
|
||||
"maximum": 1000,
|
||||
"trusted": false,
|
||||
"default": 20
|
||||
},
|
||||
"settle-seconds": {
|
||||
"type": "number",
|
||||
"description": "seconds a delivery that ended well still holds the messages about what it changed, so a problem that passes as the delivery settles is not sent (ADR 0505 §3); 0 holds nothing after the end.",
|
||||
@@ -194,7 +202,7 @@
|
||||
"type": "file",
|
||||
"path": "${dir:state}/settings.json",
|
||||
"mode": "0600",
|
||||
"takes-config": ["time-zone", "desktop-machines", "only-what-needs-me", "messages-per-hour", "settle-seconds", "readers"],
|
||||
"takes-config": ["time-zone", "desktop-machines", "only-what-needs-me", "messages-per-hour", "settle-seconds", "refused-kept", "readers"],
|
||||
"merge": "json",
|
||||
"template": "files/settings.json",
|
||||
"owner": "messenger"
|
||||
|
||||
Reference in New Issue
Block a user