Files
mesh-tools/node-tools/internal/ledger/ledger.go
T

494 lines
18 KiB
Go

// Package ledger is the tool runner's side of a ledger (novox/hq ADR 0597, design 25 § 12.4; issue 1966): a storage type
// whose state is derived from its history. `apply` checks an operation against its type, appends it to the instance's
// history with the last sequence expected, derives the state by the operation's effect and writes it as an update of the
// revision read, and writes the instance's snapshot. What the history, the state and the snapshots are kept in is given
// (History, State, Snapshots): the bus in the runtime, fakes in a test.
package ledger
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"regexp"
"strings"
"time"
"git.novox.be/novox/mesh-sdk/go/buscalls"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The refusals the stores answer, told by errors.Is.
var (
// ErrRaced is an append or a snapshot written with an expected sequence that is no longer the last: another writer
// wrote first.
ErrRaced = errors.New("another writer wrote the instance's history first")
// ErrChanged is a state written at a revision that is no longer the key's.
ErrChanged = errors.New("the state was written by another writer first")
// ErrFull is a state bucket that refuses a write because it is full.
ErrFull = errors.New("the state bucket is full")
)
// CodeChanged is the error code a bundle is answered a lost race with, as a lost state update (mesh-sdk stdio.CodeChanged).
const CodeChanged = -32010
// raced is an apply refused because another writer appended to the instance first (ADR 0597 §5).
type raced struct{ instance string }
func (e raced) Error() string {
return "the operation on " + e.instance + " is not taken: another writer appended to its history first; read it and apply again (novox/hq ADR 0597 §5)"
}
func (e raced) Is(target error) bool { return target == ErrRaced }
func (e raced) ErrorCode() int { return CodeChanged }
// Kept is one message a history keeps: its sequence and its bytes.
type Kept struct {
Sequence uint64
Data []byte
}
// History is a ledger's history: a stream with a subject per instance, each instance named by its key.
type History interface {
// Last is the sequence of the instance's last operation, 0 when it has none.
Last(key string) (uint64, error)
// Append writes one operation, expecting the instance's last sequence to be expect (0: it has none); ErrRaced when
// it is not.
Append(key string, op []byte, expect uint64) (uint64, error)
// Read is the operations of the instances keys names (a key, with `*` for one name and `>` for the rest) from a
// sequence on, oldest first, at most limit of them (every one when limit is 0).
Read(keys string, from uint64, limit int) ([]Kept, error)
}
// State is a ledger's state bucket: each instance's record under its key.
type State interface {
// Get is the key's value and revision; nil and 0 when it has none.
Get(key string) ([]byte, uint64, error)
// Write writes the key's value at the revision read (0: only when it has none); ErrChanged when the key is no longer
// at it, ErrFull when the bucket is full.
Write(key string, value []byte, revision uint64) (uint64, error)
// Keys is every key the bucket holds.
Keys() ([]string, error)
}
// Snapshots are a ledger's snapshots: the latest record of each instance, on a subject of its own, never expiring.
type Snapshots interface {
// Get is the key's latest snapshot and the sequence it was written at; nil and 0 when it has none.
Get(key string) ([]byte, uint64, error)
// Put writes the key's snapshot, expecting its last to be at expect (0: none); ErrRaced when it is not.
Put(key string, value []byte, expect uint64) error
}
// Ledger is one ledger of one module: its types and where it is kept.
type Ledger struct {
Module string
Name string
Types []catalogue.LedgerType
History History
State State
Snapshots Snapshots
// Now is the clock an operation is stamped by; time.Now when nil.
Now func() time.Time
// Logf says what an apply could not do after its operation was taken; nothing is said when nil.
Logf func(format string, args ...any)
}
func (l *Ledger) say(format string, args ...any) {
if l.Logf != nil {
l.Logf(format, args...)
}
}
// Record is what the state and a snapshot keep of an instance: its state and the sequence of the last operation in it.
type Record struct {
Sequence uint64 `json:"sequence"`
State json.RawMessage `json:"state"`
}
// segment is one part of an instance's path.
var segment = regexp.MustCompile(`^[A-Za-z0-9_-]+$`)
// KeyOf is the key an instance is kept under, in the state and as its history's and snapshot's subject token: its path's
// parts joined by dots (`decision/0290` is `decision.0290`).
func KeyOf(instance string) (string, error) {
parts := strings.Split(instance, "/")
if len(parts) < 2 {
return "", fmt.Errorf("%q is not an instance: an instance is named by a path whose first part is its type's name, "+
"such as decision/0290 (novox/hq ADR 0597 §1)", instance)
}
for _, p := range parts {
if !segment.MatchString(p) {
return "", fmt.Errorf("%q is not an instance: each part of its path holds letters, digits, - and _ alone", instance)
}
}
return strings.Join(parts, "."), nil
}
// InstanceOf is the instance a key names.
func InstanceOf(key string) string { return strings.ReplaceAll(key, ".", "/") }
func (l *Ledger) now() time.Time {
if l.Now != nil {
return l.Now().UTC()
}
return time.Now().UTC()
}
// typeOf is the instance's type, by its path's first part.
func (l *Ledger) typeOf(instance string) (catalogue.LedgerType, error) {
name, _, _ := strings.Cut(instance, "/")
for _, t := range l.Types {
if t.Name == name {
return t, nil
}
}
return catalogue.LedgerType{}, fmt.Errorf("%s's ledger %s has no type %q, which the instance %s names", l.Module, l.Name, name, instance)
}
// operationOf is the operation a type allows by its name.
func operationOf(t catalogue.LedgerType, name string) (catalogue.LedgerOperation, error) {
for _, op := range t.Operations {
if op.Name == name {
return op, nil
}
}
return catalogue.LedgerOperation{}, fmt.Errorf("the type %s allows no operation %q", t.Name, name)
}
// Digest is the digest a type pairs a text field with (ADR 0597 §2): sha256 of the text, as the knowledge base writes it.
func Digest(text string) string {
sum := sha256.Sum256([]byte(text))
return "sha256:" + hex.EncodeToString(sum[:])
}
// kinds is the JSON kind each schema type name is held to; a name not here is the judge's to know.
var kinds = map[string]func(any) bool{
"string": func(v any) bool { _, ok := v.(string); return ok },
"number": func(v any) bool { _, ok := v.(float64); return ok },
"integer": func(v any) bool { f, ok := v.(float64); return ok && f == float64(int64(f)) },
"boolean": func(v any) bool { _, ok := v.(bool); return ok },
"object": func(v any) bool { _, ok := v.(map[string]any); return ok },
"array": func(v any) bool { _, ok := v.([]any); return ok },
}
// checked is an operation as apply checked it: its instance's key, its operation and its body.
type checked struct {
key string
op catalogue.LedgerOperation
body map[string]any
}
// check holds an operation to its type (ADR 0597 §2): a type the instance names, an operation it allows, a body that is an
// object whose fields fit the schema, the key its effect names, and every digest matching its text.
func (l *Ledger) check(r buscalls.ApplyRequest) (checked, error) {
key, err := KeyOf(r.Instance)
if err != nil {
return checked{}, err
}
t, err := l.typeOf(r.Instance)
if err != nil {
return checked{}, err
}
op, err := operationOf(t, r.Operation)
if err != nil {
return checked{}, err
}
body := map[string]any{}
if len(r.Body) > 0 && string(r.Body) != "null" {
if err := json.Unmarshal(r.Body, &body); err != nil {
return checked{}, fmt.Errorf("the operation %s's body is a JSON object: %w", r.Operation, err)
}
}
for name, field := range t.Schema {
v, has := body[name]
if !has {
continue
}
if fits, known := kinds[field.Type]; known && !fits(v) {
return checked{}, fmt.Errorf("the operation %s's field %s is a %s in the type %s", r.Operation, name, field.Type, t.Name)
}
}
for name, field := range t.Schema {
if field.DigestOf == "" {
continue
}
digest, hasDigest := body[name]
text, hasText := body[field.DigestOf]
if !hasDigest && !hasText {
continue
}
s, isText := text.(string)
if !hasDigest || !hasText || !isText || digest != Digest(s) {
return checked{}, fmt.Errorf("the operation %s's %s is not the digest of its %s, so it is refused (novox/hq ADR 0597 §2)",
r.Operation, name, field.DigestOf)
}
}
switch op.Effect {
case catalogue.EffectSetFields, catalogue.EffectAppend:
case catalogue.EffectSetEntry, catalogue.EffectRemove:
if op.Effect == catalogue.EffectRemove && op.Into == "" {
break
}
if _, ok := body[op.Key].(string); !ok {
return checked{}, fmt.Errorf("the operation %s names its entry by its field %s, a text, which its body lacks", r.Operation, op.Key)
}
default:
return checked{}, fmt.Errorf("the operation %s's effect %q is not one the mesh offers (novox/hq ADR 0597 §2)", r.Operation, op.Effect)
}
return checked{key: key, op: op, body: body}, nil
}
// effect is an operation's effect on a state (ADR 0597 §2): it copies only the fields the operation names, and the
// operation's sequence and proven author where it says so; null is a removed instance.
func effect(state json.RawMessage, op catalogue.LedgerOperation, body map[string]any, seq uint64, author buscalls.Author) (json.RawMessage, error) {
s := map[string]any{}
if len(state) > 0 && string(state) != "null" {
if err := json.Unmarshal(state, &s); err != nil {
return nil, fmt.Errorf("the state held is not an object: %w", err)
}
}
copied := map[string]any{}
for _, f := range op.Fields {
if v, ok := body[f]; ok {
copied[f] = v
}
}
if op.Sequence {
copied["sequence"] = seq
}
// Written last, so a body's own `author` never stands for the proven one (ADR 0597 §7).
if op.Author {
copied["author"] = author
}
switch op.Effect {
case catalogue.EffectSetFields:
for k, v := range copied {
s[k] = v
}
case catalogue.EffectSetEntry:
m, _ := s[op.Into].(map[string]any)
if m == nil {
m = map[string]any{}
}
m[body[op.Key].(string)] = copied
s[op.Into] = m
case catalogue.EffectAppend:
list, _ := s[op.Into].([]any)
s[op.Into] = append(list, copied)
case catalogue.EffectRemove:
if op.Into == "" {
return json.RawMessage("null"), nil
}
if m, ok := s[op.Into].(map[string]any); ok {
delete(m, body[op.Key].(string))
}
}
return json.Marshal(s)
}
// fold applies one kept operation to a record, as apply derived it.
func (l *Ledger) fold(rec Record, k Kept) (Record, error) {
var o buscalls.Operation
if err := json.Unmarshal(k.Data, &o); err != nil {
return rec, fmt.Errorf("the history's operation %d is not one: %w", k.Sequence, err)
}
t, err := l.typeOf(o.Instance)
if err != nil {
return rec, err
}
op, err := operationOf(t, o.Operation)
if err != nil {
return rec, err
}
body := map[string]any{}
if len(o.Body) > 0 && string(o.Body) != "null" {
if err := json.Unmarshal(o.Body, &body); err != nil {
return rec, err
}
}
if (op.Effect == catalogue.EffectSetEntry || (op.Effect == catalogue.EffectRemove && op.Into != "")) && !isText(body[op.Key]) {
return rec, fmt.Errorf("the history's operation %d names no entry", k.Sequence)
}
state, err := effect(rec.State, op, body, k.Sequence, o.Author)
if err != nil {
return rec, err
}
return Record{Sequence: k.Sequence, State: state}, nil
}
func isText(v any) bool { _, ok := v.(string); return ok }
// replay folds the instance's operations after rec's sequence, up to upto (every one when 0), onto rec.
func (l *Ledger) replay(key string, rec Record, upto uint64) (Record, error) {
kept, err := l.History.Read(key, rec.Sequence+1, 0)
if err != nil {
return rec, err
}
for _, k := range kept {
if upto != 0 && k.Sequence > upto {
break
}
if k.Sequence <= rec.Sequence {
continue
}
if rec, err = l.fold(rec, k); err != nil {
return rec, err
}
}
return rec, nil
}
// readState is the instance's record in the state and its revision; an empty record when it has none.
func (l *Ledger) readState(key string) (Record, uint64, error) {
raw, rev, err := l.State.Get(key)
if err != nil || raw == nil {
return Record{}, rev, err
}
var rec Record
if err := json.Unmarshal(raw, &rec); err != nil {
return Record{}, rev, fmt.Errorf("the state of %s is not a record: %w", key, err)
}
return rec, rev, nil
}
// readSnapshot is the instance's snapshot and the sequence it was written at; an empty record when it has none.
func (l *Ledger) readSnapshot(key string) (Record, uint64, error) {
raw, at, err := l.Snapshots.Get(key)
if err != nil || raw == nil {
return Record{}, at, err
}
var rec Record
if err := json.Unmarshal(raw, &rec); err != nil {
return Record{}, at, fmt.Errorf("the snapshot of %s is not a record: %w", key, err)
}
return rec, at, nil
}
// derive is the instance's record caught up with its history up to last: from the state, or from the snapshot when it
// is further on (an instance whose state was lost, or whose operations expired, ADR 0597 §8).
func (l *Ledger) derive(key string, base Record, last uint64) (Record, error) {
if base.Sequence >= last {
return base, nil
}
snap, _, err := l.readSnapshot(key)
if err != nil {
return base, err
}
if snap.Sequence > base.Sequence {
base = snap
}
return l.replay(key, base, last)
}
// settle writes want into the state at the revision read (ADR 0597 §5): a lost update re-reads, and catches the state
// read up to want's sequence from the history, never appending again; a state already further on is left as it is. It
// answers what the state holds after it, and whether it is behind because the bucket is full (ADR 0597 §6).
func (l *Ledger) settle(key string, want Record, rev uint64) (Record, bool, error) {
for tries := 0; tries < 16; tries++ {
raw, err := json.Marshal(want)
if err != nil {
return want, false, err
}
_, err = l.State.Write(key, raw, rev)
switch {
case err == nil:
return want, false, nil
case errors.Is(err, ErrFull):
return want, true, nil
case !errors.Is(err, ErrChanged):
return want, false, err
}
cur, curRev, err := l.readState(key)
if err != nil {
return want, false, err
}
if cur.Sequence >= want.Sequence {
return cur, false, nil
}
if want, err = l.replay(key, cur, want.Sequence); err != nil {
return want, false, err
}
rev = curRev
}
return want, false, fmt.Errorf("the state of %s kept being written by other writers; the operation is in its history and the next apply catches it up", key)
}
// writeSnapshot writes rec as the instance's snapshot unless the snapshot is already as far on (ADR 0597 §8).
func (l *Ledger) writeSnapshot(key string, rec Record) error {
for tries := 0; tries < 16; tries++ {
snap, at, err := l.readSnapshot(key)
if err != nil {
return err
}
if at != 0 && snap.Sequence >= rec.Sequence {
return nil
}
raw, err := json.Marshal(rec)
if err != nil {
return err
}
if err = l.Snapshots.Put(key, raw, at); err == nil || !errors.Is(err, ErrRaced) {
return err
}
}
return fmt.Errorf("the snapshot of %s kept being written by other writers", key)
}
// Apply applies one operation to one instance, stamped with author (ADR 0597 §3, §5, §6, §8; design 25 § 12.4 steps 1-5):
// checked against its type, appended with the instance's last sequence expected, the state derived by its effect and
// written at the revision read, and the snapshot written. A racing writer's append is refused; a full state bucket
// answers taken with the state behind.
func (l *Ledger) Apply(r buscalls.ApplyRequest, author buscalls.Author) (buscalls.Applied, error) {
c, err := l.check(r)
if err != nil {
return buscalls.Applied{}, err
}
last, err := l.History.Last(c.key)
if err != nil {
return buscalls.Applied{}, err
}
held, rev, err := l.readState(c.key)
if err != nil {
return buscalls.Applied{}, err
}
base, err := l.derive(c.key, held, last)
if err != nil {
return buscalls.Applied{}, err
}
body := r.Body
if len(body) == 0 {
body = json.RawMessage("{}")
}
op, err := json.Marshal(buscalls.Operation{Instance: r.Instance, Operation: r.Operation, Body: body, Author: author, At: l.now()})
if err != nil {
return buscalls.Applied{}, err
}
seq, err := l.History.Append(c.key, op, last)
if errors.Is(err, ErrRaced) {
return buscalls.Applied{}, raced{r.Instance}
}
if err != nil {
return buscalls.Applied{}, err
}
state, err := effect(base.State, c.op, c.body, seq, author)
if err != nil {
return buscalls.Applied{}, err
}
// From here the operation is taken: it is in the history, and a fault below is the state's to catch up, said here
// and answered as the state behind, never as a refusal of what later takes effect (ADR 0597 §6).
now, behind, err := l.settle(c.key, Record{Sequence: seq, State: state}, rev)
if err != nil {
l.say("[mesh-tools] %s's ledger %s took operation %d on %s and its state could not follow: %v; the next apply "+
"or a rebuild catches it up", l.Module, l.Name, seq, r.Instance, err)
behind, now = true, Record{Sequence: seq, State: state}
}
if behind {
now = Record{Sequence: seq, State: state}
}
if err := l.writeSnapshot(c.key, now); err != nil {
l.say("[mesh-tools] %s's ledger %s took operation %d on %s and its snapshot could not be written: %v; the next "+
"apply or a snapshot repair writes it", l.Module, l.Name, seq, r.Instance, err)
}
return buscalls.Applied{Taken: true, Sequence: seq, StateBehind: behind}, nil
}