151 lines
5.5 KiB
Go
151 lines
5.5 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// What a module may call its state, and whose state it may ask to read (novox/hq ADR 0202).
|
|
//
|
|
// A module names its state **locally** — `servers`, never a bucket or a subject — and another
|
|
// module's as `<module>.<name>`, the way a consumed event names its emitter (design 32 §1). The
|
|
// mesh derives the bucket from the two names, so the module and the local name must each be one
|
|
// token: the bucket joins them with an underscore, which neither may contain, so two modules can
|
|
// never derive one bucket.
|
|
|
|
// stateName is one local name of a module's state: lower-case, no dot, no underscore.
|
|
var stateName = regexp.MustCompile(`^[a-z0-9][a-z0-9-]*$`)
|
|
|
|
// The mesh's caps on what a module may ask of a bucket's history.
|
|
const (
|
|
// StateMostHistory is the most past values a key may keep. The server's own limit.
|
|
StateMostHistory = 64
|
|
)
|
|
|
|
// StateDeclaration is one bucket a module owns: its local name, and the options that are the
|
|
// owner's to choose, as a seat chooses how long its backlog survives (design 32 §3).
|
|
type StateDeclaration struct {
|
|
Name string `json:"name"`
|
|
// History is how many values a key keeps, the current one included; zero is one.
|
|
History int `json:"history,omitempty"`
|
|
// TTLSeconds is how long a value lives once written; zero is until it is replaced or deleted.
|
|
TTLSeconds int `json:"ttl-seconds,omitempty"`
|
|
}
|
|
|
|
// UnmarshalJSON reads a bucket as its bare name, or as {name, history, ttl-seconds}.
|
|
func (s *StateDeclaration) UnmarshalJSON(raw []byte) error {
|
|
trimmed := bytes.TrimSpace(raw)
|
|
if len(trimmed) > 0 && trimmed[0] == '"' {
|
|
return json.Unmarshal(trimmed, &s.Name)
|
|
}
|
|
type plain StateDeclaration
|
|
var full plain
|
|
dec := json.NewDecoder(bytes.NewReader(trimmed))
|
|
dec.DisallowUnknownFields()
|
|
if err := dec.Decode(&full); err != nil {
|
|
return fmt.Errorf("a state is either a name or {name, history, ttl-seconds}: %w", err)
|
|
}
|
|
*s = StateDeclaration(full)
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON writes back the short form when there is nothing else to say.
|
|
func (s StateDeclaration) MarshalJSON() ([]byte, error) {
|
|
if s.History == 0 && s.TTLSeconds == 0 {
|
|
return json.Marshal(s.Name)
|
|
}
|
|
type plain StateDeclaration
|
|
return json.Marshal(plain(s))
|
|
}
|
|
|
|
// ReadState splits a read into the owning module and the local name, or says why it is not one.
|
|
func ReadState(read string) (module, local string, err error) {
|
|
at := strings.LastIndex(read, ".")
|
|
if at <= 0 || at == len(read)-1 {
|
|
return "", "", fmt.Errorf("%q does not name a module and its state: a read is <module>.<name>", read)
|
|
}
|
|
module, local = read[:at], read[at+1:]
|
|
if !stateName.MatchString(module) {
|
|
return "", "", fmt.Errorf("%q cannot own state: a module whose state is read is one plain name", module)
|
|
}
|
|
if !stateName.MatchString(local) {
|
|
return "", "", fmt.Errorf("%q is not a state name: lower-case letters, digits and hyphens", local)
|
|
}
|
|
return module, local, nil
|
|
}
|
|
|
|
// StateProblems is what is wrong with a manifest's state and reads.
|
|
//
|
|
// Refused at registration, because a bucket name the bus cannot hold is a module that installs,
|
|
// starts, and is refused on its first write with a reason about a bucket nobody named.
|
|
func StateProblems(m Manifest) []string {
|
|
var problems []string
|
|
if len(m.State) > 0 && !stateName.MatchString(m.Module) {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s keeps state, and a module's name is part of its buckets' names, which take one plain "+
|
|
"name — no dot (novox/hq ADR 0202)", m.Module))
|
|
}
|
|
seen := map[string]bool{}
|
|
for _, s := range m.State {
|
|
switch {
|
|
case !stateName.MatchString(s.Name):
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s keeps state %q: a state is named locally — lower-case letters, digits and hyphens, "+
|
|
"no dot and no underscore; the mesh derives the bucket (novox/hq ADR 0202)", m.Module, s.Name))
|
|
case seen[s.Name]:
|
|
problems = append(problems, fmt.Sprintf("%s keeps state %q twice", m.Module, s.Name))
|
|
}
|
|
seen[s.Name] = true
|
|
if s.History < 0 || s.History > StateMostHistory {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s keeps %d values of %q; a key keeps between 1 and %d", m.Module, s.History, s.Name, StateMostHistory))
|
|
}
|
|
if s.TTLSeconds < 0 {
|
|
problems = append(problems, fmt.Sprintf("%s gives %q a negative lifetime", m.Module, s.Name))
|
|
}
|
|
}
|
|
for _, r := range m.Reads {
|
|
module, _, err := ReadState(r)
|
|
if err != nil {
|
|
problems = append(problems, fmt.Sprintf("%s reads %v", m.Module, err))
|
|
continue
|
|
}
|
|
if module == m.Module {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s reads %q, which is its own state: a module reads and writes what it keeps already", m.Module, r))
|
|
}
|
|
}
|
|
return problems
|
|
}
|
|
|
|
// StateReadsNothingDeclares is every read across a catalogue whose owner is present and declares no
|
|
// such state. An absent owner says nothing — a module may be installed long before the one whose
|
|
// state it reads, as a consumer may before its emitter (design 32 §1).
|
|
func StateReadsNothingDeclares(manifests []Manifest) []string {
|
|
declared := map[string]map[string]bool{}
|
|
for _, m := range manifests {
|
|
own := map[string]bool{}
|
|
for _, s := range m.State {
|
|
own[s.Name] = true
|
|
}
|
|
declared[m.Module] = own
|
|
}
|
|
var problems []string
|
|
for _, m := range manifests {
|
|
for _, r := range m.Reads {
|
|
module, local, err := ReadState(r)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if own, present := declared[module]; present && !own[local] {
|
|
problems = append(problems, fmt.Sprintf(
|
|
"%s reads %q, and %s keeps no state called %q", m.Module, r, module, local))
|
|
}
|
|
}
|
|
}
|
|
return problems
|
|
}
|