With no plan left to take a done key in, a write of the builder's keys registers the new fingerprint, composes the module's nodes and the bus's user list, and writes its grant, so the assigner's rules can name it (ADR 0539 clauses 5 and 7). The new controller composes every node at its start, and the controller it replaced at its own fingerprint, the operator's way back (clause 10); the merge window's, silent sends' and building plans' buckets are no longer asserted and are deleted once, each said as the event bucket-deleted (clause 13). The assigner may read the compositions' digests, and a node-engine hears its backup holder's backed-up.
124 lines
5.6 KiB
Go
124 lines
5.6 KiB
Go
package broker
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
)
|
|
|
|
// The compositions' buckets (novox/hq ADR 0539 clauses 2 and 4): one per node, `composed-<node>`, holding that node's
|
|
// compositions, which only that node's node-engine reads, since a composition can carry a sealed value (ADR 0488 §2's
|
|
// reason); and `mesh-controller_composed`, holding each composition's digest and the judgement of its move, which the
|
|
// assigner reads. The controller writes both, by compare-and-set while it holds the lease; each keeps five revisions
|
|
// of a key. The names and layouts are mesh-sdk's (delivery.ComposedBucketFor, ControllerComposedBucket, ComposedKey).
|
|
|
|
// ControllerComposed is the local name of the compositions' digests, as a reader names it in `reads` after
|
|
// `mesh-controller.`: delivery.ControllerComposedBucket.
|
|
const ControllerComposed = "composed"
|
|
|
|
// ComposedValueBytes is the most a composition may hold: a file over 64 KiB travels by digest, so a module's
|
|
// composition is far smaller; just under the server's largest message, which also carries the subject and headers.
|
|
const ComposedValueBytes = 1<<20 - 8<<10
|
|
|
|
// ComposedBucketBytes is a node's composition bucket's size: five revisions of every module's compositions of the node.
|
|
const ComposedBucketBytes = 256 << 20
|
|
|
|
// ComposedDigestsBytes is the size of the bucket of digests: a value of a few hundred bytes per node, module and
|
|
// fingerprint kept.
|
|
const ComposedDigestsBytes = 64 << 20
|
|
|
|
// ComposedConfig is a node's composition bucket as the controller asserts it.
|
|
func ComposedConfig(node string) (jetstream.KeyValueConfig, error) {
|
|
bucket, err := delivery.ComposedBucketFor(node)
|
|
if err != nil {
|
|
return jetstream.KeyValueConfig{}, err
|
|
}
|
|
return jetstream.KeyValueConfig{
|
|
Bucket: bucket,
|
|
Description: "the compositions of " + node + " (novox/hq ADR 0539): each module's resources for this node at one " +
|
|
"fingerprint, and the node's own, signed; written by the controller alone, read by this node's node-engine " +
|
|
"alone; deleted when outside the module's five newest fingerprints and named by no want",
|
|
History: delivery.ComposedHistory,
|
|
MaxValueSize: ComposedValueBytes,
|
|
MaxBytes: ComposedBucketBytes,
|
|
Storage: jetstream.FileStorage,
|
|
}, nil
|
|
}
|
|
|
|
// ComposedDigestsConfig is the bucket of the compositions' digests as the controller asserts it.
|
|
func ComposedDigestsConfig() jetstream.KeyValueConfig {
|
|
return jetstream.KeyValueConfig{
|
|
Bucket: delivery.ControllerComposedBucket,
|
|
Description: "each composition's digest and the judgement of its move from what the node runs (novox/hq ADR 0539 " +
|
|
"clauses 2 and 14): written by the controller alone, read by the assigner",
|
|
History: delivery.ControllerComposedHistory,
|
|
MaxValueSize: 4 << 10,
|
|
MaxBytes: ComposedDigestsBytes,
|
|
Storage: jetstream.FileStorage,
|
|
}
|
|
}
|
|
|
|
// EnsureComposed creates the bucket of digests and each named node's composition bucket if absent and brings their
|
|
// options to match. An update, never a delete.
|
|
func EnsureComposed(ctx context.Context, js jetstream.JetStream, nodes ...string) error {
|
|
if _, err := js.CreateOrUpdateKeyValue(ctx, ComposedDigestsConfig()); err != nil {
|
|
return fmt.Errorf("asserting bucket %s: %w", delivery.ControllerComposedBucket, err)
|
|
}
|
|
for _, node := range nodes {
|
|
cfg, err := ComposedConfig(node)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := js.CreateOrUpdateKeyValue(ctx, cfg); err != nil {
|
|
return fmt.Errorf("asserting bucket %s: %w", cfg.Bucket, err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// ComposedWrites is what the controller publishes to write a node's compositions: the bucket's own subjects.
|
|
func ComposedWrites(node string) (string, error) {
|
|
bucket, err := delivery.ComposedBucketFor(node)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return "$KV." + bucket + ".>", nil
|
|
}
|
|
|
|
// ComposedReads is what a node's node-engine publishes to follow its wants and compositions (ADR 0539 clauses 1, 2 and
|
|
// 9): every key of its own composition bucket, and its own wants (`want.<node>.<module>` and `want.<node>.node`) in the
|
|
// assigner's bucket; nothing of another node's, nor the bucket of digests.
|
|
func ComposedReads(node string) ([]string, error) {
|
|
bucket, err := delivery.ComposedBucketFor(node)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if !safeSubject.MatchString(node) {
|
|
return nil, fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node)
|
|
}
|
|
return append(readGrants(bucket, ""), readGrants(delivery.AssignerWantsBucket, "want."+node+".>")...), nil
|
|
}
|
|
|
|
// VerbsTheNodeAgentAsks are the seat verbs a node's node-engine calls of its own machine while it applies a want (ADR
|
|
// 0539 clause 16, ADR 0429 §14): its backup holder backs a data module up before it moves, and says when it was. Asked
|
|
// of its own machine only, `mesh.seat.<seat>.tool.<verb>.<node>`.
|
|
var VerbsTheNodeAgentAsks = []SeatVerb{{Seat: "node-backup", Verb: "now"}, {Seat: "node-backup", Verb: "backed-up"}}
|
|
|
|
// NodeAgentAsks are the subjects of VerbsTheNodeAgentAsks for one node.
|
|
func NodeAgentAsks(node string) ([]string, error) {
|
|
if !safeSubject.MatchString(node) {
|
|
return nil, fmt.Errorf("%q cannot be part of a subject: a permission is a subject pattern, and this would widen it", node)
|
|
}
|
|
var out []string
|
|
for _, v := range VerbsTheNodeAgentAsks {
|
|
out = append(out, "mesh.seat."+v.Seat+".tool."+v.Verb+"."+node)
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// NodeAgentHears are the events a node's node-engine subscribes while it applies a want: the backup holder's word that a
|
|
// backup is taken (`backed-up`), from whichever module holds node-backup on its machine (restic today).
|
|
var NodeAgentHears = []string{"mesh.mod.*.event.backed-up"}
|