A module's state on the bus: buckets from the catalogue, grants, membership (novox/hq ADR 0201)
A manifest names the state it keeps (state) and reads (reads); the controller asserts a key-value bucket per name on every raise, grants owners write and readers read (measured against a running server), issues each assignment its buckets in the membership, and reports buckets nothing declares without removing them.
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
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 0201).
|
||||
//
|
||||
// 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 0201)", 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 0201)", 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
|
||||
}
|
||||
Reference in New Issue
Block a user