Make the settle after a delivered walk a setting, two minutes by default (ADR 0505 §3)

This commit is contained in:
2026-10-22 14:02:17 +02:00
parent b7b9f2a3da
commit 92225cd528
6 changed files with 92 additions and 2 deletions
@@ -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)
}
}
@@ -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
+20
View File
@@ -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 {
+14
View File
@@ -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"`
+2 -1
View File
@@ -1,5 +1,6 @@
{
"time-zone": "",
"only-what-needs-me": [],
"messages-per-hour": 100
"messages-per-hour": 100,
"settle-seconds": 120
}
+9 -1
View File
@@ -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"