The licence manager keeps its data on the bus and switches licences by itself at 99% (issue 1409) #657

Merged
mesh-tools merged 32 commits from feat/39-licence-switch into main 2026-11-12 00:13:05 +00:00
2 changed files with 47 additions and 4 deletions
Showing only changes of commit c8e321c8e1 - Show all commits
@@ -686,11 +686,26 @@ func (m *Manager) ReadUsageOf(ctx context.Context, licence string, dry bool) ([]
if l.Kind != "subscription" || l.Generation == 0 || (licence != "" && l.Name != licence) {
continue
}
g, err := m.grantOf(ctx, l)
// The reading takes the licence's grant under its lease, so it never reads a grant a refresh is replacing (§4,
// §16); a grant the vault does not answer is a failed reading like a vendor's refusal (§31).
key := "licence." + KeyFor(l.Name)
leased, err := m.Store.Lease(ctx, key, m.Holder, leaseFor)
if err != nil {
return out, err
}
raw, err := m.Vendor.Usage(ctx, g.AccessToken)
if !leased {
out = append(out, Read{Licence: l.Name, Error: "another run holds its lease", Switches: []Switch{}})
continue
}
unlease := func() { _ = m.Store.Unlease(ctx, key, m.Holder) }
if fresh, err := m.Store.Licence(ctx, l.Name); err == nil && fresh != nil {
l = *fresh
}
var raw map[string]any
g, err := m.grantOf(ctx, l)
if err == nil {
raw, err = m.Vendor.Usage(ctx, g.AccessToken)
}
g = FullGrant{}
if err == nil && raw == nil {
err = errors.New("the usage endpoint answered nothing")
@@ -703,7 +718,9 @@ func (m *Manager) ReadUsageOf(ctx context.Context, licence string, dry bool) ([]
}
kept.ReadError = err.Error()
l.Usage = &kept
if err := m.Store.SaveLicence(ctx, l); err != nil {
err := m.Store.SaveLicence(ctx, l)
unlease()
if err != nil {
return out, err
}
m.Log("reading the usage of %s failed: %v", l.Name, err)
@@ -712,13 +729,16 @@ func (m *Manager) ReadUsageOf(ctx context.Context, licence string, dry bool) ([]
}
reading := FlattenUsage(raw)
if err := m.Store.RecordUsage(ctx, l.Name, m.Now().UnixMilli(), reading, raw); err != nil {
unlease()
return out, err
}
// The latest reading beside its licence, so licences answers from one key (§2).
latest := reading
latest.ReadAt = m.Now().UTC().Format(time.RFC3339Nano)
l.Usage = &latest
if err := m.Store.SaveLicence(ctx, l); err != nil {
err = m.Store.SaveLicence(ctx, l)
unlease()
if err != nil {
return out, err
}
_ = m.Emit("usage.read", map[string]any{"licence": l.Name, "sessionPct": reading.SessionPct,
@@ -64,6 +64,29 @@ func TestAFailedReadingKeepsThePercentages(t *testing.T) {
if n := len(mm.usage.entries); n != 1 {
t.Fatalf("a failed reading appended: %d entries", n)
}
// §16, §31: a grant the vault does not answer is a failed reading, written and gone past, never the end of the round.
mm.vendor.usageErr = nil
mm.grants.mu.Lock()
delete(mm.grants.held, SecretName(KeyFor(licence1), PartGrant))
mm.grants.mu.Unlock()
if err := mm.m.ReadUsage(ctx); err != nil {
t.Fatalf("a grant the vault did not answer ended the round: %v", err)
}
l, _ = mm.store.Licence(ctx, licence1)
if !strings.Contains(l.Usage.ReadError, "cannot be read from the vault") || *l.Usage.WeeklyPct != 97 {
t.Fatalf("after the vault's refusal the reading is %+v", l.Usage)
}
// §4, §16: a reading is taken under the licence's lease; with another run holding it, nothing is read or written.
if ok, _ := mm.store.Lease(ctx, "licence."+KeyFor(licence1), "another run", time.Minute); !ok {
t.Fatal("the lease was not taken")
}
reads, err := mm.m.ReadUsageOf(ctx, licence1, true)
if err != nil || len(reads) != 1 || reads[0].Error != "another run holds its lease" {
t.Fatalf("a reading under another's lease answered %+v, %v", reads, err)
}
if again, _ := mm.store.Licence(ctx, licence1); again.Usage.ReadError != l.Usage.ReadError {
t.Fatalf("a reading under another's lease wrote %+v", again.Usage)
}
}
// design 39 bus-and-switch §25/AC1, §48: a table of readings, ages and healths and the room each gives.