281 lines
10 KiB
Go
281 lines
10 KiB
Go
package runtime
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"sync"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
"github.com/novox/mesh-tools/node-tools/internal/launch"
|
|
"github.com/novox/mesh-tools/node-tools/internal/ledger"
|
|
)
|
|
|
|
// A module's ledgers as the tool runner serves them (novox/hq ADR 0597, design 25 § 12.4, § 12.5; issue 1966): opened
|
|
// for the module whose bundle or whose repair tool asks, from its own bindings alone, with its types and the connection
|
|
// its history is published on.
|
|
|
|
// ledgerSource is how the runner opens a module's ledger beside its binding: the connection its history and snapshots
|
|
// are published on (the module's ledger credential, ADR 0597 §9).
|
|
type ledgerSource struct {
|
|
writer func(module string) (*bus.Conn, error)
|
|
}
|
|
|
|
// callsInFlight are the tool calls the runtime is serving, each with the module whose bundle serves it and the caller
|
|
// the runtime proved from the subject the call arrived on (bus.CalledSubject, novox/hq issue 365).
|
|
type callsInFlight struct {
|
|
mu sync.Mutex
|
|
of map[string]callInFlight
|
|
}
|
|
|
|
type callInFlight struct{ module, caller string }
|
|
|
|
// begin keeps a call of module in flight for caller, under an id no bundle can guess, until end.
|
|
func (f *callsInFlight) begin(module, caller string) (string, func()) {
|
|
var raw [16]byte
|
|
_, _ = rand.Read(raw[:])
|
|
id := hex.EncodeToString(raw[:])
|
|
f.mu.Lock()
|
|
if f.of == nil {
|
|
f.of = map[string]callInFlight{}
|
|
}
|
|
f.of[id] = callInFlight{module, caller}
|
|
f.mu.Unlock()
|
|
return id, func() {
|
|
f.mu.Lock()
|
|
delete(f.of, id)
|
|
f.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// authorOf is the author an apply of module naming call is stamped with (ADR 0597 §7): the module's account, and the
|
|
// caller the runtime proved for that call; naming no call, the account alone. A call not in flight, or another
|
|
// module's, is refused, naming it. Nothing the bundle writes is taken as the author.
|
|
func (f *callsInFlight) authorOf(module, account, call string) (buscalls.Author, error) {
|
|
author := buscalls.Author{Account: account}
|
|
if call == "" {
|
|
return author, nil
|
|
}
|
|
f.mu.Lock()
|
|
inFlight, ok := f.of[call]
|
|
f.mu.Unlock()
|
|
switch {
|
|
case !ok:
|
|
return author, fmt.Errorf("the apply names the call %s, which is not a tool call in flight here; an apply names the "+
|
|
"call it serves as its tools/call's _meta %s gave it, or none (novox/hq ADR 0597 §7)", call, buscalls.MetaCall)
|
|
case inFlight.module != module:
|
|
return author, fmt.Errorf("the apply names the call %s, which is another module's tool call; %s names only its own "+
|
|
"(novox/hq ADR 0597 §7)", call, module)
|
|
}
|
|
author.Caller = inFlight.caller
|
|
return author, nil
|
|
}
|
|
|
|
// Begin keeps one of the module's tool calls in flight for the caller the runtime proved (launch.Bus).
|
|
func (b *moduleBus) Begin(caller string) (string, func()) { return b.all.calls.begin(b.module, caller) }
|
|
|
|
// ledger opens one of a module's ledgers by its local name.
|
|
func (c *consumers) ledger(module, local string) (*ledger.Ledger, error) {
|
|
b, err := c.conn.Binding(module, local, catalogue.StorageLedger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Its types travel on its binding, copied by the grants part from the entry the manifest-accepted event carries
|
|
// (mesh-sdk membership.BusBinding.Types): the runner reads no other part's state for them (ADR 0596).
|
|
ts := b.Types
|
|
if len(ts) == 0 {
|
|
return nil, fmt.Errorf("%s's ledger %q is bound with no types, so no operation can be checked against one: its "+
|
|
"binding carries the types its manifest asks for in `requires` (novox/hq ADR 0597 §1)", module, local)
|
|
}
|
|
var writer *bus.Conn
|
|
if c.ledgers.writer != nil {
|
|
if writer, err = c.ledgers.writer(module); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
history, state, snapshots, err := c.conn.LedgerStores(module, b, writer)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// The owning module's authenticated account: the user of the connection its history is published on.
|
|
account := module
|
|
if writer != nil && writer.Credential().User != "" {
|
|
account = writer.Credential().User
|
|
}
|
|
return &ledger.Ledger{Module: module, Name: local, Account: account, Types: ts, History: history, State: state,
|
|
Snapshots: snapshots, Indexes: c.conn.LedgerIndexes(b), Logf: c.logf}, nil
|
|
}
|
|
|
|
// ledgerCall answers a bundle's ledger call, as the module whose bundle asks.
|
|
func (b *moduleBus) ledgerCall(method string, params json.RawMessage) (any, error) {
|
|
var named struct {
|
|
Ledger string `json:"ledger"`
|
|
}
|
|
if err := decode(method, params, &named); err != nil {
|
|
return nil, err
|
|
}
|
|
if method == buscalls.LedgerApply {
|
|
var r buscalls.ApplyRequest
|
|
if err := decode(method, params, &r); err != nil {
|
|
return nil, err
|
|
}
|
|
// The call is judged first: a forged one is refused before anything of the ledger is touched.
|
|
if _, err := b.all.calls.authorOf(b.module, "", r.Call); err != nil {
|
|
return nil, err
|
|
}
|
|
l, err := b.all.ledger(b.module, r.Ledger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
author, err := b.all.calls.authorOf(b.module, l.Account, r.Call)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return l.Apply(r, author)
|
|
}
|
|
l, err := b.all.ledger(b.module, named.Ledger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch method {
|
|
case buscalls.LedgerGet:
|
|
var r buscalls.InstanceRequest
|
|
_ = json.Unmarshal(params, &r)
|
|
return l.Get(r)
|
|
case buscalls.LedgerHistory:
|
|
var r buscalls.HistoryRequest
|
|
_ = json.Unmarshal(params, &r)
|
|
return l.ReadHistory(r)
|
|
case buscalls.LedgerQuery:
|
|
var r buscalls.QueryRequest
|
|
_ = json.Unmarshal(params, &r)
|
|
return l.Query(r)
|
|
case buscalls.LedgerRebuild:
|
|
var r buscalls.RebuildRequest
|
|
_ = json.Unmarshal(params, &r)
|
|
return l.Rebuild(r)
|
|
case buscalls.LedgerReindex:
|
|
var r buscalls.ReindexRequest
|
|
_ = json.Unmarshal(params, &r)
|
|
return l.Reindex(r)
|
|
case buscalls.LedgerSnapshot:
|
|
var r buscalls.InstanceRequest
|
|
_ = json.Unmarshal(params, &r)
|
|
return l.Snapshot(r)
|
|
case buscalls.LedgerLag:
|
|
var r buscalls.LedgerRequest
|
|
_ = json.Unmarshal(params, &r)
|
|
return l.Lag(r)
|
|
}
|
|
return nil, fmt.Errorf("the runtime answers no %s", method)
|
|
}
|
|
|
|
// ledgerWatch hands a bundle each change of its ledger's states.
|
|
func (b *moduleBus) ledgerWatch(params json.RawMessage, deliver func(json.RawMessage) error) (func(), error) {
|
|
var r buscalls.LedgerWatchRequest
|
|
if err := decode(buscalls.LedgerWatch, params, &r); err != nil {
|
|
return nil, err
|
|
}
|
|
l, err := b.all.ledger(b.module, r.Ledger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return l.Watch(r, func(change buscalls.InstanceChange) error {
|
|
raw, err := json.Marshal(change)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return deliver(raw)
|
|
})
|
|
}
|
|
|
|
// hasLedger is whether a module's membership binds a ledger.
|
|
func hasLedger(conn *bus.Conn, module string) bool {
|
|
for _, b := range conn.Bindings(module) {
|
|
if b.Storage == catalogue.StorageLedger {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// The repair tools the runner serves for every module owning a ledger (design 25 § 12.5), named so no bundle's tool is
|
|
// taken for one.
|
|
const (
|
|
RepairRebuild = "ledger_rebuild"
|
|
RepairReindex = "ledger_reindex"
|
|
RepairSnapshot = "ledger_snapshot"
|
|
RepairLag = "ledger_lag"
|
|
)
|
|
|
|
// The effects a tool carries (novox/hq ADR 0387 §1).
|
|
const (
|
|
EffectReads = "reads"
|
|
EffectActs = "acts"
|
|
)
|
|
|
|
// repairTools are a module's ledger repairs as tools, each limited to its own ledgers: the request's ledger is a local
|
|
// name of the module whose tool is called.
|
|
func (c *consumers) repairTools(module string) []launch.Tool {
|
|
schema := func(props string, required ...string) json.RawMessage {
|
|
req, _ := json.Marshal(append([]string{"ledger"}, required...))
|
|
return json.RawMessage(`{"type":"object","properties":{"ledger":{"type":"string","description":"the ledger's local name"}` +
|
|
props + `},"required":` + string(req) + `}`)
|
|
}
|
|
instance := `,"instance":{"type":"string","description":"one instance, such as decision/0290; every instance when empty"}`
|
|
run := func(do func(l *ledger.Ledger, args json.RawMessage) (any, error)) func(string, json.RawMessage) (json.RawMessage, error) {
|
|
return func(_ string, args json.RawMessage) (json.RawMessage, error) {
|
|
var named struct {
|
|
Ledger string `json:"ledger"`
|
|
}
|
|
if err := json.Unmarshal(args, &named); err != nil || named.Ledger == "" {
|
|
return nil, errors.New("a repair names the ledger, by its local name")
|
|
}
|
|
l, err := c.ledger(module, named.Ledger)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
answer, err := do(l, args)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return json.Marshal(answer)
|
|
}
|
|
}
|
|
return []launch.Tool{
|
|
{Name: RepairRebuild, Effect: EffectActs, Input: schema(instance + `,"from":{"type":"string","enum":["history","snapshot"],"description":"derive from the whole history (the default) or from the snapshot and the operations after it"}`),
|
|
Description: "Derive a ledger instance's state again from its history or its snapshot, and write it and its snapshot (novox/hq ADR 0597 §10).",
|
|
Run: run(func(l *ledger.Ledger, args json.RawMessage) (any, error) {
|
|
var r buscalls.RebuildRequest
|
|
_ = json.Unmarshal(args, &r)
|
|
return l.Rebuild(r)
|
|
})},
|
|
{Name: RepairReindex, Effect: EffectActs, Input: schema(`,"index":{"type":"string","description":"the declared index"}`, "index"),
|
|
Description: "Build a ledger's declared index again from its states, derived from its history (novox/hq ADR 0597 §4, §10).",
|
|
Run: run(func(l *ledger.Ledger, args json.RawMessage) (any, error) {
|
|
var r buscalls.ReindexRequest
|
|
_ = json.Unmarshal(args, &r)
|
|
return l.Reindex(r)
|
|
})},
|
|
{Name: RepairSnapshot, Effect: EffectActs, Input: schema(instance),
|
|
Description: "Rewrite the snapshot of a ledger instance that is behind its state (novox/hq ADR 0597 §8, §10).",
|
|
Run: run(func(l *ledger.Ledger, args json.RawMessage) (any, error) {
|
|
var r buscalls.InstanceRequest
|
|
_ = json.Unmarshal(args, &r)
|
|
return l.Snapshot(r)
|
|
})},
|
|
{Name: RepairLag, Effect: EffectReads, Input: schema(""),
|
|
Description: "Say every ledger instance whose state or snapshot is behind its history, and each declared index behind it (novox/hq ADR 0597 §10).",
|
|
Run: run(func(l *ledger.Ledger, args json.RawMessage) (any, error) {
|
|
var r buscalls.LedgerRequest
|
|
_ = json.Unmarshal(args, &r)
|
|
return l.Lag(r)
|
|
})},
|
|
}
|
|
}
|