From 2992b08a11242a2b87d301dae04d4b6443d9c86d Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 23:05:15 +0100 Subject: [PATCH] =?UTF-8?q?=C2=A7469=20(GR,=20A-8=20line=20of=20G):=20a=20?= =?UTF-8?q?bucket=20composed=20from=20an=20old=20state=20section=20takes?= =?UTF-8?q?=20its=20maximum=20size=20from=20its=20max-mib?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The grants part's composer gains Bucket.MaxMiB and Bucket.MaxBytes() (as mesh-sdk's buscompose has them): the bucket's stream is created with that cap and an assertion raises a held one to it; an entry naming none keeps the mesh's cap, so no bucket of today changes. Builds: design 47 G §469, §695 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h --- modules/mesh-grants/grants_test.go | 29 +++++++++++++++++++ .../mesh-grants/internal/buscompose/create.go | 4 +++ .../mesh-grants/internal/buscompose/state.go | 12 ++++++++ .../mesh-grants/internal/grants/assertions.go | 9 ++++-- .../mesh-grants/internal/grants/records.go | 2 +- 5 files changed, 52 insertions(+), 4 deletions(-) diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 2d251a68..e48802c2 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -5033,3 +5033,32 @@ func TestThePartNamesTheStreamItReadsWholeWaiting(t *testing.T) { t.Fatalf("after its whole read ended the streams are %+v, want it read", got) } } + +// TestAnOldStateSectionDeclaresItsCap is design 47 G §469: a bucket composed from an old state section takes its maximum size from the +// entry's `max-mib`, and one naming none keeps the mesh's cap, so no bucket of today changes. +func TestAnOldStateSectionDeclaresItsCap(t *testing.T) { + m, err := catalogue.Decode([]byte(`{"module":"mod-p","version":"1","state":[{"name":"pipelines","max-mib":256},"plain"]}`)) + if err != nil { + t.Fatal(err) + } + want := map[string]int64{"KV_mod-p_pipelines": 256 << 20, "KV_mod-p_plain": 0} + got := map[string]int64{} + for _, it := range grants.Composed(m, []string{"a"}) { + if it.Kind == "bucket" { + got[it.Stream] = it.Composed.MaxBytes + } + } + if !maps.Equal(got, want) { + t.Errorf("the composed buckets' caps = %v, want %v", got, want) + } + for name, max := range map[string]int{"pipelines": 256, "plain": 0} { + cfg := buscompose.BucketStreamConfig(buscompose.Bucket{Module: "mod-p", Name: name, MaxMiB: max}) + wantBytes := int64(buscompose.StateMaxBytes) + if max > 0 { + wantBytes = int64(max) << 20 + } + if cfg.MaxBytes != wantBytes { + t.Errorf("%s is created with %d bytes as its maximum size, want %d", name, cfg.MaxBytes, wantBytes) + } + } +} diff --git a/modules/mesh-grants/internal/buscompose/create.go b/modules/mesh-grants/internal/buscompose/create.go index 07d87f16..003a403b 100644 --- a/modules/mesh-grants/internal/buscompose/create.go +++ b/modules/mesh-grants/internal/buscompose/create.go @@ -43,6 +43,10 @@ func BucketStreamConfig(b Bucket) jetstream.StreamConfig { AllowDirect: true, Discard: jetstream.DiscardNew, } + // An old state section's entry names its own cap (design 47 G §469); one naming none keeps the mesh's. + if max := b.MaxBytes(); max > 0 { + cfg.MaxBytes = max + } if b.KeyTTL { cfg.AllowMsgTTL, cfg.SubjectDeleteMarkerTTL = true, KeyTTLMarkerTTL } diff --git a/modules/mesh-grants/internal/buscompose/state.go b/modules/mesh-grants/internal/buscompose/state.go index 7a57694a..86e9029a 100644 --- a/modules/mesh-grants/internal/buscompose/state.go +++ b/modules/mesh-grants/internal/buscompose/state.go @@ -48,6 +48,18 @@ type Bucket struct { // Whole is the bucket's name where it is not BucketName(Module, Name): one node's bucket of a state kept one bucket per // node, the node-engine's compositions `composed-` (ADR 0593 §6, EngineComposedBuckets). Whole string + // MaxMiB is the bucket's cap in MiB, from an old state section's `max-mib` (design 47 G §469); zero names none, and the bucket + // keeps the mesh's cap (StateMaxBytes) as every bucket of today does. + MaxMiB int +} + +// MaxBytes is the bucket's cap in bytes as the bus's maximum size, from its MaxMiB: -1 where it names none (buscompose.Bucket.MaxBytes +// of mesh-sdk). +func (b Bucket) MaxBytes() int64 { + if b.MaxMiB <= 0 { + return -1 + } + return int64(b.MaxMiB) * 1024 * 1024 } // KeyTTLMarkerTTL is how long the marker an expired key leaves is kept: long enough for a watcher to hear it. diff --git a/modules/mesh-grants/internal/grants/assertions.go b/modules/mesh-grants/internal/grants/assertions.go index 03f1c8bc..3de82b57 100644 --- a/modules/mesh-grants/internal/grants/assertions.go +++ b/modules/mesh-grants/internal/grants/assertions.go @@ -277,9 +277,12 @@ func Composed(m catalogue.Manifest, nodes []string) []Item { if per < 1 { per = 1 } - out = append(out, Item{Kind: "bucket", Stream: "KV_" + b.Bucket(), Composed: Limits{ - Subjects: []string{"$KV." + b.Bucket() + ".>"}, Retention: string(buscompose.RetentionLimits), - MaxAgeHours: b.TTLSeconds / 3600, MaxPerSubject: per}}) + limits := Limits{Subjects: []string{"$KV." + b.Bucket() + ".>"}, Retention: string(buscompose.RetentionLimits), + MaxAgeHours: b.TTLSeconds / 3600, MaxPerSubject: per} + if max := b.MaxBytes(); max > 0 { + limits.MaxBytes = max + } + out = append(out, Item{Kind: "bucket", Stream: "KV_" + b.Bucket(), Composed: limits}) } for _, l := range logsOf(m) { c := l.Config() diff --git a/modules/mesh-grants/internal/grants/records.go b/modules/mesh-grants/internal/grants/records.go index ac3c4d04..afef61c2 100644 --- a/modules/mesh-grants/internal/grants/records.go +++ b/modules/mesh-grants/internal/grants/records.go @@ -308,7 +308,7 @@ func issuedTools(m catalogue.Manifest) []string { func bucketsOf(m catalogue.Manifest) []buscompose.Bucket { var out []buscompose.Bucket for _, s := range m.State { - out = append(out, buscompose.Bucket{Module: m.Module, Name: s.Name, History: s.History, TTLSeconds: s.TTLSeconds, + out = append(out, buscompose.Bucket{Module: m.Module, Name: s.Name, History: s.History, TTLSeconds: s.TTLSeconds, MaxMiB: s.MaxMiB, PerMachine: s.PerMachine, KeyTTL: s.KeyTTL, ReadOnly: s.ReadOnly}) } return out