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"` } // 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}. 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"` } 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)) } 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 } 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 { 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 }