diff --git a/modules/messenger/cmd/messenger/adr505_test.go b/modules/messenger/cmd/messenger/adr505_test.go new file mode 100644 index 00000000..f423f011 --- /dev/null +++ b/modules/messenger/cmd/messenger/adr505_test.go @@ -0,0 +1,44 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +// The tests of novox/hq ADR 0505 "How it is checked" for the router: what a delivery changes is held until it has +// settled, every problem message says whether a delivery ran, and a failed delivery is told at once. + +// ADR 0505 §3: the settle is a setting, settle-seconds, two minutes when none is given. +func TestTheSettleIsReadFromTheSettingsFileAndIsTwoMinutesByDefault(t *testing.T) { + read := func(body string) Settings { + p := filepath.Join(t.TempDir(), "settings.json") + if err := os.WriteFile(p, []byte(body), 0o600); err != nil { + t.Fatal(err) + } + s, err := readSettings(p) + if err != nil { + t.Fatal(err) + } + return s + } + if s := read(`{"settle-seconds": 60}`); s.SettleSeconds == nil || *s.SettleSeconds != 60 { + t.Fatalf("set: %+v", s) + } + if s := read(`{"time-zone": ""}`); s.SettleSeconds != nil { + t.Fatalf("unset: %+v", s) + } + h := &Holder{} + if got := h.settle(); got != 2*time.Minute { + t.Fatalf("no setting: settle %s, want 2m", got) + } + h.Settle = func() time.Duration { return time.Minute } + if got := h.settle(); got != time.Minute { + t.Fatalf("a setting of 60 s: settle %s, want 1m", got) + } + h.Settle = func() time.Duration { return 0 } + if got := h.settle(); got != 0 { + t.Fatalf("a setting of 0: settle %s, want none", got) + } +} diff --git a/modules/messenger/cmd/messenger/holder.go b/modules/messenger/cmd/messenger/holder.go index 076fc270..1c83d925 100644 --- a/modules/messenger/cmd/messenger/holder.go +++ b/modules/messenger/cmd/messenger/holder.go @@ -327,6 +327,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 + // 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 work sync.Mutex // one event, call or tick at a time mu sync.Mutex // what the status reads diff --git a/modules/messenger/cmd/messenger/main.go b/modules/messenger/cmd/messenger/main.go index 5fbfa29b..57bccaaa 100644 --- a/modules/messenger/cmd/messenger/main.go +++ b/modules/messenger/cmd/messenger/main.go @@ -35,6 +35,9 @@ type Settings struct { // MessagesPerHour is the most messages one channel gets in an hour (issue 1476); 0, not given, is // DefaultCapPerHour; 0 is no cap at all. MessagesPerHour *int `json:"messages-per-hour"` + // 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"` } // zoneOf is the location a time-zone setting names, or this machine's own when it names none or one @@ -90,6 +93,17 @@ func readSettings(path string) (Settings, error) { default: return s, fmt.Errorf("only-what-needs-me is not a list of channel kinds") } + switch v := loose["settle-seconds"].(type) { + case nil: + case float64: + n := int(v) + if v < 0 || v != float64(n) { + return s, fmt.Errorf("settle-seconds is not a whole number of zero or more") + } + s.SettleSeconds = &n + default: + return s, fmt.Errorf("settle-seconds is not a number") + } return s, nil } @@ -351,6 +365,12 @@ func main() { } return DefaultCapPerHour }, + Settle: func() time.Duration { + if n := settings().SettleSeconds; n != nil && *n >= 0 { + return time.Duration(*n) * time.Second + } + return DefaultSettle + }, NeedsOnly: func(kind string) bool { for _, k := range settings().OnlyWhatNeedsMe { if k == kind { diff --git a/modules/messenger/cmd/messenger/walks.go b/modules/messenger/cmd/messenger/walks.go index 365937f7..d16dce8c 100644 --- a/modules/messenger/cmd/messenger/walks.go +++ b/modules/messenger/cmd/messenger/walks.go @@ -68,6 +68,9 @@ const ( // DefaultWalkBound is a walk's bound when the controller gives none: the bound of a walk none of whose // deliveries has a budget (ADR 0308 §5). DefaultWalkBound = 15 * time.Minute + // DefaultSettle is how long a delivered walk still holds what it touched after its end when the setting + // settle-seconds gives none: the longest the first-node gate holds a healthy module (novox/hq ADR 0505 §3, ADR 0352). + DefaultSettle = 2 * time.Minute // WalkForget: a walk whose end never came is forgotten this long after it started. WalkForget = 24 * time.Hour // WalkEndWhat is the outbox's name for a walk's end or bound message: it goes alone, never in a digest. @@ -76,6 +79,17 @@ const ( MostNamed = 6 ) +// settle is how long a delivered walk still holds after its end (ADR 0505 §3). +func (h *Holder) settle() time.Duration { + if h.Settle == nil { + return DefaultSettle + } + if d := h.Settle(); d >= 0 { + return d + } + return DefaultSettle +} + // Touch is what a walk moves on one machine. type Touch struct { Machine string `json:"machine"` diff --git a/modules/messenger/files/settings.json b/modules/messenger/files/settings.json index 6d22854a..726c29cb 100644 --- a/modules/messenger/files/settings.json +++ b/modules/messenger/files/settings.json @@ -1,5 +1,6 @@ { "time-zone": "", "only-what-needs-me": [], - "messages-per-hour": 100 + "messages-per-hour": 100, + "settle-seconds": 120 } diff --git a/modules/messenger/module.json b/modules/messenger/module.json index 60451958..d7da1d68 100644 --- a/modules/messenger/module.json +++ b/modules/messenger/module.json @@ -140,6 +140,14 @@ "maximum": 1000, "trusted": false, "default": 100 + }, + "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.", + "minimum": 0, + "maximum": 3600, + "trusted": false, + "default": 120 } }, "resources": [ @@ -163,7 +171,7 @@ "type": "file", "path": "${dir:state}/settings.json", "mode": "0600", - "takes-config": ["time-zone", "desktop-machines", "only-what-needs-me", "messages-per-hour"], + "takes-config": ["time-zone", "desktop-machines", "only-what-needs-me", "messages-per-hour", "settle-seconds"], "merge": "json", "template": "files/settings.json", "owner": "messenger"