Let a module's log keep its entries for an age, so the hand-act log holds each act 400 days as ADR 0565 decides instead of refusing when full (issue 1953 S-age)

This commit is contained in:
2026-10-29 22:17:50 +01:00
parent bc6c8a6d24
commit 2e5cc4f166
4 changed files with 41 additions and 9 deletions
+5
View File
@@ -659,6 +659,11 @@ func healthChecks(t *testing.T) {
t.Errorf("%s writes the assigner's policies", u.Username())
}
}
// 1953 S-age (ADR 0565 §10): a log that says an age keeps its entries that long and no longer; one that says none, as
// long as the log (ADR 0297).
if aged, kept := (Log{Module: "mesh-hand-acts", Name: "acts", MaxAgeDays: 400}).Config(), (Log{Module: "m", Name: "l"}).Config(); aged.MaxAge != 400*24*time.Hour || kept.MaxAge != 0 || !aged.DenyDelete {
t.Errorf("an aged log's stream keeps %s, an unaged one %s", aged.MaxAge, kept.MaxAge)
}
// The tools account's secret is the module's own broker-<binary>, which its manifest's own-secrets names and the
// vault keeps by that name (AccountSecret).
if w := (WantedAccount{User: "a.nats.nats-tools", Kind: KindProcess, Node: "a", Module: "nats", Process: "nats-tools",
+5 -2
View File
@@ -4,6 +4,7 @@ import (
"fmt"
"sort"
"strings"
"time"
"github.com/nats-io/nats.go/jetstream"
)
@@ -37,6 +38,8 @@ type Log struct {
Name string
// MaxMiB is its cap in MiB; zero is LogDefaultMiB.
MaxMiB int
// MaxAgeDays is how long an entry is kept; zero is as long as the log (catalogue.LogDeclaration).
MaxAgeDays int
}
// LogStreamPrefix starts every log's stream name, which no other stream of the mesh's starts with.
@@ -74,7 +77,7 @@ func (l Log) Why() string {
}
// Config is the stream a log is, field by field as novox/hq ADR 0297 fixes it: a file stream kept by
// limits, with no maximum age and no cap per key, whose message is an entry's value and 4 KiB of
// limits, with no maximum age unless the log says one (ADR 0565 §10) and no cap per key, whose message is an entry's value and 4 KiB of
// headers at most, that refuses a new entry when full rather than
// drop an old one, and that refuses deleting an entry or purging it. Direct gets are allowed, which
// is how the runtime reads it.
@@ -85,7 +88,7 @@ func (l Log) Config() jetstream.StreamConfig {
Subjects: []string{l.Subject() + ".>"},
Storage: jetstream.FileStorage,
Retention: jetstream.LimitsPolicy,
MaxAge: 0,
MaxAge: time.Duration(l.MaxAgeDays) * 24 * time.Hour,
MaxMsgs: -1,
MaxMsgsPerSubject: -1,
MaxBytes: l.MaxBytes(),
+20 -5
View File
@@ -27,8 +27,15 @@ type LogDeclaration struct {
// MaxMiB is the log's cap in MiB; zero is LogDefaultMiB. When full, the log refuses new entries
// and never drops old ones.
MaxMiB int `json:"max-mib,omitempty"`
// MaxAgeDays is how long an entry is kept, in days; zero is as long as the log (novox/hq ADR 0297). The one way an
// entry leaves a log: the server ages it out, never a delete or a purge (novox/hq ADR 0565 §10: the hand-act log
// keeps every act 400 days; issue 1953 S-age).
MaxAgeDays int `json:"max-age-days,omitempty"`
}
// LogMostAgeDays is the longest a log may keep an entry by age: past it, a log keeps its entries as long as it is.
const LogMostAgeDays = 3650
// Cap is the log's cap in MiB, the default where none is said.
func (l LogDeclaration) Cap() int {
if l.MaxMiB == 0 {
@@ -37,20 +44,21 @@ func (l LogDeclaration) Cap() int {
return l.MaxMiB
}
// UnmarshalJSON reads a log as its bare name, or as {name, max-mib}.
// UnmarshalJSON reads a log as its bare name, or as {name, max-mib, max-age-days}.
func (l *LogDeclaration) UnmarshalJSON(raw []byte) error {
trimmed := bytes.TrimSpace(raw)
if len(trimmed) > 0 && trimmed[0] == '"' {
return json.Unmarshal(trimmed, &l.Name)
}
var full struct {
Name string `json:"name"`
MaxMiB *int `json:"max-mib"`
Name string `json:"name"`
MaxMiB *int `json:"max-mib"`
MaxAgeDays *int `json:"max-age-days"`
}
dec := json.NewDecoder(bytes.NewReader(trimmed))
dec.DisallowUnknownFields()
if err := dec.Decode(&full); err != nil {
return fmt.Errorf("a log is either a name or {name, max-mib}: %w", typedUnknown(err))
return fmt.Errorf("a log is either a name or {name, max-mib, max-age-days}: %w", typedUnknown(err))
}
l.Name = full.Name
l.MaxMiB = 0
@@ -61,12 +69,19 @@ func (l *LogDeclaration) UnmarshalJSON(raw []byte) error {
}
l.MaxMiB = *full.MaxMiB
}
l.MaxAgeDays = 0
if full.MaxAgeDays != nil {
if *full.MaxAgeDays < 1 || *full.MaxAgeDays > LogMostAgeDays {
return fmt.Errorf("log %q: max-age-days is between 1 and %d, not %d", full.Name, LogMostAgeDays, *full.MaxAgeDays)
}
l.MaxAgeDays = *full.MaxAgeDays
}
return nil
}
// MarshalJSON writes back the short form when there is nothing else to say.
func (l LogDeclaration) MarshalJSON() ([]byte, error) {
if l.MaxMiB == 0 {
if l.MaxMiB == 0 && l.MaxAgeDays == 0 {
return json.Marshal(l.Name)
}
type plain LogDeclaration
+11 -2
View File
@@ -24,7 +24,15 @@ func TestAManifestMaySayWhatLogsItKeeps(t *testing.T) {
if string(out) != `["changes",{"name":"moves","max-mib":2048}]` {
t.Fatalf("written back as %s", out)
}
for _, edge := range []string{`{"name":"a","max-mib":1}`, `{"name":"a","max-mib":8192}`} {
// 1953 S-age (ADR 0565 §10): a log may keep its entries for an age, written back with it.
aged, err := ParseManifest([]byte(`{"module":"mesh-hand-acts","version":"1","logs":[{"name":"acts","max-age-days":400}]}`))
if err != nil || aged.Logs[0].MaxAgeDays != 400 {
t.Fatalf("an aged log read as %+v, %v", aged.Logs, err)
}
if out, _ := json.Marshal(aged.Logs); string(out) != `[{"name":"acts","max-age-days":400}]` {
t.Fatalf("an aged log written back as %s", out)
}
for _, edge := range []string{`{"name":"a","max-mib":1}`, `{"name":"a","max-mib":8192}`, `{"name":"a","max-age-days":3650}`} {
if _, err := ParseManifest([]byte(`{"module":"a","version":"1","logs":[` + edge + `]}`)); err != nil {
t.Errorf("%s is inside the caps and was refused: %v", edge, err)
}
@@ -42,7 +50,8 @@ func TestALogIsNamedLocallyAndCapped(t *testing.T) {
{`{"module":"a","version":"1","logs":[{"name":"c","max-mib":8193}]}`, `between 1 and 8192 MiB`},
{`{"module":"a","version":"1","logs":[{"name":"c","max-mib":-1}]}`, `between 1 and 8192 MiB`},
{`{"module":"a","version":"1","logs":[{"name":"c","max-mib":0}]}`, `max-mib is between 1 and 8192, not 0`},
{`{"module":"a","version":"1","logs":[{"name":"c","stream":"LOG_x"}]}`, `{name, max-mib}`},
{`{"module":"a","version":"1","logs":[{"name":"c","stream":"LOG_x"}]}`, `{name, max-mib, max-age-days}`},
{`{"module":"a","version":"1","logs":[{"name":"c","max-age-days":0}]}`, `max-age-days is between 1 and 3650, not 0`},
{`{"module":"a.b","version":"1","logs":["c"]}`, `no dot`},
} {
_, err := ParseManifest([]byte(c.manifest))