C4: a licence's room is its weekly window alone (issue 1409)
mesh/delivery merged: source.main moved to novox/mesh-catalog@f9dcbc295723
mesh/check checking 94a0ebbedfa6 on f9dcbc295723

The vendor retired the session limit and answers the session window empty, so Room required a percentage that never
comes: live, no licence had room and no switch could choose a target. Room is now 100 less the weekly percentage; the
session window takes no part in the switch threshold, a target's bound, the near-limit warning or no-room's words.
A reading with no weekly percentage still has unknown room. The settings' descriptions and the README say so.

The whole tests' vendor answers the session window empty, as it does live; on the old rule they failed with the live
symptom ("has no session or weekly window in its reading").

Builds: design 39 bus-and-switch C4
This commit is contained in:
2026-11-12 10:57:19 +01:00
parent c5f6845386
commit 94a0ebbedf
9 changed files with 70 additions and 67 deletions
+4 -4
View File
@@ -55,10 +55,10 @@ reads alone and answers every act with "the carry-over has not ended"; a failed
## Switching on usage
Every `usage_minutes` the manager reads each subscription's usage with its access token from the vault: the session,
weekly and sonnet windows, each with its reset time. A licence's room is 100 less the higher of its session and weekly
percentages, known only when it was read within `usage_stale_minutes` and its refresh is healthy. When a reading puts
a licence with consumers at or above `switch_at_pct`, every consumer bound to it moves, in one act, to the licence
with the most room whose windows are both below `room_below_pct`: one `binding.changed` per consumer, one
weekly and sonnet windows, each with its reset time. A licence's room is 100 less its weekly percentage alone (the session
window takes no part in room, the switch, the warning or the conditions: the vendor retired the session limit), known only when it was read within `usage_stale_minutes` and its refresh is healthy. When a reading puts
a licence with consumers at or above `switch_at_pct` in its weekly window, every consumer bound to it moves, in one act, to the licence
with the most room whose weekly window is below `room_below_pct`: one `binding.changed` per consumer, one
`licence.switched`, and one `operator-channel.tell`. A consumer is never moved back by the rule.
Its conditions are said on the operator channel's condition subject, the router deciding where they go:
@@ -227,16 +227,16 @@ func (m *Manager) unread(l Licence, s Settings) (string, bool) {
func (m *Manager) roomWords(o Licence, s Settings) string {
room, known := Room(o, m.Now(), s)
switch {
case known && (*o.Usage.SessionPct >= s.RoomBelow || *o.Usage.WeeklyPct >= s.RoomBelow):
return fmt.Sprintf("has %g %% room, a window not below %g %%", room, s.RoomBelow)
case known && *o.Usage.WeeklyPct >= s.RoomBelow:
return fmt.Sprintf("has %g %% room, its weekly window not below %g %%", room, s.RoomBelow)
case known:
return fmt.Sprintf("has %g %% room", room)
case o.Kind != "subscription":
return "is an API key, whose room is never known"
case o.Usage == nil || o.Usage.ReadAt == "":
return "has no reading"
case o.Usage.SessionPct == nil || o.Usage.WeeklyPct == nil:
return "has no session or weekly window in its reading"
case o.Usage.WeeklyPct == nil:
return "has no weekly window in its reading"
case o.Failures >= s.FailuresToNotify || (o.RefreshExpiresAt != 0 && o.RefreshExpiresAt <= m.Now().UnixMilli()):
return "does not refresh"
}
@@ -140,7 +140,7 @@ func TestNoRoomIsSaidOnceAndClearedByRoom(t *testing.T) {
ctx := context.Background()
mm := fourOnA(t)
_, said := saying(mm)
_ = mm.store.SaveLicence(ctx, sub("b", read(95, 20)))
_ = mm.store.SaveLicence(ctx, sub("b", read(20, 95)))
_ = mm.store.SaveLicence(ctx, sub("c", read(40, 99)))
for range 2 {
_ = mm.m.SayConditions(ctx, false)
@@ -158,7 +158,7 @@ func TestNoRoomIsSaidOnceAndClearedByRoom(t *testing.T) {
if c.Severity != occ.SeverityUrgent || c.Headline != "No licence has room for 4 machines" ||
c.Needs != "add a licence, or wait for a's weekly window to reset at 2026-11-14T09:00:00Z" ||
!strings.Contains(c.Explanation, "a is at 99 % of its weekly window, with ace, g14, laptop, server bound to it") ||
!strings.Contains(c.Explanation, "b has 5 % room, a window not below 90 %") ||
!strings.Contains(c.Explanation, "b has 5 % room, its weekly window not below 90 %") ||
c.Resolved != "ace, g14, laptop, server have a licence with room again" {
t.Fatalf("no-room %+v", c)
}
@@ -178,7 +178,7 @@ func TestNearLimitIsSaidOncePerCrossing(t *testing.T) {
key := "licence.a.near-limit"
var last oc.Condition
for _, w := range []float64{80, 89, 91, 91, 95} {
_ = mm.store.SaveLicence(ctx, sub("a", read(10, w)))
_ = mm.store.SaveLicence(ctx, sub("a", read(95, w))) // a session at 95 % takes no part (C4)
_ = mm.m.SayConditions(ctx, false)
for _, s := range *said {
if s.Key == key {
@@ -191,8 +191,8 @@ func TestNearLimitIsSaidOncePerCrossing(t *testing.T) {
!strings.Contains(last.Explanation, "which resets at 2026-11-14T09:00:00Z") {
t.Fatalf("said %v, last %+v", of(*said, key), last)
}
_ = mm.store.SaveLicence(ctx, sub("b", read(95, 0)))
_ = mm.store.SaveLicence(ctx, sub("c", read(95, 0)))
_ = mm.store.SaveLicence(ctx, sub("b", read(0, 95)))
_ = mm.store.SaveLicence(ctx, sub("c", read(0, 95)))
_ = mm.m.SayConditions(ctx, false)
if c := (*said)[len(*said)-1]; c.Key != key || c.Needs != "no other licence has room" {
t.Fatalf("with no target said %+v", c)
@@ -1,18 +1,15 @@
package main
// A licence's room (design 39 bus-and-switch §25, §48): 100 less the higher of its session and weekly percentages in
// its latest reading, known only for a subscription read successfully within usage_stale_minutes, whose refresh has not
// A licence's room (design 39 bus-and-switch §25, §48, C4): 100 less its weekly percentage in its latest reading, the
// session window taking no part (the vendor retired the session limit), known only for a subscription read successfully within usage_stale_minutes, whose refresh has not
// failed failures_to_notify times in a row and whose refresh token has not expired. An API key's room is never known,
// so it is never a target.
import (
"math"
"time"
)
import "time"
// Room is a licence's room at now, and whether it is known.
func Room(l Licence, now time.Time, s Settings) (float64, bool) {
if l.Kind != "subscription" || l.Usage == nil || l.Usage.SessionPct == nil || l.Usage.WeeklyPct == nil {
if l.Kind != "subscription" || l.Usage == nil || l.Usage.WeeklyPct == nil {
return 0, false
}
read, err := time.Parse(time.RFC3339Nano, l.Usage.ReadAt)
@@ -22,5 +19,5 @@ func Room(l Licence, now time.Time, s Settings) (float64, bool) {
if l.Failures >= s.FailuresToNotify || (l.RefreshExpiresAt != 0 && l.RefreshExpiresAt <= now.UnixMilli()) {
return 0, false
}
return 100 - math.Max(*l.Usage.SessionPct, *l.Usage.WeeklyPct), true
return 100 - *l.Usage.WeeklyPct, true
}
@@ -89,7 +89,7 @@ func TestAFailedReadingKeepsThePercentages(t *testing.T) {
}
}
// design 39 bus-and-switch §25/AC1, §48: a table of readings, ages and healths and the room each gives.
// design 39 bus-and-switch §25/AC1, §48, C4: a table of readings, ages and healths and the room each gives.
func TestTheRoomOfEachLicence(t *testing.T) {
now := t0
pct := func(v float64) *float64 { return &v }
@@ -102,8 +102,10 @@ func TestTheRoomOfEachLicence(t *testing.T) {
room float64
known bool
}{
{"the higher window counts", Licence{Kind: "subscription", Usage: read(41, 97, time.Minute)}, 3, true},
{"the session can be the higher", Licence{Kind: "subscription", Usage: read(80, 20, time.Minute)}, 20, true},
{"the weekly window counts", Licence{Kind: "subscription", Usage: read(41, 97, time.Minute)}, 3, true},
{"the session takes no part (C4)", Licence{Kind: "subscription", Usage: read(80, 20, time.Minute)}, 80, true},
{"no session window, weekly 33 (C4)", Licence{Kind: "subscription", Usage: &UsageReading{WeeklyPct: pct(33),
ReadAt: now.Format(time.RFC3339Nano)}}, 67, true},
{"read at the stale bound", Licence{Kind: "subscription", Usage: read(10, 10, 15*time.Minute)}, 90, true},
{"read before the stale bound", Licence{Kind: "subscription", Usage: read(10, 10, 16*time.Minute)}, 0, false},
{"a failure since keeps the room", Licence{Kind: "subscription", Usage: func() *UsageReading {
@@ -1,9 +1,9 @@
package main
// The switch rule (design 39 bus-and-switch §26, §27, §28, §32): after a reading of licence L, when switch_auto is on and
// L, with one or more consumers bound to it, is at or above switch_at_pct in its session or weekly window, every
// consumer bound to L moves, in one act, to the other licence with known room whose windows are both below
// room_below_pct and that has the most room, the first by name on a tie. The act is said as one licence.switched, a
// L, with one or more consumers bound to it, is at or above switch_at_pct in its weekly window (C4: the session window takes
// no part), every consumer bound to L moves, in one act, to the other licence with known room whose weekly window is
// below room_below_pct and that has the most room, the first by name on a tie. The act is said as one licence.switched, a
// binding.changed per consumer, one audit entry switched, and told to the operator. A consumer is only ever moved off
// a licence at its threshold, so none flaps between two.
@@ -61,24 +61,17 @@ func consumersOf(licence string, bindings []Binding) []string {
return consumers
}
// windowAt is l's window at or above pct, the higher of the two, the session window on a tie (B36): its name,
// percentage and reset; ok false when neither is.
// windowAt is l's weekly window when it is at or above pct (C4: the session window takes no part): its name,
// percentage and reset; ok false when it is not.
func windowAt(u *UsageReading, pct float64) (window string, at float64, resetsAt string, ok bool) {
if u == nil {
if u == nil || u.WeeklyPct == nil || *u.WeeklyPct < pct {
return "", 0, "", false
}
session, weekly := u.SessionPct, u.WeeklyPct
switch {
case session != nil && *session >= pct && (weekly == nil || *session >= *weekly):
return "session", *session, u.SessionResetsAt, true
case weekly != nil && *weekly >= pct:
return "weekly", *weekly, u.WeeklyResetsAt, true
}
return "", 0, "", false
return "weekly", *u.WeeklyPct, u.WeeklyResetsAt, true
}
// targetFor is the licence §26 moves l's consumers to: among the others with known room whose windows are both below
// room_below_pct, the one with the most room, the first by name on a tie; empty when none has room.
// targetFor is the licence §26 moves l's consumers to: among the others with known room whose weekly window is below
// room_below_pct (C4), the one with the most room, the first by name on a tie; empty when none has room.
func (m *Manager) targetFor(l Licence, licences []Licence, s Settings) (string, float64) {
others := append([]Licence(nil), licences...)
sort.Slice(others, func(i, j int) bool { return others[i].Name < others[j].Name })
@@ -88,7 +81,7 @@ func (m *Manager) targetFor(l Licence, licences []Licence, s Settings) (string,
continue
}
room, known := Room(o, m.Now(), s)
if !known || *o.Usage.SessionPct >= s.RoomBelow || *o.Usage.WeeklyPct >= s.RoomBelow || room <= best {
if !known || *o.Usage.WeeklyPct >= s.RoomBelow || room <= best {
continue
}
best, to = room, o.Name
@@ -14,11 +14,18 @@ func read(session, weekly float64) *UsageReading {
WeeklyResetsAt: "2026-11-14T09:00:00Z", ReadAt: t0.UTC().Format(time.RFC3339Nano)}
}
// weekly is a reading with no session window, as the vendor answers since the session limit was retired (C4).
func weekly(pct float64) *UsageReading {
u := read(0, pct)
u.SessionPct, u.SessionResetsAt = nil, ""
return u
}
func sub(name string, u *UsageReading) Licence {
return Licence{Name: name, Kind: "subscription", Generation: 1, Usage: u}
}
// design 39 bus-and-switch §26/AC1, §25, §48: a table of licences, readings and bindings and the moves each gives.
// design 39 bus-and-switch §26/AC1, §25, §48, C4: a table of licences, readings and bindings and the moves each gives.
func TestTheRuleMovesEachTable(t *testing.T) {
mm := newMesh(t)
on := func(consumers ...string) []Binding {
@@ -41,12 +48,15 @@ func TestTheRuleMovesEachTable(t *testing.T) {
{"no consumer", []Licence{sub("a", read(99, 99)), sub("b", read(0, 0))}, nil, "", "", false, 0},
{"the weekly window at 99", []Licence{sub("a", read(40, 99)), sub("b", read(10, 30)), sub("c", read(5, 20))},
on("x", "y", "z", "w"), "c", "weekly", false, 4},
{"the session window at 100", []Licence{sub("a", read(100, 50)), sub("b", read(10, 30))}, on("x"), "b", "session", false, 1},
{"a tie goes to the first by name", []Licence{sub("a", read(99, 0)), sub("c", read(10, 10)), sub("b", read(10, 10))},
on("x"), "b", "session", false, 1},
{"a target's windows both below room_below_pct", []Licence{sub("a", read(99, 0)), sub("b", read(0, 90)),
sub("c", read(89, 89))}, on("x"), "c", "session", false, 1},
{"no other licence has room", []Licence{sub("a", read(99, 0)), sub("b", read(95, 0)),
{"the session window at 100 moves nothing (C4)", []Licence{sub("a", read(100, 50)), sub("b", read(10, 30))},
on("x"), "", "", false, 0},
{"no session window, weekly 99, to weekly 33 (C4)", []Licence{sub("a", weekly(99)), sub("b", weekly(33))},
on("x", "y"), "b", "weekly", false, 2},
{"a tie goes to the first by name", []Licence{sub("a", read(0, 99)), sub("c", read(10, 10)), sub("b", read(10, 10))},
on("x"), "b", "weekly", false, 1},
{"a target's weekly below room_below_pct, its session not counted (C4)", []Licence{sub("a", read(0, 99)),
sub("b", read(0, 90)), sub("c", read(95, 89))}, on("x"), "c", "weekly", false, 1},
{"no other licence has room", []Licence{sub("a", read(0, 99)), sub("b", read(0, 95)),
{Name: "k", Kind: "api-key", Generation: 1}}, on("x"), "", "", true, 0},
{"an unread licence is no target", []Licence{sub("a", read(0, 99)), sub("b", nil)}, on("x"), "", "", true, 0},
} {
@@ -65,7 +75,7 @@ func TestTheRuleMovesEachTable(t *testing.T) {
func fourOnA(t *testing.T) *miniMesh {
mm := newMesh(t)
ctx := context.Background()
for _, l := range []Licence{sub("a", read(40, 99)), sub("b", read(30, 20)), sub("c", read(40, 10))} {
for _, l := range []Licence{sub("a", read(40, 99)), sub("b", read(30, 30)), sub("c", read(40, 40))} {
_ = mm.store.SaveLicence(ctx, l)
}
for _, c := range []string{"ace", "g14", "laptop", "server"} {
@@ -211,13 +211,13 @@ func fillTodaysRows(t *testing.T, conn *pgx.Conn) {
type vendor struct {
mu sync.Mutex
calls int
logins map[string][2]string // refresh token: the next access token and the account
usage map[string][2]float64 // access token: session and weekly percentages
logins map[string][2]string // refresh token: the next access token and the account
usage map[string]float64 // access token: the weekly percentage; the session window is answered empty (C4)
server *httptest.Server
}
func newVendor(t *testing.T) *vendor {
v := &vendor{logins: map[string][2]string{}, usage: map[string][2]float64{}}
v := &vendor{logins: map[string][2]string{}, usage: map[string]float64{}}
v.server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
v.mu.Lock()
defer v.mu.Unlock()
@@ -240,16 +240,17 @@ func newVendor(t *testing.T) *vendor {
return
}
resets := time.Now().Add(3 * time.Hour).UTC().Format(time.RFC3339)
_ = json.NewEncoder(w).Encode(map[string]any{"five_hour": map[string]any{"utilization": u[0], "resets_at": resets},
"seven_day": map[string]any{"utilization": u[1], "resets_at": resets}})
// As the vendor answers since it retired the session limit: the session window with no utilization (C4).
_ = json.NewEncoder(w).Encode(map[string]any{"five_hour": map[string]any{"utilization": nil, "resets_at": nil},
"seven_day": map[string]any{"utilization": u, "resets_at": resets}})
}))
t.Cleanup(v.server.Close)
return v
}
func (v *vendor) set(access string, session, weekly float64) {
func (v *vendor) set(access string, weekly float64) {
v.mu.Lock()
v.usage[access] = [2]float64{session, weekly}
v.usage[access] = weekly
v.mu.Unlock()
}
@@ -1065,8 +1066,8 @@ func TestAReadingAtTheThresholdMovesEveryConsumer(t *testing.T) {
m.mu.Lock()
carried := m.count("binding.changed")
m.mu.Unlock()
m.vendor.set("at-b", 10, 20)
m.vendor.set("at-a", 99, 50)
m.vendor.set("at-b", 20)
m.vendor.set("at-a", 99)
p.must("licences_read_usage", map[string]any{"licence": licenceB})
p.must("licences_read_usage", map[string]any{"licence": licenceA})
m.until("the four consumers moved", func() bool { return m.count("binding.changed") >= carried+4 })
@@ -1098,8 +1099,8 @@ func TestAReadingAtTheThresholdMovesEveryConsumer(t *testing.T) {
// Every licence at its threshold: the consumers have nowhere to go.
noRoom := "licence." + KeyFor(licenceB) + ".no-room"
m.vendor.set("at-a", 99, 50)
m.vendor.set("at-b", 99, 60)
m.vendor.set("at-a", 99)
m.vendor.set("at-b", 99)
p.must("licences_read_usage", map[string]any{"licence": licenceA})
p.must("licences_read_usage", map[string]any{"licence": licenceB})
m.until("no-room is raised", func() bool { return len(m.conditionsOf(noRoom)) > 0 })
@@ -1110,7 +1111,7 @@ func TestAReadingAtTheThresholdMovesEveryConsumer(t *testing.T) {
t.Errorf("no-room said %+v", r)
}
// A reading with room: the next reading of the full licence moves its consumers, and no-room is cleared.
m.vendor.set("at-a", 5, 30)
m.vendor.set("at-a", 30)
p.must("licences_read_usage", map[string]any{"licence": licenceA})
p.must("licences_read_usage", map[string]any{"licence": licenceB})
m.until("no-room is cleared", func() bool {
@@ -1159,8 +1160,8 @@ func TestANewLoginIsAdoptedListedAndATarget(t *testing.T) {
}
// The full licence is read first: nothing has room yet, so its consumer stays.
m.vendor.set("at-old", 99, 40)
m.vendor.set("at-team", 10, 5)
m.vendor.set("at-old", 99)
m.vendor.set("at-team", 5)
p.must("licences_read_usage", map[string]any{"licence": licenceA})
p.must("licences_read_usage", map[string]any{"licence": "team@example.org"})
var listed []map[string]any
+4 -4
View File
@@ -186,13 +186,13 @@
},
"switch_auto": {
"type": "boolean",
"description": "Whether the manager moves every consumer off a licence whose session or weekly usage reaches switch_at_pct, to the licence with the most room; off, it still raises its no-room and near-limit conditions.",
"description": "Whether the manager moves every consumer off a licence whose weekly usage reaches switch_at_pct, to the licence with the most room; off, it still raises its no-room and near-limit conditions.",
"trusted": true,
"default": true
},
"switch_at_pct": {
"type": "number",
"description": "The session or weekly usage of a licence, in percent, at which its consumers are moved to the licence with the most room.",
"description": "The weekly usage of a licence, in percent, at which its consumers are moved to the licence with the most room.",
"minimum": 1,
"maximum": 100,
"trusted": true,
@@ -200,7 +200,7 @@
},
"room_below_pct": {
"type": "number",
"description": "What both the session and the weekly usage of a licence must be below, in percent, for it to be a target of a switch.",
"description": "What the weekly usage of a licence must be below, in percent, for it to be a target of a switch.",
"minimum": 1,
"maximum": 100,
"trusted": true,
@@ -208,7 +208,7 @@
},
"warn_at_pct": {
"type": "number",
"description": "The session or weekly usage of a licence with consumers, in percent, at which it is warned about once.",
"description": "The weekly usage of a licence with consumers, in percent, at which it is warned about once.",
"minimum": 1,
"maximum": 100,
"trusted": true,