121 lines
4.5 KiB
Go
121 lines
4.5 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// What a module may call its logs (novox/hq ADR 0297).
|
|
//
|
|
// A log is a module's record of operations: entries appended under a key, each kept as long as the
|
|
// log is, in the order they came. A module names each log it owns **locally** — `changes`, never a
|
|
// stream or a subject (ADR 0201 §4) — and the mesh derives the stream from the module and the local
|
|
// name, as it derives a bucket. So the rule for a log's name is a state name's: one plain token, and
|
|
// a module that keeps a log has a name that is one plain token too.
|
|
|
|
// The mesh's caps on a log, in MiB: what a module may ask, and what it gets when it asks nothing.
|
|
const (
|
|
LogLeastMiB = 1
|
|
LogMostMiB = 8192
|
|
LogDefaultMiB = 1024
|
|
)
|
|
|
|
// LogDeclaration is one log a module owns: its local name, and how large it may grow.
|
|
type LogDeclaration struct {
|
|
Name string `json:"name"`
|
|
// 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 {
|
|
return LogDefaultMiB
|
|
}
|
|
return l.MaxMiB
|
|
}
|
|
|
|
// 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"`
|
|
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, max-age-days}: %w", typedUnknown(err))
|
|
}
|
|
l.Name = full.Name
|
|
l.MaxMiB = 0
|
|
if full.MaxMiB != nil {
|
|
// Said, and said as nothing: refused rather than read as the default, which it did not say.
|
|
if *full.MaxMiB == 0 {
|
|
return fmt.Errorf("log %q: max-mib is between %d and %d, not 0", full.Name, LogLeastMiB, LogMostMiB)
|
|
}
|
|
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 && l.MaxAgeDays == 0 {
|
|
return json.Marshal(l.Name)
|
|
}
|
|
type plain LogDeclaration
|
|
return json.Marshal(plain(l))
|
|
}
|
|
|
|
// LogProblems is what is wrong with a manifest's logs.
|
|
//
|
|
// Refused at registration, for a bucket's reason: a stream name the bus cannot hold, or a cap the
|
|
// mesh would not grant, is a module that installs, starts, and is refused on its first append.
|
|
func LogProblems(m Manifest) []string {
|
|
var problems []string
|
|
if len(m.Logs) > 0 && !stateName.MatchString(m.Module) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s keeps a log, and a module's name is part of its logs' names, which take one plain "+
|
|
"name — no dot (novox/hq ADR 0297)", m.Module))
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, l := range m.Logs {
|
|
switch {
|
|
case !stateName.MatchString(l.Name):
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s keeps log %q: a log is named locally — lower-case letters, digits and hyphens, "+
|
|
"no dot and no underscore; the mesh derives the stream (novox/hq ADR 0297)", m.Module, l.Name))
|
|
case seen[l.Name]:
|
|
problems = append(problems, fmt.Sprintf("%s keeps log %q twice", m.Module, l.Name))
|
|
}
|
|
seen[l.Name] = true
|
|
if l.MaxMiB != 0 && (l.MaxMiB < LogLeastMiB || l.MaxMiB > LogMostMiB) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s caps log %q at %d MiB; a log holds between %d and %d MiB (novox/hq ADR 0297)",
|
|
m.Module, l.Name, l.MaxMiB, LogLeastMiB, LogMostMiB))
|
|
}
|
|
}
|
|
return problems
|
|
}
|