Make the messenger's per-channel hourly cap the setting messages-per-hour, default 100 (issue 1476)
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestTheCapFollowsTheSetting(t *testing.T) {
|
||||
h, tg, _, c, _ := newHolder(t)
|
||||
h.Desk = nil
|
||||
h.CapPerHour = func() int { return 3 }
|
||||
for i := 0; i < 5; i++ {
|
||||
h.Condition(EventRaised, cond(fmt.Sprintf("machine.m%d.silent", i), Urgent, "a machine is silent"))
|
||||
c.pass(BurstWindow + time.Second)
|
||||
h.Flush()
|
||||
}
|
||||
if len(tg.sends) != 4 || !strings.Contains(tg.sends[3].Body, "sent 3 messages") {
|
||||
t.Fatalf("sent %d, want 3 and the cap said", len(tg.sends))
|
||||
}
|
||||
if cap := h.Status("listening").Channels[0].Cap; cap != 3 {
|
||||
t.Fatalf("status cap %d", cap)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessagesPerHourIsReadFromTheSettingsFile(t *testing.T) {
|
||||
read := func(body string) (Settings, error) {
|
||||
p := filepath.Join(t.TempDir(), "settings.json")
|
||||
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return readSettings(p)
|
||||
}
|
||||
if s, err := read(`{"messages-per-hour": 100}`); err != nil || s.MessagesPerHour != 100 {
|
||||
t.Fatalf("set: %+v %v", s, err)
|
||||
}
|
||||
if s, err := read(`{"time-zone": ""}`); err != nil || s.MessagesPerHour != 0 {
|
||||
t.Fatalf("unset: %+v %v", s, err)
|
||||
}
|
||||
}
|
||||
@@ -72,10 +72,11 @@ const (
|
||||
// ClearSettle: a clearing is said once the condition stayed cleared this long. One raised again sooner
|
||||
// (a unit failing again after each apply, issue 373) continues its message without a word, and its
|
||||
// clearing is never said. Once it has come back, its next clearing waits the reopen window less a minute.
|
||||
ClearSettle = 2 * time.Minute
|
||||
CapPerHour = 20
|
||||
KeptSends = 200
|
||||
KeptRefusals = 50
|
||||
ClearSettle = 2 * time.Minute
|
||||
// DefaultCapPerHour: the messages one channel gets in an hour when the setting messages-per-hour gives none.
|
||||
DefaultCapPerHour = 100
|
||||
KeptSends = 200
|
||||
KeptRefusals = 50
|
||||
|
||||
// FreshFor: an event older than this by its own time is history — state, never a message.
|
||||
FreshFor = 10 * time.Minute
|
||||
@@ -323,6 +324,9 @@ type Holder struct {
|
||||
// NeedsOnly says whether a kind is set to say only what needs the operator (the setting
|
||||
// `only-what-needs-me`, ADR 0308 §4); nil, none is.
|
||||
NeedsOnly func(kind string) bool
|
||||
// CapPerHour is the most messages one channel gets an hour (the setting messages-per-hour, issue 1476);
|
||||
// nil, or a value below one, is DefaultCapPerHour. TellCapPerHour is its own and stays fixed.
|
||||
CapPerHour func() int
|
||||
|
||||
work sync.Mutex // one event, call or tick at a time
|
||||
mu sync.Mutex // what the status reads
|
||||
@@ -1207,3 +1211,13 @@ func withheldLine(line string, refusal Refusal, passes func(string) (Refusal, bo
|
||||
}
|
||||
return "(a line carrying " + refusal.What + " is withheld here)"
|
||||
}
|
||||
|
||||
// capPerHour is the cap now in force on a channel's messages an hour.
|
||||
func (h *Holder) capPerHour() int {
|
||||
if h.CapPerHour != nil {
|
||||
if n := h.CapPerHour(); n >= 1 {
|
||||
return n
|
||||
}
|
||||
}
|
||||
return DefaultCapPerHour
|
||||
}
|
||||
|
||||
@@ -205,6 +205,8 @@ func TestAWarningGoesToTheDesktopWhenASessionAnswersElseTelegram(t *testing.T) {
|
||||
func TestTheCapIsALastLineAndIsSaidOnce(t *testing.T) {
|
||||
h, tg, _, c, _ := newHolder(t)
|
||||
h.Desk = nil
|
||||
const capHere = 20
|
||||
h.CapPerHour = func() int { return capHere }
|
||||
// Urgent conditions, each alone and after a quiet half minute: each is its own message — until
|
||||
// the cap, which is said once, in a message of its own, and holds the rest.
|
||||
for i := 0; i < 25; i++ {
|
||||
@@ -212,27 +214,27 @@ func TestTheCapIsALastLineAndIsSaidOnce(t *testing.T) {
|
||||
c.pass(BurstWindow + time.Second)
|
||||
h.Flush()
|
||||
}
|
||||
if len(tg.sends) != CapPerHour+1 {
|
||||
t.Fatalf("sent %d, want the cap of %d and one saying so", len(tg.sends), CapPerHour)
|
||||
if len(tg.sends) != capHere+1 {
|
||||
t.Fatalf("sent %d, want the cap of %d and one saying so", len(tg.sends), capHere)
|
||||
}
|
||||
if last := tg.sends[CapPerHour]; !strings.HasPrefix(last.Title, "Mesh messages paused for this hour") {
|
||||
if last := tg.sends[capHere]; !strings.HasPrefix(last.Title, "Mesh messages paused for this hour") {
|
||||
t.Fatalf("the cap was not said: %q", last.Title)
|
||||
}
|
||||
st := h.Status("listening")
|
||||
if st.Channels[0].Held != 5 || st.Channels[0].SentLastHour != CapPerHour {
|
||||
if st.Channels[0].Held != 5 || st.Channels[0].SentLastHour != capHere {
|
||||
t.Fatalf("status: %+v", st.Channels[0])
|
||||
}
|
||||
for i := 0; i < 10; i++ {
|
||||
c.pass(time.Minute)
|
||||
h.Tick()
|
||||
}
|
||||
if len(tg.sends) != CapPerHour+1 {
|
||||
if len(tg.sends) != capHere+1 {
|
||||
t.Fatalf("said the cap again or sent past it: %d", len(tg.sends))
|
||||
}
|
||||
// The hour frees the channel: what was held goes out as one digest.
|
||||
c.pass(time.Hour)
|
||||
h.Tick()
|
||||
if len(tg.sends) != CapPerHour+2 {
|
||||
if len(tg.sends) != capHere+2 {
|
||||
t.Fatalf("sends %d", len(tg.sends))
|
||||
}
|
||||
d := tg.sends[len(tg.sends)-1]
|
||||
@@ -579,14 +581,14 @@ func TestATellIsDroppedPastItsOwnHourlyCap(t *testing.T) {
|
||||
func TestATellDoesNotCountTowardTheConditionsCap(t *testing.T) {
|
||||
h, tg, _, c, _ := newHolder(t)
|
||||
h.Desk = nil
|
||||
// Bring conditions close to, but under, their own cap: CapPerHour-5.
|
||||
for i := 0; i < CapPerHour-5; i++ {
|
||||
// Bring conditions close to, but under, their own cap: DefaultCapPerHour-5.
|
||||
for i := 0; i < DefaultCapPerHour-5; i++ {
|
||||
h.Condition(EventRaised, cond(fmt.Sprintf("machine.m%d.silent", i), Urgent, "a machine is silent"))
|
||||
c.pass(BurstWindow + time.Second)
|
||||
h.Flush()
|
||||
}
|
||||
if len(tg.sends) != CapPerHour-5 {
|
||||
t.Fatalf("sent %d conditions, want %d", len(tg.sends), CapPerHour-5)
|
||||
if len(tg.sends) != DefaultCapPerHour-5 {
|
||||
t.Fatalf("sent %d conditions, want %d", len(tg.sends), DefaultCapPerHour-5)
|
||||
}
|
||||
// A full hour of tells: these must not be counted against the conditions' cap (ADR 0380 §8).
|
||||
for i := 0; i < TellCapPerHour; i++ {
|
||||
@@ -597,7 +599,7 @@ func TestATellDoesNotCountTowardTheConditionsCap(t *testing.T) {
|
||||
h.Condition(EventRaised, cond("machine.mlast.silent", Urgent, "a machine is silent"))
|
||||
c.pass(BurstWindow + time.Second)
|
||||
h.Flush()
|
||||
want := CapPerHour - 5 + TellCapPerHour + 1
|
||||
want := DefaultCapPerHour - 5 + TellCapPerHour + 1
|
||||
if len(tg.sends) != want {
|
||||
t.Fatalf("sent %d, want %d (the condition after the tells still sent)", len(tg.sends), want)
|
||||
}
|
||||
@@ -644,14 +646,14 @@ func TestATellCapDropIsLoggedOnceAnHour(t *testing.T) {
|
||||
func TestAFloodOfDroppedTellsDoesNotEvictTheConditionsCapHistory(t *testing.T) {
|
||||
h, tg, _, c, _ := newHolder(t)
|
||||
h.Desk = nil
|
||||
// Fill the conditions' cap exactly: CapPerHour sent, none held yet.
|
||||
for i := 0; i < CapPerHour; i++ {
|
||||
// Fill the conditions' cap exactly: DefaultCapPerHour sent, none held yet.
|
||||
for i := 0; i < DefaultCapPerHour; i++ {
|
||||
h.Condition(EventRaised, cond(fmt.Sprintf("machine.m%d.silent", i), Urgent, "a machine is silent"))
|
||||
c.pass(BurstWindow + time.Second)
|
||||
h.Flush()
|
||||
}
|
||||
if len(tg.sends) != CapPerHour {
|
||||
t.Fatalf("sent %d conditions, want the cap of %d", len(tg.sends), CapPerHour)
|
||||
if len(tg.sends) != DefaultCapPerHour {
|
||||
t.Fatalf("sent %d conditions, want the cap of %d", len(tg.sends), DefaultCapPerHour)
|
||||
}
|
||||
|
||||
// Use up the tell cap, then flood 300 more: all past TellCapPerHour are dropped, far more than KeptSends.
|
||||
@@ -670,7 +672,7 @@ func TestAFloodOfDroppedTellsDoesNotEvictTheConditionsCapHistory(t *testing.T) {
|
||||
h.Condition(EventRaised, cond("machine.mlast.silent", Urgent, "a machine is silent"))
|
||||
c.pass(BurstWindow + time.Second)
|
||||
h.Flush()
|
||||
want := CapPerHour + TellCapPerHour + 1 // the cap's own conditions, the tells that did send, and the cap message
|
||||
want := DefaultCapPerHour + TellCapPerHour + 1 // the cap's own conditions, the tells that did send, and the cap message
|
||||
if len(tg.sends) != want {
|
||||
t.Fatalf("sent %d, want %d (the cap still held after a flood of dropped tells)", len(tg.sends), want)
|
||||
}
|
||||
@@ -688,3 +690,22 @@ func TestATestMessageRespectsTheRule(t *testing.T) {
|
||||
t.Fatalf("%v", out)
|
||||
}
|
||||
}
|
||||
|
||||
// The cap is a setting, messages-per-hour (issue 1476), and 100 when none is given: the 21st to the 100th
|
||||
// message of an hour still goes out, the 101st is held and said so with the real number.
|
||||
func TestTheCapUnsetIsAHundredAnHour(t *testing.T) {
|
||||
h, tg, _, c, _ := newHolder(t)
|
||||
h.Desk = nil
|
||||
for i := 0; i < 101; i++ {
|
||||
h.Condition(EventRaised, cond(fmt.Sprintf("machine.m%d.silent", i), Urgent, "a machine is silent"))
|
||||
c.pass(BurstWindow + time.Second)
|
||||
h.Flush()
|
||||
}
|
||||
if len(tg.sends) != 101 {
|
||||
t.Fatalf("sent %d, want 100 and one saying the cap", len(tg.sends))
|
||||
}
|
||||
last := tg.sends[100]
|
||||
if !strings.HasPrefix(last.Title, "Mesh messages paused for this hour") || !strings.Contains(last.Body, "sent 100 messages") {
|
||||
t.Fatalf("the cap was not said with its number: %q %q", last.Title, last.Body)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,9 @@ type Settings struct {
|
||||
// OnlyWhatNeedsMe are the channel kinds set to say only what needs the operator: a walk that delivered
|
||||
// with nothing held and nothing open is not told there at all (novox/hq ADR 0308 §4).
|
||||
OnlyWhatNeedsMe []string `json:"only-what-needs-me"`
|
||||
// MessagesPerHour is the most messages one channel gets in an hour (issue 1476); 0, not given, is
|
||||
// DefaultCapPerHour.
|
||||
MessagesPerHour int `json:"messages-per-hour"`
|
||||
}
|
||||
|
||||
// zoneOf is the location a time-zone setting names, or this machine's own when it names none or one
|
||||
@@ -63,6 +66,13 @@ func readSettings(path string) (Settings, error) {
|
||||
if tz, ok := loose["time-zone"].(string); ok {
|
||||
s.TimeZone = strings.TrimSpace(tz)
|
||||
}
|
||||
switch v := loose["messages-per-hour"].(type) {
|
||||
case nil:
|
||||
case float64:
|
||||
s.MessagesPerHour = int(v)
|
||||
default:
|
||||
return s, fmt.Errorf("messages-per-hour is not a number")
|
||||
}
|
||||
switch v := loose["only-what-needs-me"].(type) {
|
||||
case nil:
|
||||
case []any:
|
||||
@@ -330,7 +340,8 @@ func main() {
|
||||
}
|
||||
return loc
|
||||
},
|
||||
Emit: func(event string, body any) error { return stdio.Emit(event, body) },
|
||||
Emit: func(event string, body any) error { return stdio.Emit(event, body) },
|
||||
CapPerHour: func() int { return settings().MessagesPerHour },
|
||||
NeedsOnly: func(kind string) bool {
|
||||
for _, k := range settings().OnlyWhatNeedsMe {
|
||||
if k == kind {
|
||||
|
||||
@@ -318,7 +318,7 @@ func (h *Holder) flushChannel(ch Channel, now time.Time, force bool) {
|
||||
if said.IsZero() || now.Sub(said) >= time.Hour {
|
||||
m := Message{Title: "Mesh messages paused for this hour",
|
||||
Body: fmt.Sprintf("The mesh sent %d messages here this hour, its limit. %d more are held and come as "+
|
||||
"one message when the hour allows.", CapPerHour, len(entries)), Urgent: true}
|
||||
"one message when the hour allows.", h.capPerHour(), len(entries)), Urgent: true}
|
||||
_, err := ch.Send(m)
|
||||
h.noteChannel(name, err)
|
||||
if err == nil {
|
||||
@@ -613,7 +613,7 @@ func (h *Holder) sendNow(ch Channel, key, what string, m Message) bool {
|
||||
// tell is not counted here: it has its own cap (TellCapPerHour) so a burst of tells cannot crowd
|
||||
// out a condition or an ask (ADR 0380 §8).
|
||||
func (h *Holder) allow(channel string) bool {
|
||||
return h.sentLastHour(channel) < CapPerHour
|
||||
return h.sentLastHour(channel) < h.capPerHour()
|
||||
}
|
||||
|
||||
func (h *Holder) sentLastHour(channel string) int {
|
||||
|
||||
@@ -47,7 +47,7 @@ func (h *Holder) Status(listening string) Status {
|
||||
now := h.Now()
|
||||
var st Status
|
||||
for _, ch := range h.kindChannels() {
|
||||
cs := ChannelStatus{Name: ch.Name(), Cap: CapPerHour, SentLastHour: h.sentLastHour(ch.Name())}
|
||||
cs := ChannelStatus{Name: ch.Name(), Cap: h.capPerHour(), SentLastHour: h.sentLastHour(ch.Name())}
|
||||
if err := ch.Ready(); err != nil {
|
||||
cs.NotReady = err.Error()
|
||||
} else {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"time-zone": "",
|
||||
"only-what-needs-me": []
|
||||
"only-what-needs-me": [],
|
||||
"messages-per-hour": 100
|
||||
}
|
||||
|
||||
@@ -132,6 +132,14 @@
|
||||
"description": "The kinds of channel set to say only what needs the operator: a walk that delivered with nothing held and nothing open is not told there (ADR 0308 §4); empty is none.",
|
||||
"trusted": true,
|
||||
"default": []
|
||||
},
|
||||
"messages-per-hour": {
|
||||
"type": "number",
|
||||
"description": "The most messages the mesh sends one channel in an hour, in messages per channel per hour; past it the rest are held and said once as one message (not the tell verb, which has its own cap of 10).",
|
||||
"minimum": 1,
|
||||
"maximum": 1000,
|
||||
"trusted": false,
|
||||
"default": 100
|
||||
}
|
||||
},
|
||||
"resources": [
|
||||
@@ -155,7 +163,7 @@
|
||||
"type": "file",
|
||||
"path": "${dir:state}/settings.json",
|
||||
"mode": "0600",
|
||||
"takes-config": ["time-zone", "desktop-machines", "only-what-needs-me"],
|
||||
"takes-config": ["time-zone", "desktop-machines", "only-what-needs-me", "messages-per-hour"],
|
||||
"merge": "json",
|
||||
"template": "files/settings.json",
|
||||
"owner": "messenger"
|
||||
|
||||
Reference in New Issue
Block a user