M-13: a licence at its threshold moves every consumer, in one act (issue 1409)
After each reading of a licence with consumers whose session or weekly window is at or above switch_at_pct, the manager moves every consumer bound to it, when switch_auto is on and the settings are valid, to the other licence with known room whose windows are both below room_below_pct and the most room, the first by name on a tie. Each consumer moves as the switch verb does, by usage, and says binding.changed; the act says one licence.switched and one audit entry switched with the window, its percentage and reset and the target's room, and is told to the operator; the switch verb is not told. By hand the rule runs with switch_auto off, and dry answers the moves alone. Builds: design 39 bus-and-switch §26, §27, §28, §32, §33, B22 Claude-Session: https://claude.ai/code/session_01NcbgfHb9d4T9nq5Q6WqdiJ
This commit is contained in:
@@ -86,8 +86,13 @@ func manager() (*Manager, error) {
|
||||
}
|
||||
return a, json.Unmarshal(raw, &a)
|
||||
},
|
||||
Reported: heard.Of,
|
||||
Emit: func(event string, body map[string]any) error { return stdio.Emit(event, body) },
|
||||
Reported: heard.Of,
|
||||
Emit: func(event string, body map[string]any) error { return stdio.Emit(event, body) },
|
||||
Tell: func(_ context.Context, headline string, lines []string) error {
|
||||
_, err := stdio.Ask("seat:operator-channel.tell", map[string]any{"subject": Seat, "headline": headline,
|
||||
"lines": lines})
|
||||
return err
|
||||
},
|
||||
Now: time.Now,
|
||||
Log: say,
|
||||
Holder: fmt.Sprintf("%s:%d", node, os.Getpid()),
|
||||
|
||||
@@ -148,8 +148,10 @@ type Manager struct {
|
||||
// Reported is the binding generation a node last reported applying (B11), 0 when none is known.
|
||||
Reported func(node string) int64
|
||||
Emit func(event string, body map[string]any) error
|
||||
Now func() time.Time
|
||||
Log func(format string, args ...any)
|
||||
// Tell tells the operator one notice that needs no answer, through the seat operator-channel's tell (§28).
|
||||
Tell func(ctx context.Context, headline string, lines []string) error
|
||||
Now func() time.Time
|
||||
Log func(format string, args ...any)
|
||||
// Holder names this process in a lease, so a second run is told apart.
|
||||
Holder string
|
||||
// Settings are those in force, read again from SettingsFile at every reading (settings.go); an empty SettingsFile
|
||||
@@ -693,6 +695,10 @@ func (m *Manager) ReadUsage(ctx context.Context) error {
|
||||
"sessionResetsAt": reading.SessionResetsAt, "weeklyPct": reading.WeeklyPct,
|
||||
"weeklyResetsAt": reading.WeeklyResetsAt, "sonnetPct": reading.SonnetPct,
|
||||
"sonnetResetsAt": reading.SonnetResetsAt})
|
||||
// On each reading, a licence at its threshold moves every consumer bound to it (§26).
|
||||
if _, err := m.ApplyRule(ctx, l.Name, false, false); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
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
|
||||
// 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.
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// ByUsage is who moves a consumer by the rule (§26).
|
||||
const ByUsage = "usage"
|
||||
|
||||
// Switch is one act of the rule: from L to the target, the consumers it moves, the window of L that reached its
|
||||
// threshold, its percentage and reset, and the target's room.
|
||||
type Switch struct {
|
||||
From string `json:"from"`
|
||||
To string `json:"to"`
|
||||
Consumers []string `json:"consumers"`
|
||||
Window string `json:"window"`
|
||||
Pct float64 `json:"pct"`
|
||||
ResetsAt string `json:"resetsAt"`
|
||||
Room float64 `json:"room"`
|
||||
}
|
||||
|
||||
// why is the cause a moved binding carries: L, the window, its percentage and its reset (§26).
|
||||
func (s Switch) why() string {
|
||||
return fmt.Sprintf("%s reached %g %% of its %s window, which resets at %s", s.From, s.Pct, s.Window, s.ResetsAt)
|
||||
}
|
||||
|
||||
// fields are the act's fields, the same in licence.switched and the audit entry switched (§27).
|
||||
func (s Switch) fields() map[string]any {
|
||||
f := switchedFields(s.From, s.To, s.Consumers, Cause{Why: s.why(), By: ByUsage})
|
||||
f["window"], f["pct"], f["resetsAt"], f["room"] = s.Window, s.Pct, s.ResetsAt, s.Room
|
||||
return f
|
||||
}
|
||||
|
||||
// Rule is what the rule finds for one licence at its threshold: the act, or none with NoTarget when no other licence
|
||||
// has room.
|
||||
type Rule struct {
|
||||
Switch *Switch
|
||||
NoTarget bool
|
||||
}
|
||||
|
||||
// RuleFor is what §26 gives for licence l among every licence and binding, under the settings s. A licence below its
|
||||
// threshold, or with no consumer, gives nothing.
|
||||
func (m *Manager) RuleFor(l Licence, licences []Licence, bindings []Binding, s Settings) Rule {
|
||||
var consumers []string
|
||||
for _, b := range bindings {
|
||||
if b.Licence == l.Name {
|
||||
consumers = append(consumers, b.Consumer)
|
||||
}
|
||||
}
|
||||
if len(consumers) == 0 || l.Usage == nil {
|
||||
return Rule{}
|
||||
}
|
||||
sw := Switch{From: l.Name, Consumers: consumers}
|
||||
session, weekly := l.Usage.SessionPct, l.Usage.WeeklyPct
|
||||
switch {
|
||||
case session != nil && *session >= s.SwitchAt && (weekly == nil || *session >= *weekly):
|
||||
sw.Window, sw.Pct, sw.ResetsAt = "session", *session, l.Usage.SessionResetsAt
|
||||
case weekly != nil && *weekly >= s.SwitchAt:
|
||||
sw.Window, sw.Pct, sw.ResetsAt = "weekly", *weekly, l.Usage.WeeklyResetsAt
|
||||
default:
|
||||
return Rule{}
|
||||
}
|
||||
sort.Strings(sw.Consumers)
|
||||
others := append([]Licence(nil), licences...)
|
||||
sort.Slice(others, func(i, j int) bool { return others[i].Name < others[j].Name })
|
||||
best := -1.0
|
||||
for _, o := range others {
|
||||
if o.Name == l.Name {
|
||||
continue
|
||||
}
|
||||
room, known := Room(o, m.Now(), s)
|
||||
if !known || *o.Usage.SessionPct >= s.RoomBelow || *o.Usage.WeeklyPct >= s.RoomBelow || room <= best {
|
||||
continue
|
||||
}
|
||||
best, sw.To, sw.Room = room, o.Name, room
|
||||
}
|
||||
if sw.To == "" {
|
||||
return Rule{NoTarget: true}
|
||||
}
|
||||
return Rule{Switch: &sw}
|
||||
}
|
||||
|
||||
// ApplyRule applies §26 to one licence, or to every one when licence is empty: after a reading, by switch_auto, or by
|
||||
// hand, even with switch_auto off (§32, §39). With dry it answers the moves and moves nothing. An invalid pair of
|
||||
// settings moves no consumer (§33).
|
||||
func (m *Manager) ApplyRule(ctx context.Context, licence string, byHand, dry bool) ([]Switch, error) {
|
||||
s := m.settings()
|
||||
if (!s.SwitchAuto && !byHand) || len(s.Invalid()) > 0 {
|
||||
return nil, nil
|
||||
}
|
||||
licences, err := m.Store.Licences(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
bindings, err := m.Store.Bindings(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var acts []Switch
|
||||
for _, l := range licences {
|
||||
if licence != "" && l.Name != licence {
|
||||
continue
|
||||
}
|
||||
r := m.RuleFor(l, licences, bindings, s)
|
||||
if r.Switch == nil {
|
||||
continue
|
||||
}
|
||||
acts = append(acts, *r.Switch)
|
||||
if dry {
|
||||
continue
|
||||
}
|
||||
if err := m.act(ctx, *r.Switch, licences); err != nil {
|
||||
return acts, err
|
||||
}
|
||||
// What was moved is bound to the target now, for the next licence's rule.
|
||||
if bindings, err = m.Store.Bindings(ctx); err != nil {
|
||||
return acts, err
|
||||
}
|
||||
}
|
||||
return acts, nil
|
||||
}
|
||||
|
||||
// act moves every consumer of one act as the switch verb does, then says the act once and tells the operator (§26 to
|
||||
// §28).
|
||||
func (m *Manager) act(ctx context.Context, sw Switch, licences []Licence) error {
|
||||
kind := ""
|
||||
for _, l := range licences {
|
||||
if l.Name == sw.To {
|
||||
kind = l.Kind
|
||||
}
|
||||
}
|
||||
floor := m.floorIn(nil)
|
||||
for _, consumer := range sw.Consumers {
|
||||
b, err := m.Store.Bind(ctx, consumer, sw.To, Move{Kind: kind, Why: sw.why(), By: ByUsage, Floor: floor})
|
||||
if err != nil {
|
||||
return fmt.Errorf("moving %s to %s: %w", consumer, sw.To, err)
|
||||
}
|
||||
m.changed(b, sw.From)
|
||||
}
|
||||
f := sw.fields()
|
||||
if err := m.record(ctx, "switched", fmt.Sprintf("switched %s from %s to %s: %s; by %s",
|
||||
strings.Join(sw.Consumers, ", "), sw.From, sw.To, sw.why(), ByUsage), f); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = m.Emit("licence.switched", f)
|
||||
if m.Tell != nil {
|
||||
line := fmt.Sprintf("%s; %s has %g %% room", sw.why(), sw.To, sw.Room)
|
||||
if err := m.Tell(ctx, strings.Join(sw.Consumers, ", ")+" moved to "+sw.To, []string{line}); err != nil {
|
||||
m.Log("the switch of %s to %s could not be told: %v", strings.Join(sw.Consumers, ", "), sw.To, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// read is a licence's latest reading, read at t0.
|
||||
func read(session, weekly float64) *UsageReading {
|
||||
return &UsageReading{SessionPct: &session, SessionResetsAt: "2026-11-11T23:00:00Z", WeeklyPct: &weekly,
|
||||
WeeklyResetsAt: "2026-11-14T09:00:00Z", ReadAt: t0.UTC().Format(time.RFC3339Nano)}
|
||||
}
|
||||
|
||||
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.
|
||||
func TestTheRuleMovesEachTable(t *testing.T) {
|
||||
mm := newMesh(t)
|
||||
on := func(consumers ...string) []Binding {
|
||||
var out []Binding
|
||||
for _, c := range consumers {
|
||||
out = append(out, Binding{Consumer: c, Licence: "a"})
|
||||
}
|
||||
return out
|
||||
}
|
||||
for _, c := range []struct {
|
||||
name string
|
||||
licences []Licence
|
||||
bindings []Binding
|
||||
to string
|
||||
window string
|
||||
noTarget bool
|
||||
consumers int
|
||||
}{
|
||||
{"below its threshold", []Licence{sub("a", read(50, 98)), sub("b", read(0, 0))}, on("x"), "", "", false, 0},
|
||||
{"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)),
|
||||
{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},
|
||||
} {
|
||||
r := mm.m.RuleFor(c.licences[0], c.licences, c.bindings, Defaults)
|
||||
to, window, n := "", "", 0
|
||||
if r.Switch != nil {
|
||||
to, window, n = r.Switch.To, r.Switch.Window, len(r.Switch.Consumers)
|
||||
}
|
||||
if to != c.to || window != c.window || n != c.consumers || r.NoTarget != c.noTarget {
|
||||
t.Errorf("%s: to %q window %q consumers %d no target %v", c.name, to, window, n, r.NoTarget)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// fourOnA is four consumers bound to a, at 99 % of its weekly window, b with 70 % room and c with 60 %.
|
||||
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))} {
|
||||
_ = mm.store.SaveLicence(ctx, l)
|
||||
}
|
||||
for _, c := range []string{"ace", "g14", "laptop", "server"} {
|
||||
_, _ = mm.store.Bind(ctx, c, "a", Move{Kind: "subscription", Why: "set", By: "test"})
|
||||
}
|
||||
mm.events = nil
|
||||
return mm
|
||||
}
|
||||
|
||||
// design 39 bus-and-switch §27/AC1, §26, §21: one act says one licence.switched with the consumers moved, the window,
|
||||
// its percentage, reset and the target's room; one binding.changed per consumer; one audit entry switched with the same
|
||||
// fields; each binding moved by usage, its why naming the licence, the window, the percentage and the reset.
|
||||
func TestOneSwitchedPerAct(t *testing.T) {
|
||||
mm := fourOnA(t)
|
||||
acts, err := mm.m.ApplyRule(context.Background(), "", false, false)
|
||||
if err != nil || len(acts) != 1 || acts[0].To != "b" || acts[0].Room != 70 {
|
||||
t.Fatalf("acts %+v %v", acts, err)
|
||||
}
|
||||
var switched []map[string]any
|
||||
changed := 0
|
||||
for _, e := range mm.events {
|
||||
name, body, _ := strings.Cut(e, " ")
|
||||
var f map[string]any
|
||||
_ = json.Unmarshal([]byte(body), &f)
|
||||
switch name {
|
||||
case "licence.switched":
|
||||
switched = append(switched, f)
|
||||
case "binding.changed":
|
||||
changed++
|
||||
}
|
||||
}
|
||||
if len(switched) != 1 || changed != 4 {
|
||||
t.Fatalf("said %v", mm.events)
|
||||
}
|
||||
s := switched[0]
|
||||
if s["from"] != "a" || s["to"] != "b" || len(s["consumers"].([]any)) != 4 || s["by"] != ByUsage ||
|
||||
s["window"] != "weekly" || s["pct"] != 99.0 || s["resetsAt"] != "2026-11-14T09:00:00Z" || s["room"] != 70.0 {
|
||||
t.Fatalf("licence.switched %v", s)
|
||||
}
|
||||
audit := mm.audit.under("switched")
|
||||
if len(audit) != 1 || audit[0]["window"] != "weekly" || len(audit[0]["consumers"].([]any)) != 4 {
|
||||
t.Fatalf("audit %v", audit)
|
||||
}
|
||||
b := mm.bound("laptop")
|
||||
if b.Licence != "b" || b.By != ByUsage || b.Why != "a reached 99 % of its weekly window, which resets at 2026-11-14T09:00:00Z" {
|
||||
t.Fatalf("laptop %+v", b)
|
||||
}
|
||||
}
|
||||
|
||||
// design 39 bus-and-switch §28/AC1: the rule tells the operator once per act, and the switch verb never.
|
||||
func TestTheRuleTellsOnceAndTheVerbNever(t *testing.T) {
|
||||
mm := fourOnA(t)
|
||||
var told []string
|
||||
mm.m.Tell = func(_ context.Context, headline string, lines []string) error {
|
||||
told = append(told, headline+" | "+strings.Join(lines, " "))
|
||||
return nil
|
||||
}
|
||||
_, _ = mm.m.ApplyRule(context.Background(), "", false, false)
|
||||
if len(told) != 1 || told[0] != "ace, g14, laptop, server moved to b | a reached 99 % of its weekly window, "+
|
||||
"which resets at 2026-11-14T09:00:00Z; b has 70 % room" {
|
||||
t.Fatalf("told %v", told)
|
||||
}
|
||||
_, _ = mm.m.Bind(WithCause(context.Background(), Cause{Why: "the switch verb", By: "a person"}), "ace", "c")
|
||||
if len(told) != 1 {
|
||||
t.Fatalf("the switch verb told %v", told)
|
||||
}
|
||||
}
|
||||
|
||||
// design 39 bus-and-switch §32/AC1, §33/AC2: dry answers the same moves as the act and moves nothing; an invalid pair
|
||||
// of settings, or switch_auto off, moves nothing after a reading, and the rule by hand runs with switch_auto off.
|
||||
func TestDryAnswersTheSameMovesAndMovesNothing(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
mm := fourOnA(t)
|
||||
dry, err := mm.m.ApplyRule(ctx, "a", true, true)
|
||||
if err != nil || len(dry) != 1 || mm.bound("ace").Licence != "a" || len(mm.events) != 0 {
|
||||
t.Fatalf("dry %+v %v, said %v", dry, err, mm.events)
|
||||
}
|
||||
acts, _ := mm.m.ApplyRule(ctx, "a", true, false)
|
||||
if len(acts) != 1 || acts[0].To != dry[0].To || strings.Join(acts[0].Consumers, ",") != strings.Join(dry[0].Consumers, ",") {
|
||||
t.Fatalf("dry %+v, act %+v", dry, acts)
|
||||
}
|
||||
for name, change := range map[string]func(*Settings){
|
||||
"an invalid pair": func(s *Settings) { s.RoomBelow = 100 },
|
||||
"switch_auto off": func(s *Settings) { s.SwitchAuto = false },
|
||||
} {
|
||||
mm := fourOnA(t)
|
||||
change(&mm.m.Settings)
|
||||
if acts, _ := mm.m.ApplyRule(ctx, "", false, false); len(acts) != 0 || mm.bound("ace").Licence != "a" {
|
||||
t.Errorf("%s moved %+v", name, acts)
|
||||
}
|
||||
}
|
||||
mm = fourOnA(t)
|
||||
mm.m.Settings.SwitchAuto = false
|
||||
if acts, _ := mm.m.ApplyRule(ctx, "", true, false); len(acts) != 1 || mm.bound("ace").Licence != "b" {
|
||||
t.Fatalf("by hand with switch_auto off moved %+v", acts)
|
||||
}
|
||||
}
|
||||
|
||||
// design 39 bus-and-switch §26: a reading at the threshold applies the rule at once.
|
||||
func TestAReadingAtTheThresholdApplies(t *testing.T) {
|
||||
mm := newMesh(t)
|
||||
ctx := context.Background()
|
||||
_, _ = mm.m.Consider(ctx, []Holdings{mm.login("laptop", "rt-a", true, t0)})
|
||||
_ = mm.store.SaveLicence(ctx, Licence{Name: "b@example.org", Kind: "subscription", Usage: read(10, 10)})
|
||||
mm.vendor.usage = map[string]any{"five_hour": map[string]any{"utilization": 30.0},
|
||||
"seven_day": map[string]any{"utilization": 99.0, "resets_at": "2026-11-14T09:00:00Z"}}
|
||||
if err := mm.m.ReadUsage(ctx); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if b := mm.bound("laptop"); b.Licence != "b@example.org" || b.By != ByUsage {
|
||||
t.Fatalf("laptop %+v", b)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user