Merge branch 'b7/registry' into b7f/registry
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
|
||||
|
||||
// Package meshartifactsweep is the contract of the seat "mesh-artifact-sweep", defined whole (seats/mesh-artifact-sweep/seat.json): no description.
|
||||
package meshartifactsweep
|
||||
|
||||
import "time"
|
||||
|
||||
// Seat is the seat's name.
|
||||
const Seat = "mesh-artifact-sweep"
|
||||
|
||||
const (
|
||||
VerbArtifacts = "artifacts"
|
||||
VerbHeld = "held"
|
||||
VerbCollect = "collect"
|
||||
VerbMirrors = "mirrors"
|
||||
VerbImages = "images"
|
||||
)
|
||||
|
||||
const (
|
||||
EventArtifactsGone = "artifacts-gone"
|
||||
)
|
||||
|
||||
// Verbs is every verb the seat serves, in its definition's order: a new copy each call.
|
||||
func Verbs() []string {
|
||||
return []string{VerbArtifacts, VerbHeld, VerbCollect, VerbMirrors, VerbImages}
|
||||
}
|
||||
|
||||
// Events is every event published on the seat's subject: a new copy each call.
|
||||
func Events() []string {
|
||||
return []string{EventArtifactsGone}
|
||||
}
|
||||
|
||||
// Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects.
|
||||
const (
|
||||
Family = "mesh-artifact-sweep.events"
|
||||
FamilyMaxAgeDays = 7
|
||||
)
|
||||
|
||||
// SubjectArtifactsGone is where artifacts-gone is published.
|
||||
const SubjectArtifactsGone = "mesh.seat.mesh-artifact-sweep.event.artifacts-gone"
|
||||
|
||||
// ArtifactsGone is the body of artifacts-gone.
|
||||
type ArtifactsGone struct {
|
||||
// when the sweep found it
|
||||
At *time.Time `json:"at,omitempty"`
|
||||
// artifacts-gone
|
||||
Event string `json:"event"`
|
||||
// the key's fingerprint
|
||||
Fingerprint string `json:"fingerprint"`
|
||||
// every reference of the key the store does not hold, sorted
|
||||
Gone []string `json:"gone"`
|
||||
// the builder's done key
|
||||
Key string `json:"key,omitempty"`
|
||||
// the module whose builder's key names a gone artifact
|
||||
Module string `json:"module"`
|
||||
}
|
||||
|
||||
// ArtifactsInput is what artifacts takes.
|
||||
type ArtifactsInput struct {
|
||||
// "true": list the collected references too (counted always)
|
||||
Collected string `json:"collected,omitempty"`
|
||||
// only this repository's references, as <module>/<artifact> (the counts stay whole)
|
||||
Repository string `json:"repository,omitempty"`
|
||||
}
|
||||
|
||||
// The values of ArtifactsInput's collected.
|
||||
const (
|
||||
ArtifactsInputCollectedTrue = "true"
|
||||
ArtifactsInputCollectedFalse = "false"
|
||||
)
|
||||
|
||||
// ArtifactsAnswer is what artifacts answers.
|
||||
type ArtifactsAnswer = map[string]any
|
||||
|
||||
// HeldInput is what held takes.
|
||||
type HeldInput struct {
|
||||
// the artifacts' references, artifact-store://<module>/<artifact>/blobs/sha256:<hex>: a list, or one text separated by commas
|
||||
References []string `json:"references"`
|
||||
}
|
||||
|
||||
// HeldAnswer is what held answers.
|
||||
type HeldAnswer struct {
|
||||
// the references the store answered 404 for
|
||||
Gone []string `json:"gone"`
|
||||
// the references the store holds
|
||||
Held []string `json:"held"`
|
||||
// the store asked, host and port
|
||||
Store string `json:"store,omitempty"`
|
||||
// a reference the store could not be asked about, to why
|
||||
Unknown map[string]string `json:"unknown,omitempty"`
|
||||
}
|
||||
|
||||
// CollectInput is what collect takes.
|
||||
type CollectInput struct {
|
||||
// "true": let go of what is eligible (needs why); without it nothing is done
|
||||
Confirm string `json:"confirm,omitempty"`
|
||||
// let go of at most this many (500 by default, at most 5000)
|
||||
Most string `json:"most,omitempty"`
|
||||
// with confirm: why, required and recorded
|
||||
Why string `json:"why,omitempty"`
|
||||
}
|
||||
|
||||
// The values of CollectInput's confirm.
|
||||
const (
|
||||
CollectInputConfirmTrue = "true"
|
||||
CollectInputConfirmFalse = "false"
|
||||
)
|
||||
|
||||
// CollectAnswer is what collect answers.
|
||||
type CollectAnswer = map[string]any
|
||||
|
||||
// MirrorsInput is what mirrors takes.
|
||||
type MirrorsInput struct {
|
||||
}
|
||||
|
||||
// MirrorsAnswer is what mirrors answers.
|
||||
type MirrorsAnswer = map[string]any
|
||||
|
||||
// ImagesInput is what images takes.
|
||||
type ImagesInput struct {
|
||||
// the machine's name
|
||||
Node string `json:"node"`
|
||||
}
|
||||
|
||||
// ImagesAnswer is what images answers.
|
||||
type ImagesAnswer = map[string]any
|
||||
|
||||
// ArtifactsGoneEvent is the body of the event artifacts-gone.
|
||||
type ArtifactsGoneEvent = ArtifactsGone
|
||||
@@ -0,0 +1,54 @@
|
||||
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
|
||||
|
||||
package meshartifactsweep
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestFixtures reads every fixture of seats/mesh-artifact-sweep/fixtures: each decodes into its type, a field the type does not
|
||||
// know is dropped (ADR 0608 §6), and it reads back as its wire says; each subject named is the contract's.
|
||||
func TestFixtures(t *testing.T) {
|
||||
cases := []struct {
|
||||
name, subject, given, wire string
|
||||
decode func([]byte) (any, error)
|
||||
subjectIs *regexp.Regexp
|
||||
}{
|
||||
{name: "artifacts-gone on the seat's subject", subject: "mesh.seat.mesh-artifact-sweep.event.artifacts-gone", given: "{\"at\":\"2026-11-05T15:04:05Z\",\"event\":\"artifacts-gone\",\"fingerprint\":\"f1\",\"gone\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"key\":\"ca-trust.archive.f1\",\"module\":\"ca-trust\"}", wire: "{\"at\":\"2026-11-05T15:04:05Z\",\"event\":\"artifacts-gone\",\"fingerprint\":\"f1\",\"gone\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"key\":\"ca-trust.archive.f1\",\"module\":\"ca-trust\"}",
|
||||
decode: func(b []byte) (any, error) { var v ArtifactsGoneEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-artifact-sweep\\.event\\.artifacts-gone$")},
|
||||
{name: "held asks the references", subject: "", given: "{\"references\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\"]}", wire: "{\"references\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\",\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\",\"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\"]}",
|
||||
decode: func(b []byte) (any, error) { var v HeldInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
|
||||
{name: "held answers held, gone and unknown", subject: "", given: "{\"gone\":[\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"held\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"],\"store\":\"store.test:5100\",\"unknown\":{\"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\":\"the store answered 503\"}}", wire: "{\"gone\":[\"artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb\"],\"held\":[\"artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\"],\"store\":\"store.test:5100\",\"unknown\":{\"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc\":\"the store answered 503\"}}",
|
||||
decode: func(b []byte) (any, error) { var v HeldAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
|
||||
{name: "held answers with nothing unknown", subject: "", given: "{\"gone\":[],\"held\":[],\"store\":\"store.test:5100\"}", wire: "{\"gone\":[],\"held\":[],\"store\":\"store.test:5100\"}",
|
||||
decode: func(b []byte) (any, error) { var v HeldAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil},
|
||||
}
|
||||
for _, c := range cases {
|
||||
v, err := c.decode([]byte(c.given))
|
||||
if err != nil {
|
||||
t.Errorf("%s: does not decode: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
back, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
t.Errorf("%s: does not encode: %v", c.name, err)
|
||||
continue
|
||||
}
|
||||
var got, want any
|
||||
if err := json.Unmarshal(back, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := json.Unmarshal([]byte(c.wire), &want); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Errorf("%s: reads back as %s, want %s", c.name, back, c.wire)
|
||||
}
|
||||
if c.subjectIs != nil && !c.subjectIs.MatchString(c.subject) {
|
||||
t.Errorf("%s: subject %q is not the contract's %s", c.name, c.subject, c.subjectIs)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
module git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep
|
||||
|
||||
go 1.26.0
|
||||
@@ -23,6 +23,16 @@
|
||||
"series": "0.1",
|
||||
"generated-from": "."
|
||||
},
|
||||
{
|
||||
"name": "go-meshartifactsweep",
|
||||
"kind": "package",
|
||||
"language": "go",
|
||||
"paths": [
|
||||
"go/seats/meshartifactsweep"
|
||||
],
|
||||
"series": "0.1",
|
||||
"generated-from": "."
|
||||
},
|
||||
{
|
||||
"name": "go-meshcatalog",
|
||||
"kind": "package",
|
||||
@@ -186,6 +196,16 @@
|
||||
"series": "0.1",
|
||||
"generated-from": "."
|
||||
},
|
||||
{
|
||||
"name": "ts-meshartifactsweep",
|
||||
"kind": "package",
|
||||
"language": "typescript",
|
||||
"paths": [
|
||||
"ts/seats/meshartifactsweep"
|
||||
],
|
||||
"series": "0.1",
|
||||
"generated-from": "."
|
||||
},
|
||||
{
|
||||
"name": "ts-meshcatalog",
|
||||
"kind": "package",
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{
|
||||
"name": "artifacts-gone on the seat's subject",
|
||||
"of": "event:artifacts-gone",
|
||||
"subject": "mesh.seat.mesh-artifact-sweep.event.artifacts-gone",
|
||||
"given": {
|
||||
"event": "artifacts-gone",
|
||||
"module": "ca-trust",
|
||||
"key": "ca-trust.archive.f1",
|
||||
"fingerprint": "f1",
|
||||
"gone": ["artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"],
|
||||
"at": "2026-11-05T15:04:05Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1 @@
|
||||
[]
|
||||
@@ -0,0 +1,24 @@
|
||||
[
|
||||
{
|
||||
"name": "held asks the references",
|
||||
"of": "verb:held.input",
|
||||
"why": "the references are the holder's own test values (the tool refuses a call without them through the console)",
|
||||
"given": {"references": ["artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", "artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"]}
|
||||
},
|
||||
{
|
||||
"name": "held answers held, gone and unknown",
|
||||
"of": "verb:held.answer",
|
||||
"why": "the answer of the holder's test (cmd/artifact-store-tools): every reference in one of the three",
|
||||
"given": {
|
||||
"store": "store.test:5100",
|
||||
"held": ["artifact-store://ca-trust/archive/blobs/sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"],
|
||||
"gone": ["artifact-store://ca-trust/archive/blobs/sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"],
|
||||
"unknown": {"artifact-store://ca-trust/archive/blobs/sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc": "the store answered 503"}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "held answers with nothing unknown",
|
||||
"of": "verb:held.answer",
|
||||
"given": {"store": "store.test:5100", "held": [], "gone": []}
|
||||
}
|
||||
]
|
||||
@@ -2,7 +2,7 @@
|
||||
"format": 1,
|
||||
"seat": "mesh-artifact-sweep",
|
||||
"scope": "mesh",
|
||||
"grain": "seat-list",
|
||||
"grain": "whole",
|
||||
"domain": "Delivery",
|
||||
"decision": "novox/hq ADR 0571",
|
||||
"verbs": [
|
||||
@@ -25,6 +25,10 @@
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "what artifacts answers, as its description says"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -44,6 +48,41 @@
|
||||
"references"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"store": {
|
||||
"type": "string",
|
||||
"description": "the store asked, host and port"
|
||||
},
|
||||
"held": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "the references the store holds"
|
||||
},
|
||||
"gone": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "the references the store answered 404 for"
|
||||
},
|
||||
"unknown": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "a reference the store could not be asked about, to why"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"held",
|
||||
"gone"
|
||||
],
|
||||
"description": "what held answers: a reference is in held, gone or unknown, never none of them"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -69,6 +108,10 @@
|
||||
}
|
||||
},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "what collect answers, as its description says"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -77,6 +120,10 @@
|
||||
"input": {
|
||||
"properties": {},
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "what mirrors answers, as its description says"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -93,12 +140,65 @@
|
||||
"node"
|
||||
],
|
||||
"type": "object"
|
||||
},
|
||||
"answer": {
|
||||
"type": "object",
|
||||
"description": "what images answers, as its description says"
|
||||
}
|
||||
}
|
||||
],
|
||||
"family": {
|
||||
"name": "mesh-artifact-sweep.events",
|
||||
"max-age-days": 7
|
||||
},
|
||||
"events": [
|
||||
{
|
||||
"name": "artifacts-gone"
|
||||
"name": "artifacts-gone",
|
||||
"body": {
|
||||
"$ref": "#/defs/ArtifactsGone"
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
"defs": {
|
||||
"ArtifactsGone": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"event": {
|
||||
"type": "string",
|
||||
"description": "artifacts-gone"
|
||||
},
|
||||
"module": {
|
||||
"type": "string",
|
||||
"description": "the module whose builder's key names a gone artifact"
|
||||
},
|
||||
"key": {
|
||||
"type": "string",
|
||||
"description": "the builder's done key"
|
||||
},
|
||||
"fingerprint": {
|
||||
"type": "string",
|
||||
"description": "the key's fingerprint"
|
||||
},
|
||||
"gone": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "every reference of the key the store does not hold, sorted"
|
||||
},
|
||||
"at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"description": "when the sweep found it"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"event",
|
||||
"module",
|
||||
"fingerprint",
|
||||
"gone"
|
||||
],
|
||||
"description": "the body of artifacts-gone"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"lockfileVersion": 3,
|
||||
"name": "@novox/mesh-delivery-contracts-mesh-artifact-sweep",
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@novox/mesh-delivery-contracts-mesh-artifact-sweep",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
},
|
||||
"requires": true,
|
||||
"version": "0.1.0"
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"description": "The contract of the seat mesh-artifact-sweep, generated from its definition.",
|
||||
"exports": {
|
||||
".": "./src/index.ts"
|
||||
},
|
||||
"files": [
|
||||
"src"
|
||||
],
|
||||
"name": "@novox/mesh-delivery-contracts-mesh-artifact-sweep",
|
||||
"type": "module",
|
||||
"types": "./src/index.ts",
|
||||
"version": "0.1.0"
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
// Code generated by seatgen from the seat definitions; DO NOT EDIT.
|
||||
|
||||
// The contract of the seat "mesh-artifact-sweep", defined whole (seats/mesh-artifact-sweep/seat.json): no description.
|
||||
|
||||
export const seat = "mesh-artifact-sweep";
|
||||
|
||||
export const verbs = {
|
||||
artifacts: "artifacts",
|
||||
held: "held",
|
||||
collect: "collect",
|
||||
mirrors: "mirrors",
|
||||
images: "images",
|
||||
} as const;
|
||||
|
||||
export const events = {
|
||||
artifactsGone: "artifacts-gone",
|
||||
} as const;
|
||||
|
||||
export const family = { name: "mesh-artifact-sweep.events", maxAgeDays: 7 } as const;
|
||||
|
||||
export const artifactsGoneSubject = "mesh.seat.mesh-artifact-sweep.event.artifacts-gone";
|
||||
|
||||
/** the body of artifacts-gone */
|
||||
export interface ArtifactsGone {
|
||||
/** when the sweep found it */
|
||||
"at"?: string;
|
||||
/** artifacts-gone */
|
||||
"event": string;
|
||||
/** the key's fingerprint */
|
||||
"fingerprint": string;
|
||||
/** every reference of the key the store does not hold, sorted */
|
||||
"gone": string[];
|
||||
/** the builder's done key */
|
||||
"key"?: string;
|
||||
/** the module whose builder's key names a gone artifact */
|
||||
"module": string;
|
||||
}
|
||||
|
||||
/** is what artifacts takes. */
|
||||
export interface ArtifactsInput {
|
||||
/** "true": list the collected references too (counted always) */
|
||||
"collected"?: "true" | "false";
|
||||
/** only this repository's references, as <module>/<artifact> (the counts stay whole) */
|
||||
"repository"?: string;
|
||||
}
|
||||
|
||||
/** is what artifacts answers. */
|
||||
export type ArtifactsAnswer = Record<string, unknown>;
|
||||
|
||||
/** is what held takes. */
|
||||
export interface HeldInput {
|
||||
/** the artifacts' references, artifact-store://<module>/<artifact>/blobs/sha256:<hex>: a list, or one text separated by commas */
|
||||
"references": string[];
|
||||
}
|
||||
|
||||
/** is what held answers. */
|
||||
export interface HeldAnswer {
|
||||
/** the references the store answered 404 for */
|
||||
"gone": string[];
|
||||
/** the references the store holds */
|
||||
"held": string[];
|
||||
/** the store asked, host and port */
|
||||
"store"?: string;
|
||||
/** a reference the store could not be asked about, to why */
|
||||
"unknown"?: Record<string, string>;
|
||||
}
|
||||
|
||||
/** is what collect takes. */
|
||||
export interface CollectInput {
|
||||
/** "true": let go of what is eligible (needs why); without it nothing is done */
|
||||
"confirm"?: "true" | "false";
|
||||
/** let go of at most this many (500 by default, at most 5000) */
|
||||
"most"?: string;
|
||||
/** with confirm: why, required and recorded */
|
||||
"why"?: string;
|
||||
}
|
||||
|
||||
/** is what collect answers. */
|
||||
export type CollectAnswer = Record<string, unknown>;
|
||||
|
||||
/** is what mirrors takes. */
|
||||
export interface MirrorsInput {
|
||||
}
|
||||
|
||||
/** is what mirrors answers. */
|
||||
export type MirrorsAnswer = Record<string, unknown>;
|
||||
|
||||
/** is what images takes. */
|
||||
export interface ImagesInput {
|
||||
/** the machine's name */
|
||||
"node": string;
|
||||
}
|
||||
|
||||
/** is what images answers. */
|
||||
export type ImagesAnswer = Record<string, unknown>;
|
||||
|
||||
/** is the body of the event artifacts-gone. */
|
||||
export type ArtifactsGoneEvent = ArtifactsGone;
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "NodeNext",
|
||||
"moduleResolution": "NodeNext",
|
||||
"noEmit": true,
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"forceConsistentCasingInFileNames": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
@@ -35,7 +36,7 @@ func (h *heldVerb) serve(t *testing.T, b *bed) {
|
||||
References []string `json:"references"`
|
||||
}
|
||||
_ = json.Unmarshal(m.Data, &args)
|
||||
a := delivery.HeldAnswer{Store: "store.test:5100", Held: []string{}, Gone: []string{}}
|
||||
a := sweep.HeldAnswer{Store: "store.test:5100", Held: []string{}, Gone: []string{}}
|
||||
h.mu.Lock()
|
||||
for _, r := range args.References {
|
||||
h.asked = append(h.asked, r)
|
||||
@@ -65,11 +66,11 @@ func (h *heldVerb) asks() int {
|
||||
// from the manifest's consumes.
|
||||
func eventsConsumer(t *testing.T, b *bed, node string) {
|
||||
t.Helper()
|
||||
if _, err := b.js.CreateStream(b.ctx, jetstream.StreamConfig{Name: eventsStream, Subjects: []string{delivery.ArtifactsGoneSubject}}); err != nil {
|
||||
if _, err := b.js.CreateOrUpdateStream(b.ctx, jetstream.StreamConfig{Name: eventsStream, Subjects: []string{"mesh.mod.*.event.>", "mesh.seat.*.event.>"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := b.js.CreateOrUpdateConsumer(b.ctx, eventsStream, jetstream.ConsumerConfig{Durable: node + "_mesh-delivery-builder",
|
||||
AckPolicy: jetstream.AckExplicitPolicy, FilterSubjects: []string{delivery.ArtifactsGoneSubject}}); err != nil {
|
||||
AckPolicy: jetstream.AckExplicitPolicy, FilterSubjects: []string{sweep.SubjectArtifactsGone}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -168,12 +169,13 @@ func TestArtifactsGoneLeadsTheBuilderToBuildTheKeyAgain(t *testing.T) {
|
||||
collected = true
|
||||
mu.Unlock()
|
||||
key, _ := delivery.BuilderBuildKey("ca-trust", fp)
|
||||
body, err := delivery.EncodeArtifactsGone(delivery.ArtifactsGone{Module: "ca-trust", Key: key, Fingerprint: fp,
|
||||
Gone: []string{ref}, At: time.Now().UTC()})
|
||||
at := time.Now().UTC()
|
||||
body, err := json.Marshal(sweep.ArtifactsGone{Event: sweep.EventArtifactsGone, Module: "ca-trust", Key: key, Fingerprint: fp,
|
||||
Gone: []string{ref}, At: &at})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := b.js.Publish(b.ctx, delivery.ArtifactsGoneSubject, body); err != nil {
|
||||
if _, err := b.js.Publish(b.ctx, sweep.SubjectArtifactsGone, body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
v := b.await(t, "ca-trust", func(v delivery.BuilderBuild) bool {
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
runner "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry"
|
||||
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
|
||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/buscompose"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
@@ -1191,11 +1192,10 @@ func (b *bed) registry(t *testing.T, node string, seatVersions ...string) *regis
|
||||
mk(versions.FloorWaitsBucket)
|
||||
// EVENTS with two seats' outcomes, and the forge bridge's stream (a branch's end): each with the builder's durable consumer.
|
||||
for stream, subjects := range map[string][]string{
|
||||
eventsStream: {delivery.ArtifactsGoneSubject, ociartifactregistry.SubjectPublished, ociartifactregistry.SubjectRefused, ociartifactregistry.SubjectRemoved,
|
||||
gopackageregistry.SubjectPublished, gopackageregistry.SubjectRefused, gopackageregistry.SubjectRemoved},
|
||||
eventsStream: {"mesh.mod.*.event.>", "mesh.seat.*.event.>"},
|
||||
forgeStream: {process.ModuleStreamSubject(delivery.ForgeBridgeModule, ">")},
|
||||
} {
|
||||
if _, err := b.js.CreateStream(b.ctx, jetstream.StreamConfig{Name: stream, Subjects: subjects}); err != nil {
|
||||
if _, err := b.js.CreateOrUpdateStream(b.ctx, jetstream.StreamConfig{Name: stream, Subjects: subjects}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := b.js.CreateOrUpdateConsumer(b.ctx, stream, jetstream.ConsumerConfig{Durable: node + "_" + delivery.BuilderModule,
|
||||
@@ -1216,10 +1216,6 @@ func (b *bed) registry(t *testing.T, node string, seatVersions ...string) *regis
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = rm.Unsubscribe() })
|
||||
own := "mesh.mod." + delivery.BuilderModule + ".event.>"
|
||||
if _, err := b.js.CreateStream(b.ctx, jetstream.StreamConfig{Name: "BUILDER_EVENTS", Subjects: []string{own}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
nc := b.js.Conn()
|
||||
// As a build module hears it: on the contract's subject, read by the contract's decoder (BG2's late publish).
|
||||
sub, err := nc.Subscribe(delivery.VersionNumberedSubject, func(m *nats.Msg) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
@@ -75,7 +76,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return err
|
||||
}
|
||||
defer calls.Close()
|
||||
reader := &views.ProcessReader{Module: delivery.BuilderModule, Call: calls.Call, Logf: logger.Printf}
|
||||
// The module has one durable consumer of EVENTS: one hub pulls it and hands each message to the part that hears it (the
|
||||
// views reader and the builder's own events), started once every part is registered.
|
||||
hub := process.NewEventHub(logger.Printf)
|
||||
reader := &views.ProcessReader{Module: delivery.BuilderModule, Call: calls.Call, Logf: logger.Printf, Hub: hub}
|
||||
findingsKV := process.AwaitBucket(ctx, js, conn, findingsBucket, "the builder's findings, a first view unanswered past its bound among them, are said in its log only", logger.Printf)
|
||||
reader.Findings = func() views.FindingWriter { return sdkfindings.Writer(ctx, findingsKV.KV) }
|
||||
modules, err := reader.Follow(delivery.IndexerModule, modulesState, delivery.IndexerModule+".indexer_read_modules")
|
||||
@@ -330,16 +334,29 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
floor := floorWake{b: b, waits: floorWaits, logf: logger.Printf}
|
||||
// And the four registry seats' published, refused and removed (DLV-B4), which move a numbered version.
|
||||
seats := seatHearer{n: numberer, b: b, logf: logger.Printf, pub: pubs}
|
||||
hear(eventsStream, delivery.ArtifactsGoneEvent+", what wakes a floor, the registry seats' words and manifest-reads-changed are not heard until the builder starts again", func(m jetstream.Msg) {
|
||||
events := func(m jetstream.Msg) {
|
||||
if reads.heard(m, logger.Printf) || floor.heard(ctx, m) || seats.heard(ctx, m) { // reads: the node-engine seat's manifest-reads-changed (ADR 0618 §4)
|
||||
return
|
||||
}
|
||||
heardGone(ctx, b, m, logger.Printf)
|
||||
})
|
||||
}
|
||||
// The seats' events (artifacts-gone, the registry seats' words, manifest-accepted and manifest-reads-changed) and the
|
||||
// condition keeper's condition-cleared; the owners' views events are the views reader's.
|
||||
for _, pattern := range []string{"mesh.seat.*.event.>", conditionClearedSubject} {
|
||||
if err := hub.Handle(pattern, events); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if stopped != nil {
|
||||
return stopped
|
||||
}
|
||||
reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "builder_first_views reads nothing and builder_first_view_again is not heard", logger.Printf))
|
||||
// Every part has registered: the hub pulls the module's durable. Where the consumer is not there yet the builder says so
|
||||
// and binds it at a later change (heard.Writer; ADR 0615 §5).
|
||||
_ = notHeard.Bind(eventsStream, sweep.EventArtifactsGone+", what wakes a floor, the registry seats' words, the views and manifest-reads-changed are not heard until the builder starts again", func() error {
|
||||
return hub.Start(ctx, js, cred)
|
||||
})
|
||||
go func() { <-ctx.Done(); hub.Stop() }()
|
||||
// The modules, once their first views and the manifests' are answered: what a module builds is read from the manifests.
|
||||
go func() { errs <- buildFromModules(ctx, b, modules, manifests, candidates) }()
|
||||
// A package's name heard in the indexer's packages looks at its module again, so a done build is numbered then.
|
||||
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/enginereads"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/forms"
|
||||
@@ -223,7 +224,7 @@ func aFloorsWakeIsHeard(t *testing.T) {
|
||||
if !f.heard(context.Background(), cleared) || !strings.Contains(logs.String(), "the floor of m/img is asked again: condition-cleared") {
|
||||
t.Fatalf("condition-cleared: log %q", logs.String())
|
||||
}
|
||||
if f.heard(context.Background(), &wakeMsg{subject: delivery.ArtifactsGoneSubject}) {
|
||||
if f.heard(context.Background(), &wakeMsg{subject: sweep.SubjectArtifactsGone}) {
|
||||
t.Fatal("artifacts-gone was taken as a floor's wake")
|
||||
}
|
||||
// DLV-B4: a registry seat's outcome on the same consumer moves a version; a published wakes a name whose seat was
|
||||
|
||||
@@ -9,8 +9,8 @@ import (
|
||||
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"mesh-delivery-builder/internal/build"
|
||||
)
|
||||
@@ -35,7 +35,7 @@ func storedVia(call toolCaller) func(ctx context.Context, references []string) (
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("asking held: %w", err)
|
||||
}
|
||||
a, err := delivery.DecodeHeldAnswer(raw, references)
|
||||
a, err := decodeHeld(raw, references)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -54,13 +54,62 @@ func storedVia(call toolCaller) func(ctx context.Context, references []string) (
|
||||
// heardGone takes one artifacts-gone event in: acknowledged, then handed to the builder, which asks the store and builds
|
||||
// the key again. A body that is not one is said and acknowledged, so it is not handed over again.
|
||||
func heardGone(ctx context.Context, b *build.Builder, m jetstream.Msg, logf func(string, ...any)) {
|
||||
e, err := delivery.DecodeArtifactsGone(m.Data())
|
||||
e, err := decodeGone(m.Data())
|
||||
if ackErr := m.Ack(); ackErr != nil {
|
||||
logf("acknowledging %s: %v", m.Subject(), ackErr)
|
||||
}
|
||||
if err != nil {
|
||||
logf("%s holds what is not %s, left: %v", m.Subject(), delivery.ArtifactsGoneEvent, err)
|
||||
logf("%s holds what is not %s, left: %v", m.Subject(), sweep.EventArtifactsGone, err)
|
||||
return
|
||||
}
|
||||
b.ArtifactsGone(ctx, e)
|
||||
}
|
||||
|
||||
// decodeHeld reads the seat's answer to held: the verb answers {output, ok, answer}, the answer being the seat's
|
||||
// HeldAnswer. A reference asked that the answer names in none of held, gone and unknown is an error: an answer that
|
||||
// leaves one out would let a key with a gone blob be reused.
|
||||
func decodeHeld(raw []byte, asked []string) (sweep.HeldAnswer, error) {
|
||||
var wrapped struct {
|
||||
Output string `json:"output"`
|
||||
OK bool `json:"ok"`
|
||||
Answer json.RawMessage `json:"answer"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &wrapped); err != nil {
|
||||
return sweep.HeldAnswer{}, fmt.Errorf("the answer of %s is not {output, ok, answer}: %w", SeatHeld, err)
|
||||
}
|
||||
if !wrapped.OK || len(wrapped.Answer) == 0 {
|
||||
return sweep.HeldAnswer{}, fmt.Errorf("%s did not answer: %s", SeatHeld, wrapped.Output)
|
||||
}
|
||||
var a sweep.HeldAnswer
|
||||
if err := json.Unmarshal(wrapped.Answer, &a); err != nil {
|
||||
return sweep.HeldAnswer{}, fmt.Errorf("the answer of %s is not {held, gone}: %w", SeatHeld, err)
|
||||
}
|
||||
named := map[string]bool{}
|
||||
for _, r := range a.Held {
|
||||
named[r] = true
|
||||
}
|
||||
for _, r := range a.Gone {
|
||||
named[r] = true
|
||||
}
|
||||
for r := range a.Unknown {
|
||||
named[r] = true
|
||||
}
|
||||
for _, r := range asked {
|
||||
if !named[r] {
|
||||
return sweep.HeldAnswer{}, fmt.Errorf("the answer of %s names nothing of %s", SeatHeld, r)
|
||||
}
|
||||
}
|
||||
return a, nil
|
||||
}
|
||||
|
||||
// decodeGone reads the event's body, refusing one that names no module, fingerprint or reference.
|
||||
func decodeGone(raw []byte) (sweep.ArtifactsGone, error) {
|
||||
var e sweep.ArtifactsGone
|
||||
if err := json.Unmarshal(raw, &e); err != nil {
|
||||
return sweep.ArtifactsGone{}, fmt.Errorf("%s is not one: %w", sweep.EventArtifactsGone, err)
|
||||
}
|
||||
if e.Event != sweep.EventArtifactsGone || e.Module == "" || e.Fingerprint == "" || len(e.Gone) == 0 {
|
||||
return sweep.ArtifactsGone{}, fmt.Errorf("%s names no module, fingerprint or gone reference: %s", sweep.EventArtifactsGone, raw)
|
||||
}
|
||||
return e, nil
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/ociartifactregistry"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/health"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
@@ -235,7 +236,7 @@ func emitEvent(ctx context.Context, js jetstream.JetStream, cred process.Account
|
||||
|
||||
// The events on the builder's consumer of EVENTS that wake a floor (design 51 *The floor*, DLV-B3b).
|
||||
var (
|
||||
manifestAcceptedSubject = "mesh.mod." + delivery.IndexerModule + ".event." + delivery.ManifestAcceptedEvent
|
||||
manifestAcceptedSubject = meshcatalog.SubjectManifestAccepted
|
||||
conditionClearedSubject = "mesh.mod." + health.KeeperModule + ".event." + health.EventCleared
|
||||
)
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ module mesh-delivery-builder
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
@@ -46,3 +47,7 @@ require (
|
||||
)
|
||||
|
||||
replace git.novox.be/novox/mesh-delivery/sdk => ../../sdk
|
||||
|
||||
require git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep v0.0.0-00010101000000-000000000000
|
||||
|
||||
replace git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep => ../../contracts/go/seats/meshartifactsweep
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -184,14 +185,14 @@ func TestArtifactsGoneMakesTheBuilderAskAndBuildAgain(t *testing.T) {
|
||||
held.mu.Lock()
|
||||
held.gone[goneRef] = true // collected after the builder's look; the controller's take-in found it
|
||||
held.mu.Unlock()
|
||||
r.b.ArtifactsGone(context.Background(), delivery.ArtifactsGone{Event: delivery.ArtifactsGoneEvent, Module: "m",
|
||||
r.b.ArtifactsGone(context.Background(), sweep.ArtifactsGone{Event: sweep.EventArtifactsGone, Module: "m",
|
||||
Fingerprint: "src2:" + strings.Repeat("9", 64), Gone: []string{goneRef}})
|
||||
r.b.Wait()
|
||||
if r.st.revs[r.key] != rev || !log.has("a fingerprint the module does not have now") {
|
||||
t.Fatalf("an event for another fingerprint moved the key (revision %d, was %d): %v", r.st.revs[r.key], rev, log.lines)
|
||||
}
|
||||
|
||||
r.b.ArtifactsGone(context.Background(), delivery.ArtifactsGone{Event: delivery.ArtifactsGoneEvent, Module: "m",
|
||||
r.b.ArtifactsGone(context.Background(), sweep.ArtifactsGone{Event: sweep.EventArtifactsGone, Module: "m",
|
||||
Key: r.key, Fingerprint: fp1, Gone: []string{goneRef}})
|
||||
r.b.Wait()
|
||||
v := valueOf(t, r.st, r.key)
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
sweep "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshartifactsweep"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/forms"
|
||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
@@ -281,7 +282,7 @@ func (b *Builder) storedOnce(key string) func(ctx context.Context, references []
|
||||
// that the store does not hold what the module's key names. The module is looked at again, which asks the store (ClaimOptions.Stored) and
|
||||
// builds the key again; an event for a fingerprint the module no longer has is said and left. The event only wakes the
|
||||
// builder: what is built rests on the store's answer, never on the event alone.
|
||||
func (b *Builder) ArtifactsGone(ctx context.Context, e delivery.ArtifactsGone) {
|
||||
func (b *Builder) ArtifactsGone(ctx context.Context, e sweep.ArtifactsGone) {
|
||||
b.init()
|
||||
b.mu.Lock()
|
||||
m, ok := b.known[e.Module]
|
||||
@@ -295,11 +296,11 @@ func (b *Builder) ArtifactsGone(ctx context.Context, e delivery.ArtifactsGone) {
|
||||
b.mu.Unlock()
|
||||
if !ok || m.FP2 != e.Fingerprint {
|
||||
b.logf("%s: heard %s for %s, a fingerprint the module does not have now: nothing to build", e.Module,
|
||||
delivery.ArtifactsGoneEvent, e.Fingerprint)
|
||||
sweep.EventArtifactsGone, e.Fingerprint)
|
||||
return
|
||||
}
|
||||
b.logf("%s: heard %s (%s): asking the artifact store and building the key again", e.Module,
|
||||
delivery.ArtifactsGoneEvent, strings.Join(e.Gone, ", "))
|
||||
sweep.EventArtifactsGone, strings.Join(e.Gone, ", "))
|
||||
b.consider(ctx, e.Module, m)
|
||||
}
|
||||
|
||||
|
||||
@@ -134,9 +134,9 @@ func TestManifestDeclarations(t *testing.T) { // 1607/AC1
|
||||
// DLV-M1: the indexer's and the assigner's views families of the states it follows.
|
||||
if want := []string{"container-image-registry.kept", "container-image-registry.published", "container-image-registry.refused", "container-image-registry.removed",
|
||||
"go-package-registry.kept", "go-package-registry.published", "go-package-registry.refused", "go-package-registry.removed",
|
||||
"mesh-artifact-sweep.artifacts-gone", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported",
|
||||
"mesh-artifact-sweep.artifacts-gone", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported", "mesh-catalog.manifest-accepted",
|
||||
"mesh-conditions.condition-cleared", "mesh-delivery-assigner.views.placement", "mesh-delivery-checker.tag-wanted", "mesh-delivery-forge-bridge.forge.**",
|
||||
"mesh-delivery-indexer.manifest-accepted", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.views.packages", "node-engine.manifest-reads-changed",
|
||||
"npm-package-registry.kept", "npm-package-registry.published", "npm-package-registry.refused", "npm-package-registry.removed",
|
||||
"oci-artifact-registry.kept", "oci-artifact-registry.published", "oci-artifact-registry.refused", "oci-artifact-registry.removed"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
|
||||
|
||||
@@ -39,10 +39,10 @@
|
||||
"mesh-artifact-sweep.artifacts-gone",
|
||||
"mesh-build-go.toolchain.reported",
|
||||
"mesh-build-node.toolchain.reported",
|
||||
"mesh-catalog.manifest-accepted",
|
||||
"mesh-conditions.condition-cleared",
|
||||
"mesh-delivery-checker.tag-wanted",
|
||||
"mesh-delivery-forge-bridge.forge.**",
|
||||
"mesh-delivery-indexer.manifest-accepted",
|
||||
"node-engine.manifest-reads-changed",
|
||||
"npm-package-registry.kept",
|
||||
"npm-package-registry.published",
|
||||
|
||||
@@ -15,8 +15,8 @@ import (
|
||||
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/composer"
|
||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/composer"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/gopackageregistry"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshdeliverychecker"
|
||||
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/npmpackageregistry"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
@@ -396,6 +395,13 @@ func startBed(t *testing.T) *bed {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The seat mesh-catalog's holder answers the seat list with no trunk seat named: the checker's reader has then heard the seat
|
||||
// and holds the bootstrapped definitions, which the judge is handed (ADR 0636 §1).
|
||||
for _, subject := range []string{"mesh.seat.mesh-catalog.call.seat-definitions", "mesh.seat.mesh-catalog.call.seat-definitions.>"} {
|
||||
if _, err := ctl.Subscribe(subject, func(m *nats.Msg) { _ = m.Respond([]byte(`{"result":{"revision":1,"seats":[]},"node":"ctl"}`)) }); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 90*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
b := &bed{ctx: ctx, js: js, buckets: map[string]jetstream.KeyValue{}, tools: map[string]jetstream.KeyValue{}, srvURL: srv.ClientURL()}
|
||||
@@ -425,8 +431,8 @@ func startBed(t *testing.T) *bed {
|
||||
meshdeliverychecker.HolderSubjectTagWanted(delivery.CheckerModule)}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The shared events stream with the package registry seats' published, and the checker's durable consumer of it.
|
||||
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: process.EventsStream, Subjects: []string{gopackageregistry.SubjectPublished, npmpackageregistry.SubjectPublished}}); err != nil {
|
||||
// The shared events stream with the seats' and modules' events (the package registry seats' published among them), and the checker's durable consumer of it.
|
||||
if _, err := js.CreateOrUpdateStream(ctx, jetstream.StreamConfig{Name: process.EventsStream, Subjects: []string{"mesh.mod.*.event.>", "mesh.seat.*.event.>"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := js.CreateOrUpdateConsumer(ctx, process.EventsStream, jetstream.ConsumerConfig{Durable: "n1_mesh-delivery-checker", AckPolicy: jetstream.AckExplicitPolicy}); err != nil {
|
||||
@@ -1057,11 +1063,6 @@ func TestATryIsJudgedAndItsEndWrittenWithNoStatusOwed(t *testing.T) {
|
||||
// the readable ones, which is what grants the forge module its read of owed (ADR 0495 §2-§3, ADR 0521; R4e finding R2-1).
|
||||
func TestTheCheckerSaysOwedReadableOnTheBus(t *testing.T) { // R2-1
|
||||
b := startBed(t)
|
||||
// The bed's shared events stream carries the registry seats' published; the module events go on it too.
|
||||
if _, err := b.js.UpdateStream(b.ctx, jetstream.StreamConfig{Name: process.EventsStream, Subjects: []string{"mesh.mod.*.event.>",
|
||||
gopackageregistry.SubjectPublished, npmpackageregistry.SubjectPublished}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.start(t, nil)
|
||||
events, err := b.js.Stream(b.ctx, "EVENTS")
|
||||
if err != nil {
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
@@ -28,6 +30,8 @@ type judgeDeps struct {
|
||||
asks check.Asks
|
||||
renames check.Renames
|
||||
seatDefs check.SeatDefinitions
|
||||
// seats are the seat definitions the manifests are judged against (seatlist.go).
|
||||
seats check.SeatList
|
||||
tools *buildrun.ToolchainSet
|
||||
trunks *check.Trunks
|
||||
kept check.KV
|
||||
@@ -152,7 +156,7 @@ func (n nodeNames) Names(ctx context.Context) ([]string, error) {
|
||||
}
|
||||
var out []string
|
||||
for key := range lister.Keys() {
|
||||
if node, _, isNode, err := delivery.ParseHaveKey(key); err == nil && isNode {
|
||||
if node, _, isNode, err := nodeengineclient.ParseHaveKey(key); err == nil && isNode {
|
||||
out = append(out, node)
|
||||
}
|
||||
}
|
||||
@@ -178,9 +182,9 @@ func (r runningServes) Served(ctx context.Context) (func(string) bool, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
haves := map[string]map[string]delivery.Have{}
|
||||
haves := map[string]map[string]nodeengine.HaveByNodeModuleValue{}
|
||||
for key := range lister.Keys() {
|
||||
node, module, isNode, err := delivery.ParseHaveKey(key)
|
||||
node, module, isNode, err := nodeengineclient.ParseHaveKey(key)
|
||||
if err != nil || isNode {
|
||||
continue
|
||||
}
|
||||
@@ -188,9 +192,9 @@ func (r runningServes) Served(ctx context.Context) (func(string) bool, error) {
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if h, err := delivery.DecodeHave(e.Value()); err == nil {
|
||||
if h, err := nodeengineclient.DecodeHave(e.Value()); err == nil {
|
||||
if haves[node] == nil {
|
||||
haves[node] = map[string]delivery.Have{}
|
||||
haves[node] = map[string]nodeengine.HaveByNodeModuleValue{}
|
||||
}
|
||||
haves[node][module] = h
|
||||
}
|
||||
@@ -220,7 +224,7 @@ func newJudge(d judgeDeps) check.Judge {
|
||||
nodes = d.nodes
|
||||
}
|
||||
ex := &check.Examiner{Nodes: nodes, Compositions: d.compositions, Asks: d.asks, Toolchains: d.tools,
|
||||
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, KnownPackages: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Renames: d.renames, SeatDefinitions: d.seatDefs, Logf: d.logf}
|
||||
Kept: check.Kept{Store: d.kept}, Previews: d.previews, Modules: d.repos, Packages: d.repos, Published: d.published, KnownPackages: d.published, Forge: d.forge, Runs: d.repos, Builds: d.builds, BuildEnds: d.ends, BuildWait: d.buildWait, Announce: d.announce, Trunks: d.trunks, Manifests: d.repos, Read: d.repos, Dirs: d.repos, Predefined: d.predefined, Tags: d.tags, Renames: d.renames, SeatDefinitions: d.seatDefs, SeatList: d.seats, Logf: d.logf}
|
||||
if d.nodes != nil && d.builds != nil && d.repos != nil {
|
||||
ex.Serving = runningServes{have: d.nodes, builds: d.builds, read: d.repos, logf: d.logf}
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"mesh-delivery-checker/internal/check"
|
||||
@@ -142,7 +143,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// first view of their read tools and then their views families, never their buckets (ADR 0615 §1, ADR 0596, DLV-M2).
|
||||
// An owner that is down never holds the start: the checks start once every first view is answered, and until then the
|
||||
// unanswered first view is the checker's finding.
|
||||
reader := &views.ProcessReader{Module: delivery.CheckerModule, Call: caller.Call, Logf: logger.Printf}
|
||||
// The module has one durable consumer of EVENTS: one hub pulls it for the views reader and for the registry seats'
|
||||
// published, started once both are registered.
|
||||
hub := process.NewEventHub(logger.Printf)
|
||||
reader := &views.ProcessReader{Module: delivery.CheckerModule, Call: caller.Call, Logf: logger.Printf, Hub: hub}
|
||||
seats := seatList{reader: followSeatList(seatlist.ProcessBus(caller.Call, hub), logger.Printf)}
|
||||
findings := process.AwaitBucket(ctx, js, conn, findingsBucket, "a first view unanswered past its bound is said in the log alone", logger.Printf)
|
||||
reader.Findings = func() views.FindingWriter { return sdkfindings.Writer(ctx, findings.KV) }
|
||||
// Each by the owner's read tool and views family as the owner's manifest names them.
|
||||
@@ -260,20 +265,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// not there yet a group's check asks and waits in vain until its bound, said in its verdict.
|
||||
tags := tagAsks{js: js, node: cred.Node, heard: &check.PublishedHeard{}, logf: logger.Printf, now: time.Now}
|
||||
ch.Says = tags // a pull request's verdict event, on the same stream asks (design 26 *Out of step*)
|
||||
switch cons, err := cred.EventConsumer(ctx, js, process.EventsStream); {
|
||||
case err != nil:
|
||||
logger.Printf("the consumer %s_%s on %s does not exist yet, so no package registry's published is heard and a group's check fails at its publish bound until the checker starts again: %v",
|
||||
cred.Node, cred.Module, process.EventsStream, err)
|
||||
default:
|
||||
cc, err := process.HearEvents(cons, func(m jetstream.Msg) {
|
||||
for subject := range publishedSeats {
|
||||
if err := hub.Handle(subject, func(m jetstream.Msg) {
|
||||
if !tags.heardPublished(m) {
|
||||
_ = m.Ack() // nothing else the checker consumes rides the shared stream
|
||||
}
|
||||
}, logger.Printf)
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
defer cc.Stop()
|
||||
}
|
||||
ch.Judge = newJudge(judgeDeps{
|
||||
asks: buildrun.Asker{Publisher: asks, Waiter: waiter, Bound: stepBound}, tools: tools, trunks: trunks,
|
||||
@@ -284,6 +283,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
predefined: keptPredefined{predefined},
|
||||
renames: seatRenames{call: caller.Call},
|
||||
seatDefs: seatDefinitionsOf(seatDefs),
|
||||
seats: seats,
|
||||
})
|
||||
ch.Announce = announce
|
||||
tryer.Judge = ch.Judge
|
||||
@@ -461,6 +461,12 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return nil
|
||||
}
|
||||
reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "checker_first_views reads nothing and checker_first_view_again is not heard", logger.Printf))
|
||||
// Every part has registered. Where the consumer is not there yet no package registry's published is heard and a group's
|
||||
// check fails at its publish bound until the checker starts again.
|
||||
if err := hub.Start(ctx, js, cred); err != nil {
|
||||
logger.Printf("%v, so no package registry's published is heard and a group's check fails at its publish bound until the checker starts again", err)
|
||||
}
|
||||
defer hub.Stop()
|
||||
// The checks, the tries and the hand asks, once every first view is answered (the lead's ruling for DLV-M2): a check
|
||||
// judged without the candidates, the modules, the manifests, the published packages, the predefined keys or the
|
||||
// placement would be another check.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"slices"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
)
|
||||
|
||||
// The checker reads the seat list as every part does (novox/hq ADR 0635 §1, ADR 0636): through the seat mesh-catalog's verb
|
||||
// seat-definitions, its two events heard as handlers of the module's event hub, from the bootstrapped definitions until the seat
|
||||
// has answered. What the reader holds is given to catalogue at every change, which the module graph is worked out with, and to the
|
||||
// indexer's judge as a request's Seats, which the manifests of a change are judged against.
|
||||
|
||||
// followSeatList starts the checker's reader of the seat list over the bus; the hub is started by the caller once every handler is registered.
|
||||
func followSeatList(bus seatlist.Bus, logf func(string, ...any)) *seatlist.Reader {
|
||||
reader, err := seatlist.Start(seatlist.Config{Bus: bus, Bootstrap: seatlist.Bootstrapped(),
|
||||
OnChange: func(seats []catalogue.Seat) { catalogue.UseSeats(seats) }})
|
||||
if err != nil {
|
||||
logf("the checker cannot follow its seat list, so no manifest is judged until it starts again: %v", err)
|
||||
}
|
||||
if reader != nil {
|
||||
catalogue.UseSeats(reader.Seats())
|
||||
}
|
||||
return reader
|
||||
}
|
||||
|
||||
// seatList is the reader as the check's SeatList: the held definitions raw, and an error while the seat has not answered.
|
||||
type seatList struct{ reader *seatlist.Reader }
|
||||
|
||||
func (s seatList) Definitions() ([]json.RawMessage, error) {
|
||||
if s.reader == nil {
|
||||
return nil, fmt.Errorf("the checker follows no seat list")
|
||||
}
|
||||
if st := s.reader.Status(); !st.Heard {
|
||||
return nil, fmt.Errorf("%s", st.Health())
|
||||
}
|
||||
defs := s.reader.Definitions()
|
||||
var out []json.RawMessage
|
||||
for _, seat := range slices.Sorted(maps.Keys(defs)) {
|
||||
raw, err := json.Marshal(defs[seat])
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("the definition of %s cannot be encoded: %w", seat, err)
|
||||
}
|
||||
out = append(out, raw)
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-sdk/go/composer"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/composer"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -205,10 +205,14 @@ func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node s
|
||||
}
|
||||
}
|
||||
}
|
||||
verdicts := map[string]delivery.ManifestVerdict{}
|
||||
verdicts := map[string]json.RawMessage{}
|
||||
if e.Verdicts != nil {
|
||||
for name, v := range e.Verdicts() {
|
||||
verdicts[name] = v
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return a, fmt.Errorf("the verdict of the manifest of %s cannot be encoded: %w", name, err)
|
||||
}
|
||||
verdicts[name] = raw
|
||||
}
|
||||
}
|
||||
for _, p := range c.Pulls {
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
|
||||
nec "git.novox.be/novox/mesh-contracts/go/clients/nodeengineclient"
|
||||
ne "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-sdk/go/composer"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/composer"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
|
||||
@@ -21,8 +21,8 @@ import (
|
||||
gobuild "git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshconfig"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
|
||||
"git.novox.be/novox/mesh-delivery/sdk/composer"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/composer"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
@@ -1054,7 +1054,13 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
|
||||
// trunk's, in a second ask (ADR 0633 clause 6, 1585/T22, T23)
|
||||
indexer string
|
||||
ownJudge bool
|
||||
// seats is the seat list the checker holds: "read" gives the judge its two definitions as the request's Seats, "unread"
|
||||
// is a reader the seat has not answered, and the manifests are then judged by nothing (ADR 0636 §1).
|
||||
seats string
|
||||
}{
|
||||
{name: "the seat list read is the request's Seats", seats: "read"},
|
||||
{name: "the seat list not read judges nothing", seats: "unread",
|
||||
fails: "the change's manifests cannot be judged: the seat list is not read (the seat list is held from the bootstrap"},
|
||||
{name: "a seat definition that does not parse", unparsed: "seats/web-api/seat.json: unexpected end of JSON input",
|
||||
fails: "novox/mesh-delivery#7: the seat definition of web-api (module web-contracts, go) does not parse: seats/web-api/seat.json: unexpected end of JSON input"},
|
||||
{name: "a rename brought, the verb not served", renames: "bring", seat: "not served",
|
||||
@@ -1162,6 +1168,9 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
|
||||
b.ex.Read = fixedManifests{strings.Repeat("a", 64): {Module: "mail"}, strings.Repeat("b", 64): {Module: "web"},
|
||||
web: {Module: "web", Claims: []catalogue.Claim{{Name: CatalogSeat}}}}
|
||||
}
|
||||
if row.seats != "" {
|
||||
b.ex.SeatList = fakeSeatList{read: row.seats == "read"}
|
||||
}
|
||||
var judged []delivery.ComposeJob
|
||||
answer := b.asks.Answer
|
||||
b.asks.Answer = func(a delivery.StepAsk) delivery.Step {
|
||||
@@ -1207,6 +1216,12 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
|
||||
}) {
|
||||
t.Errorf("%s: the case says %v, want the new indexer's manifest said accepted by the trunk's manifest validator", row.name, cs.notes)
|
||||
}
|
||||
if row.seats == "unread" {
|
||||
if v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, row.fails) || len(judged) != 0 {
|
||||
t.Errorf("%s: %+v, judged %d, want a fail saying %q before any judge", row.name, v.Failed, len(judged), row.fails)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if row.ownJudge && (len(judged) != 2 || judged[0].Fetch["commit"] != head1 || judged[1].Fetch["commit"] != trunk) {
|
||||
t.Fatalf("%s: judged %+v, want the head's judge and then the trunk's", row.name, judged)
|
||||
}
|
||||
@@ -1224,6 +1239,12 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
|
||||
req.Cases[0].Manifests["mail"].Digest != strings.Repeat("a", 64) {
|
||||
t.Fatalf("%s: the request %+v: %v", row.name, req, err)
|
||||
}
|
||||
if row.seats == "read" && (len(req.Seats) != 2 || string(req.Seats[0]) != `{"seat":"a"}` || string(req.Seats[1]) != `{"seat":"b"}`) {
|
||||
t.Errorf("%s: the request's seats %s, want the two definitions the reader holds", row.name, req.Seats)
|
||||
}
|
||||
if row.seats == "" && len(req.Seats) != 0 {
|
||||
t.Errorf("%s: the request's seats %s, want none from a checker that follows no list", row.name, req.Seats)
|
||||
}
|
||||
if !reflect.DeepEqual(req.Packages, row.known) || len(req.Input) != 0 && string(req.Input) != "null" {
|
||||
t.Errorf("%s: the judge's known packages %v and input %s, want the known packages %v and no input", row.name,
|
||||
req.Packages, req.Input, row.known)
|
||||
@@ -1242,6 +1263,16 @@ func TestTheIndexersJudgeJudgesTheChangesManifests(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// fakeSeatList is the seat list reader as a test gives it: two definitions once read, else the reader's words.
|
||||
type fakeSeatList struct{ read bool }
|
||||
|
||||
func (f fakeSeatList) Definitions() ([]json.RawMessage, error) {
|
||||
if !f.read {
|
||||
return nil, errors.New("the seat list is held from the bootstrap (2 seats) and nothing new is acted on")
|
||||
}
|
||||
return []json.RawMessage{json.RawMessage(`{"seat":"a"}`), json.RawMessage(`{"seat":"b"}`)}, nil
|
||||
}
|
||||
|
||||
// knownNames are the packages state's names as a test gives them, each with the <module>/<artifact> it is built from.
|
||||
type knownNames map[string]string
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/composer"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/composer"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/graph"
|
||||
|
||||
@@ -137,6 +137,9 @@ type Examiner struct {
|
||||
Manifests TrunkManifests
|
||||
// SeatDefinitions reads the indexer's seat definitions of a pull request (seatdefs.go, design 26); nil reads none.
|
||||
SeatDefinitions SeatDefinitions
|
||||
// SeatList gives the seat definitions the change's manifests are judged against, as the seat list reader holds them
|
||||
// (manifests_judge.go, novox/hq ADR 0636 §1); nil gives the judge none.
|
||||
SeatList SeatList
|
||||
// Renames asks the seat mesh-config's verb renames for a change that brings or drops a `renamed-from` (renames.go,
|
||||
// design 32); nil fails such a change, saying it cannot ask.
|
||||
Renames Renames
|
||||
|
||||
@@ -9,9 +9,9 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/composer"
|
||||
"git.novox.be/novox/mesh-sdk/go/buildstep"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/composer"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/sdk/packagename"
|
||||
@@ -63,6 +63,13 @@ type TrunkManifests interface {
|
||||
ManifestDigests() map[string]string
|
||||
}
|
||||
|
||||
// SeatList gives the seat definitions the manifests are judged against, whole, as raw definitions (a request's Seats): the
|
||||
// list the seat list reader holds. It is an error when the list has not been read from the seat yet, since a manifest judged
|
||||
// against the bootstrapped seats alone would be refused for a seat it names that the mesh defines (novox/hq ADR 0636 §1).
|
||||
type SeatList interface {
|
||||
Definitions() ([]json.RawMessage, error)
|
||||
}
|
||||
|
||||
// PredefinedList is the catalogue's list of predefined keys the indexer keeps (its state predefined, key list), the one a
|
||||
// config value falling back to a key is judged against (ADR 0348 §7); nil when it keeps none.
|
||||
type PredefinedList interface {
|
||||
@@ -124,6 +131,13 @@ func (st *state) judgeManifests(ctx context.Context) *delivery.Verdict {
|
||||
st.c.note("the manifests are judged against the predefined keys of " + from)
|
||||
}
|
||||
req.Packages = st.knownPackages(ctx)
|
||||
if st.j.SeatList != nil {
|
||||
seats, err := st.j.SeatList.Definitions()
|
||||
if err != nil {
|
||||
return fail(goModule, "the change's manifests cannot be judged: the seat list is not read ("+err.Error()+")")
|
||||
}
|
||||
req.Seats = seats
|
||||
}
|
||||
cv, trunkCase, v := st.runJudge(ctx, req, touched, commit, at)
|
||||
if v != nil {
|
||||
return v
|
||||
|
||||
@@ -124,7 +124,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
|
||||
"mesh-delivery-assigner.assigner_read_placement", "mesh-delivery-indexer.indexer_read_candidates", "mesh-delivery-indexer.indexer_read_manifests",
|
||||
"mesh-delivery-indexer.indexer_read_modules", "mesh-delivery-indexer.indexer_read_packages", "mesh-delivery-indexer.indexer_read_predefined",
|
||||
"mesh-delivery-indexer.indexer_read_seat_definitions",
|
||||
"seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
|
||||
"seat:mesh-catalog.seat-definitions", "seat:mesh-config.renames", "seat:mesh-hand-acts.hand-act", "seat:node-engine.compose-candidate"}; !reflect.DeepEqual(sorted(m.Invokes), want) {
|
||||
t.Errorf("invokes = %v, want the forge's reads and compose-candidate", m.Invokes)
|
||||
}
|
||||
if want := []string{"node-build-go", "node-build-node"}; !reflect.DeepEqual(sorted(m.Uses), want) {
|
||||
@@ -133,7 +133,7 @@ func TestManifestDeclarations(t *testing.T) { // 1678/AC1
|
||||
// The checker is woken by a build module's report of a toolchain, as the indexer is: the bucket may not exist when it starts.
|
||||
// And a group's check by the package registry seats' published of its tagged versions (design 51 *Tagged versions*).
|
||||
if want := []string{"go-package-registry.published", "mesh-build-go.toolchain.reported", "mesh-build-node.toolchain.reported",
|
||||
"mesh-delivery-assigner.views.placement", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
|
||||
"mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-delivery-assigner.views.placement", "mesh-delivery-indexer.views.candidates", "mesh-delivery-indexer.views.manifests", "mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.views.packages", "mesh-delivery-indexer.views.predefined", "mesh-delivery-indexer.views.seat-definitions", "npm-package-registry.published"}; !reflect.DeepEqual(sorted(m.Consumes), want) {
|
||||
t.Errorf("consumes = %v, want %v", m.Consumes, want)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
],
|
||||
"consumes": [
|
||||
"go-package-registry.published",
|
||||
"mesh-catalog.seat-definition-changed",
|
||||
"mesh-catalog.seat-definition-removed",
|
||||
"mesh-build-go.toolchain.reported",
|
||||
"mesh-build-node.toolchain.reported",
|
||||
"mesh-delivery-assigner.views.placement",
|
||||
@@ -81,6 +83,7 @@
|
||||
"mesh-delivery-indexer.indexer_read_packages",
|
||||
"mesh-delivery-indexer.indexer_read_predefined",
|
||||
"mesh-delivery-indexer.indexer_read_seat_definitions",
|
||||
"seat:mesh-catalog.seat-definitions",
|
||||
"seat:mesh-config.renames",
|
||||
"seat:mesh-hand-acts.hand-act",
|
||||
"seat:node-engine.compose-candidate"
|
||||
|
||||
@@ -195,23 +195,23 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
go onReconnect(ctx, again, said, logger)
|
||||
// What a merge read of each module directory, by its tree (issue 1126): a merge reads only the directories it changed.
|
||||
inputs := newKept(process.OptionalBucket(ctx, js, inputsBucket, "a merge's module directories are kept in memory only, so the first merge after a start reads every one", logger.Printf), logger)
|
||||
cons, err := cred.EventConsumer(ctx, js, eventsStream)
|
||||
if err != nil {
|
||||
// Essential: the forge's events are what the bridge carries.
|
||||
return fmt.Errorf("the consumer %s_%s on %s cannot be read (%v): the bridge hears no forge event without it, so it stops; where the bus refused it or did not answer, this module's consumes (made by the controller with its grant) have not reached the bus yet", cred.Node, cred.Module, eventsStream, err)
|
||||
}
|
||||
cc, err := process.HearEvents(cons, func(m jetstream.Msg) {
|
||||
// The module's one pull of its durable consumer on the shared events stream is its event hub; the bridge has the one
|
||||
// handler, for every event its consumes filter lets through. Essential: the forge's events are what the bridge carries.
|
||||
hub := process.NewEventHub(logger.Printf)
|
||||
if err := hub.Handle("mesh.mod.*.event.>", func(m jetstream.Msg) {
|
||||
hear(&turn, func() {
|
||||
// Bounded: the turn is held, and no sync or other event goes on without it.
|
||||
hctx, cancel := context.WithTimeout(ctx, syncDeadline)
|
||||
defer cancel()
|
||||
handleGrouped(hctx, m, rec, treesVia(hctx, call), filesVia(hctx, call), inputs, groups, logger)
|
||||
})
|
||||
}, logger.Printf)
|
||||
if err != nil {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
defer cc.Stop()
|
||||
if err := hub.Start(ctx, js, cred); err != nil {
|
||||
return fmt.Errorf("%w: the bridge hears no forge event without it, so it stops; where the bus refused it or did not answer, this module's consumes (made by the controller with its grant) have not reached the bus yet", err)
|
||||
}
|
||||
defer hub.Stop()
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
|
||||
@@ -534,13 +534,13 @@ func startIndexerRun(t *testing.T, put func(context.Context, buildLog), before .
|
||||
t.Fatal(err)
|
||||
}
|
||||
acceptedHeard.Store(0)
|
||||
waiter, err := ctl.Subscribe("mesh.mod.mesh-delivery-indexer.event.manifest-accepted", func(*nats.Msg) { acceptedHeard.Add(1) })
|
||||
waiter, err := ctl.Subscribe("mesh.seat.mesh-catalog.event.manifest-accepted", func(*nats.Msg) { acceptedHeard.Add(1) })
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(func() { _ = waiter.Unsubscribe() })
|
||||
// The shared events stream: the indexer's ordinary events and the assigner's views events (ADR 0635 §5).
|
||||
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "EVENTS", Subjects: []string{"mesh.mod.*.event.>"}}); err != nil {
|
||||
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "EVENTS", Subjects: []string{"mesh.mod.*.event.>", "mesh.seat.*.event.>"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
put(ctx, buildLog{ctx: ctx, js: js})
|
||||
@@ -733,7 +733,7 @@ func TestIndexerKeepsManifestsAtAMerge(t *testing.T) { // 1444/AC4, 1449/AC4, 15
|
||||
break
|
||||
}
|
||||
var r delivery.ManifestRefused
|
||||
if msg.Subject == "mesh.mod.mesh-delivery-indexer.event."+delivery.ManifestRefusedEvent && json.Unmarshal(msg.Data, &r) == nil {
|
||||
if msg.Subject == "mesh.seat.mesh-catalog.event."+delivery.ManifestRefusedEvent && json.Unmarshal(msg.Data, &r) == nil {
|
||||
said[r.Module] = true
|
||||
}
|
||||
}
|
||||
|
||||
+8
-7
@@ -78,14 +78,10 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
|
||||
}
|
||||
}
|
||||
serveForge(t, js.Conn(), listing, goSumContent)
|
||||
// The seat definitions' state and stream (DLV-I17), as the grants part makes them from the manifest.
|
||||
// The seat definitions' state (DLV-I17), as the grants part makes it from the manifest.
|
||||
if seatDefs, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: index.SeatDefinitionsBucket, History: 5}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := js.CreateStream(ctx, jetstream.StreamConfig{Name: "SEAT_MESH_CATALOG",
|
||||
Subjects: []string{"mesh.seat." + index.CatalogSeat + ".event.>"}}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// DLV-I19, I20: the indexer's findings and takeins states, and the checker's stream asks with the indexer's durable
|
||||
// consumer, which carries the checker's verdict event.
|
||||
if findings, err = js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: findingsBucket, History: 1}); err != nil {
|
||||
@@ -263,12 +259,17 @@ func TestAMeshSDKMergeMovesOnlyTheModulesImportingAChangedPackage(t *testing.T)
|
||||
t.Fatalf("%s is kept 20 s after its pull request closed", pk)
|
||||
}
|
||||
}
|
||||
info, err := js.Stream(ctx, "SEAT_MESH_CATALOG")
|
||||
// The seat's events are on the shared events stream like every seat's.
|
||||
info, err := js.Stream(ctx, "EVENTS")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
full, err := info.Info(ctx, jetstream.WithSubjectFilter("mesh.seat."+index.CatalogSeat+".event.seat-definition-changed"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Said on the seat's subject: the trunk's git alone; a pull request's head (unparsed, then closed) says nothing (ADR 0636 §4).
|
||||
if n := info.CachedInfo().State.Msgs; n != 1 {
|
||||
if n := full.State.Subjects["mesh.seat."+index.CatalogSeat+".event.seat-definition-changed"]; n != 1 {
|
||||
t.Errorf("seat-definition-changed said %d times, want 1", n)
|
||||
}
|
||||
if m, err := info.GetLastMsgForSubject(ctx, "mesh.seat.mesh-catalog.event.seat-definition-changed"); err != nil ||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
)
|
||||
|
||||
// sayRefused says the indexer's event manifest-refused (novox/hq ADR 0578 §1): mesh.mod.<module>.event.manifest-refused,
|
||||
// sayRefused says the indexer's event manifest-refused (novox/hq ADR 0578 §1): mesh.seat.mesh-catalog.event.manifest-refused,
|
||||
// with the SDK's envelope headers and the digest and judge as the bus's dedup id, so one refusal is said once.
|
||||
func sayRefused(js jetstream.JetStream, cred process.Account, logf func(string, ...any)) func(context.Context, delivery.ManifestRefused) {
|
||||
return func(ctx context.Context, r delivery.ManifestRefused) {
|
||||
@@ -22,7 +22,7 @@ func sayRefused(js jetstream.JetStream, cred process.Account, logf func(string,
|
||||
logf("judge: %s for %s is not said: %v", delivery.ManifestRefusedEvent, r.Module, err)
|
||||
return
|
||||
}
|
||||
msg := nats.NewMsg(cred.EventSubject(delivery.ManifestRefusedEvent))
|
||||
msg := nats.NewMsg(seatEventSubject(delivery.ManifestRefusedEvent))
|
||||
id := "manifest-refused-" + r.Digest + "-" + r.Judge
|
||||
msg.Header.Set("x-event-id", id)
|
||||
msg.Header.Set("x-source", cred.Module)
|
||||
@@ -39,7 +39,7 @@ func sayRefused(js jetstream.JetStream, cred process.Account, logf func(string,
|
||||
}
|
||||
|
||||
// sayAccepted says the indexer's event manifest-accepted (issue 1966 D3, novox/hq ADR 0595 §4):
|
||||
// mesh.mod.<module>.event.manifest-accepted, carrying the module's bus-state entries whole, with the digest and judge as
|
||||
// mesh.seat.mesh-catalog.event.manifest-accepted, carrying the module's bus-state entries whole, with the digest and judge as
|
||||
// the bus's dedup id (delivery.ManifestAccepted.EventID), so one acceptance is said once.
|
||||
func sayAccepted(js jetstream.JetStream, cred process.Account) func(context.Context, delivery.ManifestAccepted, string) error {
|
||||
return func(ctx context.Context, a delivery.ManifestAccepted, id string) error {
|
||||
@@ -47,7 +47,7 @@ func sayAccepted(js jetstream.JetStream, cred process.Account) func(context.Cont
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
msg := nats.NewMsg(cred.EventSubject(delivery.ManifestAcceptedEvent))
|
||||
msg := nats.NewMsg(seatEventSubject(delivery.ManifestAcceptedEvent))
|
||||
msg.Header.Set("x-event-id", id)
|
||||
msg.Header.Set("x-source", cred.Module)
|
||||
msg.Header.Set("x-node", cred.Node)
|
||||
|
||||
@@ -3,6 +3,7 @@ module mesh-delivery-indexer
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-catalog/modules/mesh-build-go/buildstep v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
@@ -4355,7 +4355,7 @@ func TestTheBussAccountSaysNothingOnTheBus(t *testing.T) {
|
||||
keeper := func(bus string, more string) []byte {
|
||||
return []byte(`{"module":"nats","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}],
|
||||
"claims":[{"name":"mesh-broker","scope":"mesh","serves":["list"]}],"own-secrets":{"broker":"/run/broker"},
|
||||
"tools":["nats_server"],"consumes":["mesh-delivery-indexer.manifest-accepted"],"uses":["operator-channel"],
|
||||
"tools":["nats_server"],"consumes":["mesh-catalog.manifest-accepted"],"uses":["operator-channel"],
|
||||
"state":[{"name":"requests","history":5}],"streams":[{"name":"broker-events","events":["created"]}],
|
||||
"emits":["consumer-removed"],
|
||||
"resources":[{"id":"nats-keeper","type":"user","name":"nats-keeper"},
|
||||
|
||||
@@ -109,9 +109,9 @@ func TestManifestDeclarations(t *testing.T) { // 1238/AC1, 1444 (reads the build
|
||||
}
|
||||
// 1126: placement is the seat mesh-placement's (ADR 0567): the indexer holds no placement state. ADR 0578 §1: it says
|
||||
// a refused manifest as manifest-refused, and (issue 1966 D3, 1966/AC10) a newly accepted one asking for bus state as
|
||||
// manifest-accepted.
|
||||
if own := slices.DeleteFunc(slices.Clone(m.Emits), func(e string) bool { return strings.HasPrefix(e, views.EventPrefix) }); !reflect.DeepEqual(own, []string{"manifest-refused", "manifest-accepted", takein.TakeInPendingEvent, takein.TakeInTakenEvent}) {
|
||||
t.Errorf("emits = %v, want manifest-refused, manifest-accepted and the two take-in events", m.Emits)
|
||||
// manifest-accepted, both events of the seat mesh-catalog it holds (ADR 0636 §4), so not among its `emits`.
|
||||
if own := slices.DeleteFunc(slices.Clone(m.Emits), func(e string) bool { return strings.HasPrefix(e, views.EventPrefix) }); !reflect.DeepEqual(own, []string{takein.TakeInPendingEvent, takein.TakeInTakenEvent}) {
|
||||
t.Errorf("emits = %v, want the two take-in events (the verdict events are the seat mesh-catalog's)", m.Emits)
|
||||
}
|
||||
// 1126 note 76: heard keeps the last 64 FORGE messages the indexer handled, for indexer_heard.
|
||||
if want := []state{{Name: "candidates", History: 10}, {Name: "heard", History: 64}, {Name: "manifests", History: 1}, {Name: "modules", History: 20}, {Name: "predefined", History: 5}, {Name: "config-candidates", History: 5}, {Name: "taken", History: 1}, {Name: "takeins", History: 1}, {Name: "verdicts", History: 5}, {Name: "oldsections", History: 1}, {Name: delivery.PackagesState, History: delivery.PackagesHistory},
|
||||
|
||||
@@ -89,8 +89,6 @@
|
||||
{ "name": "pending", "history": 1 }
|
||||
],
|
||||
"emits": [
|
||||
"manifest-refused",
|
||||
"manifest-accepted",
|
||||
"take-in.pending",
|
||||
"take-in.taken",
|
||||
"views.candidates",
|
||||
|
||||
@@ -10,7 +10,8 @@ import (
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
)
|
||||
|
||||
// The assigner's asks reach the operator (issue 1906). The assigner writes its asks (the hold of a fingerprint, the restore
|
||||
@@ -89,8 +90,8 @@ func (t *Teller) waitForAssignerRoom(key, id string) {
|
||||
|
||||
// isAssignerKey says whether an ask key is one of the assigner's, not the gate's merge ask.
|
||||
func isAssignerKey(key string) bool {
|
||||
k, err := delivery.ParseAnyAskKey(key)
|
||||
return err == nil && k.Kind != delivery.AskKindMerge
|
||||
k, err := asks.ParseAnyAskKey(key)
|
||||
return err == nil && k.Kind != meshdeliveryclient.AskKindMerge
|
||||
}
|
||||
|
||||
// assignerAsk reads the assigner's ask at key: found false when the key holds none.
|
||||
@@ -115,14 +116,14 @@ type assignerAsked struct {
|
||||
Ask oc.Ask
|
||||
}
|
||||
|
||||
// decodeAssignerAsk reads the assigner's ask at key as delivery.DecodeAssignerAsk did before the ask was JSON: it refuses an
|
||||
// decodeAssignerAsk reads the assigner's ask at key as asks.DecodeAsk did before the ask was JSON: it refuses an
|
||||
// id other than the one the key and the asking give, and an ask with no option (occ.Carried).
|
||||
func decodeAssignerAsk(key string, data []byte) (assignerAsked, error) {
|
||||
a, err := delivery.DecodeAssignerAsk(key, data)
|
||||
a, err := asks.DecodeAsk(key, data)
|
||||
if err != nil {
|
||||
return assignerAsked{}, err
|
||||
}
|
||||
ask, err := occ.Carried(a.Ask, delivery.AskingID(key, a.Asking))
|
||||
ask, err := occ.Carried(a.Ask, meshdeliveryclient.AskingID(key, a.Asking))
|
||||
if err != nil {
|
||||
return assignerAsked{}, err
|
||||
}
|
||||
@@ -142,7 +143,7 @@ func (t *Teller) raiseAssignerAsk(key string) {
|
||||
return
|
||||
}
|
||||
id := a.Ask.ID
|
||||
if raw, _, has, err := t.Answers.Get(delivery.AnswerKey(key)); err != nil {
|
||||
if raw, _, has, err := t.Answers.Get(answerKey(key)); err != nil {
|
||||
t.logf("%s: the answers could not be read, so the ask is not raised: %v", key, err)
|
||||
return
|
||||
} else if has && answeredAsking(raw) == id {
|
||||
@@ -227,7 +228,7 @@ func (t *Teller) answerAssigner(key string, o oc.Option, w oc.Warrant) error {
|
||||
if occ.Digest(a.Ask) != w.AskDigest {
|
||||
return fmt.Errorf("the assigner's ask %s is no longer the ask the operator answered (its digest is %s, the warrant's %s)", key, occ.Digest(a.Ask), w.AskDigest)
|
||||
}
|
||||
body, err := delivery.EncodeAnswer(delivery.Answer{Option: o.ID, Warrant: warrantID(w), Words: occ.Says(w)})
|
||||
body, err := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: o.ID, Warrant: warrantID(w), Words: occ.Says(w)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -7,7 +7,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
)
|
||||
|
||||
// 1906/H1: given the assigner's hold ask in its bucket and no answer, when the teller hears the key, then it raises the ask on
|
||||
@@ -19,7 +20,7 @@ func TestAnAssignerAskIsRaisedOnTheOperatorChannelOnce(t *testing.T) {
|
||||
assigner[key] = value
|
||||
b.t.OnAssignerAsk(key)
|
||||
b.t.OnAssignerAsk(key)
|
||||
asked := b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))
|
||||
asked := b.askedOf(oc.AcceptSubjectAsk(tellerModule))
|
||||
if len(asked) != 1 {
|
||||
t.Fatalf("published %d asks, want one; log:\n%s", len(asked), b.logs.String())
|
||||
}
|
||||
@@ -40,7 +41,7 @@ func TestAnAssignerAskIsRaisedWithoutTheView(t *testing.T) {
|
||||
key, value, _ := holdAsk(t)
|
||||
assigner[key] = value
|
||||
b.t.OnAssignerAsk(key)
|
||||
if n := len(b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))); n != 1 {
|
||||
if n := len(b.askedOf(oc.AcceptSubjectAsk(tellerModule))); n != 1 {
|
||||
t.Fatalf("published %d asks, want one; log:\n%s", n, b.logs.String())
|
||||
}
|
||||
}
|
||||
@@ -55,11 +56,11 @@ func TestTheOperatorsAnswerToAnAssignerAskIsWrittenUnderItsKey(t *testing.T) {
|
||||
b.t.OnAssignerAsk(key)
|
||||
w := ackFor(ask, "release")
|
||||
b.deliver(t, w)
|
||||
raw, ok := b.answers.written[delivery.AnswerKey(key)]
|
||||
raw, ok := b.answers.written[answerKey(key)]
|
||||
if !ok {
|
||||
t.Fatalf("no answer under %s; log:\n%s", key, b.logs.String())
|
||||
}
|
||||
if a, err := delivery.DecodeAnswer(raw); err != nil || a.Option != "release" || answeredAsking(raw) != ask.ID {
|
||||
if a, err := meshdeliveryclient.DecodeAnswer(raw); err != nil || a.Option != "release" || answeredAsking(raw) != ask.ID {
|
||||
t.Errorf("answer = %+v, %v", a, err)
|
||||
}
|
||||
b.deliver(t, w)
|
||||
@@ -86,7 +87,7 @@ func TestAnAnswerToAnAssignerAskTakenAwayWritesNothing(t *testing.T) {
|
||||
// itself) is taken back too; an ask answered before it is heard is never raised.
|
||||
func TestAnAssignerAskAnsweredInTheViewOrTakenAwayIsTakenBack(t *testing.T) {
|
||||
cancelled := func(b *bed, id string) bool {
|
||||
for _, p := range b.askedOf(oc.AcceptSubjectCancel(delivery.TellerModule)) {
|
||||
for _, p := range b.askedOf(oc.AcceptSubjectCancel(tellerModule)) {
|
||||
if string(p.body) == `{"id":"`+id+`"}` {
|
||||
return true
|
||||
}
|
||||
@@ -119,7 +120,7 @@ func TestAnAssignerAskAnsweredInTheViewOrTakenAwayIsTakenBack(t *testing.T) {
|
||||
assigner[key] = value
|
||||
view[key+".view"] = viewAnswer(t, "keep", "")
|
||||
b.t.OnAssignerAsk(key)
|
||||
if n := len(b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))); n != 0 {
|
||||
if n := len(b.askedOf(oc.AcceptSubjectAsk(tellerModule))); n != 0 {
|
||||
t.Errorf("an ask answered in the view was raised %d times", n)
|
||||
}
|
||||
}
|
||||
@@ -162,13 +163,13 @@ func ackFor(a oc.Ask, option string) oc.Warrant {
|
||||
// holdAskOf is the assigner's hold ask of module plex at the n-th fingerprint, shaped as holdAsk.
|
||||
func holdAskOf(t *testing.T, n int) (string, []byte, oc.Ask) {
|
||||
t.Helper()
|
||||
key, err := delivery.HoldAskKey("plex", "src2:"+strings.Repeat(string(rune('0'+n)), 64))
|
||||
key, err := asks.HoldAskKey("plex", "src2:"+strings.Repeat(string(rune('0'+n)), 64))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_, _, ask := holdAsk(t)
|
||||
ask.ID = delivery.AskID(key)
|
||||
value, err := delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: rawAsk(t, ask)})
|
||||
ask.ID = meshdeliveryclient.AskID(key)
|
||||
value, err := asks.EncodeAsk(key, asks.Ask{Ask: rawAsk(t, ask)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"errors"
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/nats-io/nats.go"
|
||||
@@ -65,14 +66,16 @@ func recordVia(ctx context.Context, conn *nats.Conn, module string) func(id stri
|
||||
}
|
||||
}
|
||||
|
||||
// decidedVia hears the warrants the router says to this module alone: its own `decided` event, a subject only the router
|
||||
// publishes (the grant to subscribe it names the module). They are pulled through the module's durable consumer on EVENTS
|
||||
// (made with its grant for the seat it uses), so an answer the router published while the teller was down is delivered
|
||||
// when it is back, never lost (issue 1126). hear starts the pull, handing each message to its handler; the message is
|
||||
// acknowledged once handled. The handler is the asker's: pattern is the key it expects.
|
||||
func decidedVia(hear func(func(jetstream.Msg)) error, logf func(string, ...any)) func(pattern string, handle func(stdio.Envelope) error) error {
|
||||
// decidedVia hears the warrants the router says to this module alone: its own `decided` event, a seat event on the seat's
|
||||
// subject (`mesh.seat.<seat>.event.<event>.<module>`) that only the router publishes (the grant to subscribe it names the
|
||||
// module). They are handled on the module's one hub over its durable consumer on EVENTS (made with its grant for the seat
|
||||
// it uses), so an answer the router published while the teller was down is delivered when it is back, never lost (issue
|
||||
// 1126). hear registers the handler of the subject on the hub; the message is acknowledged once handled. The handler is the
|
||||
// asker's: pattern, `<seat>.<event>.<module>`, is the key it expects.
|
||||
func decidedVia(hear func(subject string, handle func(jetstream.Msg)) error, logf func(string, ...any)) func(pattern string, handle func(stdio.Envelope) error) error {
|
||||
return func(pattern string, handle func(stdio.Envelope) error) error {
|
||||
return hear(func(m jetstream.Msg) {
|
||||
seat, event, _ := strings.Cut(pattern, ".")
|
||||
return hear("mesh.seat."+seat+".event."+event, func(m jetstream.Msg) {
|
||||
if err := handle(stdio.Envelope{Key: pattern, Body: m.Data()}); err != nil {
|
||||
logf("a warrant could not be handled and is read again from the router's record at the next start: %v", err)
|
||||
}
|
||||
@@ -83,26 +86,6 @@ func decidedVia(hear func(func(jetstream.Msg)) error, logf func(string, ...any))
|
||||
}
|
||||
}
|
||||
|
||||
// decidedHandler is where the router's warrants are handed: the module has one durable consumer of the shared events stream,
|
||||
// pulled by the views reader (views.ProcessReader.Start), which hands every message that is not a views event to its Other.
|
||||
type decidedHandler struct{ handle func(jetstream.Msg) }
|
||||
|
||||
// hear registers the handler of the warrants; it pulls nothing.
|
||||
func (d *decidedHandler) hear(handle func(jetstream.Msg)) error {
|
||||
d.handle = handle
|
||||
return nil
|
||||
}
|
||||
|
||||
// other is the views reader's Other: a message of the consumer that is no views event is a warrant (or nothing the teller
|
||||
// hears: it is left for the bus to deliver again).
|
||||
func (d *decidedHandler) other(m jetstream.Msg) {
|
||||
if d.handle == nil {
|
||||
_ = m.Nak()
|
||||
return
|
||||
}
|
||||
d.handle(m)
|
||||
}
|
||||
|
||||
// sayVia publishes a condition on the seat's one subject for conditions, under the module's own name.
|
||||
func sayVia(ctx context.Context, js jetstream.JetStream, module string) func(c oc.Condition) error {
|
||||
return func(c oc.Condition) error {
|
||||
|
||||
@@ -23,12 +23,13 @@ func (m pulled) Ack() error { *m.acked++; return nil }
|
||||
// the handler hears the warrant under the asker's key, and the message is acknowledged, handled or not (issue 1126).
|
||||
func TestDecidedViaHearsThePulledWarrantAndAcknowledgesIt(t *testing.T) {
|
||||
var deliver func(jetstream.Msg)
|
||||
var subject string
|
||||
var logs []string
|
||||
sub := decidedVia(func(h func(jetstream.Msg)) error { deliver = h; return nil },
|
||||
sub := decidedVia(func(s string, h func(jetstream.Msg)) error { subject, deliver = s, h; return nil },
|
||||
func(f string, _ ...any) { logs = append(logs, f) })
|
||||
var got []stdio.Envelope
|
||||
fail := false
|
||||
if err := sub("k", func(e stdio.Envelope) error {
|
||||
if err := sub("operator-channel.decided.teller", func(e stdio.Envelope) error {
|
||||
got = append(got, e)
|
||||
if fail {
|
||||
return errors.New("no")
|
||||
@@ -37,18 +38,21 @@ func TestDecidedViaHearsThePulledWarrantAndAcknowledgesIt(t *testing.T) {
|
||||
}); err != nil || deliver == nil {
|
||||
t.Fatalf("subscribe: %v, delivering %v", err, deliver != nil)
|
||||
}
|
||||
if subject != "mesh.seat.operator-channel.event.decided.teller" {
|
||||
t.Errorf("the warrants are heard on %q", subject)
|
||||
}
|
||||
acked := 0
|
||||
deliver(pulled{data: []byte(`{"ask":"a1"}`), acked: &acked})
|
||||
fail = true
|
||||
deliver(pulled{data: []byte(`{"ask":"a2"}`), acked: &acked})
|
||||
if len(got) != 2 || got[0].Key != "k" || string(got[0].Body) != `{"ask":"a1"}` || string(got[1].Body) != `{"ask":"a2"}` {
|
||||
if len(got) != 2 || got[0].Key != "operator-channel.decided.teller" || string(got[0].Body) != `{"ask":"a1"}` || string(got[1].Body) != `{"ask":"a2"}` {
|
||||
t.Errorf("heard %+v", got)
|
||||
}
|
||||
if acked != 2 || len(logs) != 1 {
|
||||
t.Errorf("acknowledged %d of 2, logged %d lines, want 1 (the failed handling)", acked, len(logs))
|
||||
}
|
||||
failing := decidedVia(func(func(jetstream.Msg)) error { return errors.New("no consumer") }, nil)
|
||||
if err := failing("k", nil); err == nil {
|
||||
failing := decidedVia(func(string, func(jetstream.Msg)) error { return errors.New("no consumer") }, nil)
|
||||
if err := failing("operator-channel.decided.teller", nil); err == nil {
|
||||
t.Error("a pull that could not start is no error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,8 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
|
||||
"mesh-delivery-teller/internal/ends"
|
||||
@@ -189,7 +190,7 @@ func fullConf(dir string) string {
|
||||
teller = append(teller, readOf(viewAnswers)...) // issue 1297 B10: the manifest's reads
|
||||
// The assigner's asks by its read tool and its views family (ADR 0615, DLV-M3), and the teller's own first-views and
|
||||
// findings; the bucket itself is no longer the teller's to read.
|
||||
teller = append(append(append(teller, viewstest.Grants(delivery.AssignerModule, delivery.AssignerModule+".assigner_read_asks", tellerConsumer)...), keepsOf(firstViewsBucket)...), keepsOf(findingsBucket)...)
|
||||
teller = append(append(append(teller, viewstest.Grants(assignerModule, assignerModule+".assigner_read_asks", tellerConsumer)...), keepsOf(firstViewsBucket)...), keepsOf(findingsBucket)...)
|
||||
view := append(append(append(readOf(tellerAnswers), readOf(gateAsksKV)...), readOf(assignerAsks)...), keepsOf(viewAnswers)...)
|
||||
assigner := keepsOf(assignerAsks)
|
||||
tools := append(keepsOf(gateAsksKV), readOf(tellerAnswers)...)
|
||||
@@ -256,7 +257,7 @@ func startFull(t *testing.T) *fullBed {
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
|
||||
t.Cleanup(cancel)
|
||||
for name, history := range map[string]uint8{gateAsksKV: delivery.AsksHistory, tellerAnswers: delivery.AnswersHistory, tellerAsked: 1, ends.Bucket: 1, messengerAsks: 1, assignerAsks: delivery.AssignerAsksHistory, viewAnswers: delivery.ViewAnswersHistory, firstViewsBucket: 1, findingsBucket: 1} {
|
||||
for name, history := range map[string]uint8{gateAsksKV: gateAsksHistory, tellerAnswers: answersHistory, tellerAsked: 1, ends.Bucket: 1, messengerAsks: 1, assignerAsks: assignerAsksHistory, viewAnswers: viewAnswersHistory, firstViewsBucket: 1, findingsBucket: 1} {
|
||||
if _, err := js.CreateKeyValue(ctx, jetstream.KeyValueConfig{Bucket: name, History: history}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -273,7 +274,7 @@ func startFull(t *testing.T) *fullBed {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// The assigner as the owner of its asks: its read tool and its views family (ADR 0615).
|
||||
viewstest.Serve(t, ctx, js, ctl, delivery.AssignerModule, "asks", delivery.AssignerModule+".assigner_read_asks", tellerConsumer)
|
||||
viewstest.Serve(t, ctx, js, ctl, assignerModule, "asks", assignerModule+".assigner_read_asks", tellerConsumer)
|
||||
return &fullBed{srv: srv, ctl: ctl, js: js, ctx: ctx}
|
||||
}
|
||||
|
||||
@@ -423,18 +424,18 @@ func TestTellerCarriesAnAskAndWritesTheAnswer(t *testing.T) { // 1343/AC1, AC2
|
||||
}
|
||||
var e jetstream.KeyValueEntry
|
||||
for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
|
||||
if e, err = answers.Get(b.ctx, delivery.AnswerKey(key)); err == nil {
|
||||
if e, err = answers.Get(b.ctx, answerKey(key)); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if e == nil {
|
||||
t.Fatalf("no answer under %s: %v\n%s", key, err, logs.String())
|
||||
}
|
||||
if a, err := delivery.DecodeAnswer(e.Value()); err != nil || a.Option != "approve" {
|
||||
if a, err := meshdeliveryclient.DecodeAnswer(e.Value()); err != nil || a.Option != "approve" {
|
||||
t.Errorf("answer %+v err %v", a, err)
|
||||
}
|
||||
// A second ask, withdrawn by the gate, is cancelled at the router.
|
||||
other := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 13, Head: headB}}
|
||||
other := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 13, Head: headB}}
|
||||
key2, value2, ga2 := gateAsk(t, other, other)
|
||||
if _, err := kv.Put(b.ctx, key2, value2); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -512,20 +513,20 @@ func TestTellerAsksAgainAnAskRefusedForWantOfRoom(t *testing.T) { // 1610/AC1
|
||||
t.Fatal(err)
|
||||
}
|
||||
first := lastAsk(held.Ask.ID, 0)
|
||||
say(oc.Warrant{Ask: held.Ask.ID, Asker: delivery.TellerModule, Outcome: occ.OutcomeRefused, Words: roomRefusal, At: occ.FormatTime(now)})
|
||||
say(oc.Warrant{Ask: held.Ask.ID, Asker: tellerModule, Outcome: occ.OutcomeRefused, Words: roomRefusal, At: occ.FormatTime(now)})
|
||||
for deadline := time.Now().Add(10 * time.Second); !strings.Contains(logs.String(), "for want of room"); time.Sleep(50 * time.Millisecond) {
|
||||
if time.Now().After(deadline) {
|
||||
t.Fatalf("the refusal was not heard\n%s", logs.String())
|
||||
}
|
||||
}
|
||||
other := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 13, Head: headB}}
|
||||
other := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 13, Head: headB}}
|
||||
key2, value2, ga2 := gateAsk(t, other, other)
|
||||
if _, err := kv.Put(b.ctx, key2, value2); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
second := lastAsk(ga2.Ask.ID, first)
|
||||
// The other ask lapses at the router: there is room, and the refused ask is asked again.
|
||||
say(oc.Warrant{Ask: ga2.Ask.ID, Asker: delivery.TellerModule, Outcome: occ.OutcomeExpired, At: occ.FormatTime(now), AskDigest: occ.Digest(ga2.Ask)})
|
||||
say(oc.Warrant{Ask: ga2.Ask.ID, Asker: tellerModule, Outcome: occ.OutcomeExpired, At: occ.FormatTime(now), AskDigest: occ.Digest(ga2.Ask)})
|
||||
lastAsk(held.Ask.ID, second)
|
||||
}
|
||||
|
||||
@@ -569,14 +570,14 @@ func TestTellerFillsAnAskFromTheViewsAnswerOnce(t *testing.T) { // 1297/B10 AC
|
||||
}
|
||||
var e jetstream.KeyValueEntry
|
||||
for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
|
||||
if e, err = answers.Get(b.ctx, delivery.AnswerKey(key)); err == nil {
|
||||
if e, err = answers.Get(b.ctx, answerKey(key)); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if e == nil {
|
||||
t.Fatalf("no answer under %s: %v\n%s", key, err, logs.String())
|
||||
}
|
||||
if a, err := delivery.DecodeAnswer(e.Value()); err != nil || a.Option != "release" || answeredAsking(e.Value()) != ask.ID {
|
||||
if a, err := meshdeliveryclient.DecodeAnswer(e.Value()); err != nil || a.Option != "release" || answeredAsking(e.Value()) != ask.ID {
|
||||
t.Fatalf("answer %+v err %v", a, err)
|
||||
}
|
||||
// The view's answer written again with the same option changes nothing under the ask (another option, later, is the
|
||||
@@ -585,7 +586,7 @@ func TestTellerFillsAnAskFromTheViewsAnswerOnce(t *testing.T) { // 1297/B10 AC
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
if again, err := answers.Get(b.ctx, delivery.AnswerKey(key)); err != nil || again.Revision() != e.Revision() {
|
||||
if again, err := answers.Get(b.ctx, answerKey(key)); err != nil || again.Revision() != e.Revision() {
|
||||
t.Errorf("the answer was written a second time: %v", err)
|
||||
}
|
||||
// An answer under the merge's key fills nothing.
|
||||
@@ -597,7 +598,7 @@ func TestTellerFillsAnAskFromTheViewsAnswerOnce(t *testing.T) { // 1297/B10 AC
|
||||
t.Fatal(err)
|
||||
}
|
||||
time.Sleep(time.Second)
|
||||
if _, err := answers.Get(b.ctx, delivery.AnswerKey(mkey)); err == nil {
|
||||
if _, err := answers.Get(b.ctx, answerKey(mkey)); err == nil {
|
||||
t.Error("the merge's ask was answered from a view key")
|
||||
}
|
||||
}
|
||||
@@ -656,24 +657,24 @@ func TestTellerRaisesAnAssignerAskAndWritesTheOperatorsAnswer(t *testing.T) {
|
||||
}
|
||||
var e jetstream.KeyValueEntry
|
||||
for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
|
||||
if e, err = answers.Get(b.ctx, delivery.AnswerKey(key)); err == nil {
|
||||
if e, err = answers.Get(b.ctx, answerKey(key)); err == nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
if e == nil {
|
||||
t.Fatalf("no answer under %s: %v\n%s", key, err, logs.String())
|
||||
}
|
||||
if a, err := delivery.DecodeAnswer(e.Value()); err != nil || a.Option != "release" || answeredAsking(e.Value()) != ask.ID {
|
||||
if a, err := meshdeliveryclient.DecodeAnswer(e.Value()); err != nil || a.Option != "release" || answeredAsking(e.Value()) != ask.ID {
|
||||
t.Fatalf("answer %+v err %v", a, err)
|
||||
}
|
||||
|
||||
key2, err := delivery.HoldAskKey("plex", "src2:"+strings.Repeat("3", 64))
|
||||
key2, err := asks.HoldAskKey("plex", "src2:"+strings.Repeat("3", 64))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ask2 := ask
|
||||
ask2.ID = delivery.AskID(key2)
|
||||
value2, err := delivery.EncodeAssignerAsk(key2, delivery.AssignerAsk{Ask: rawAsk(t, ask2)})
|
||||
ask2.ID = meshdeliveryclient.AskID(key2)
|
||||
value2, err := asks.EncodeAsk(key2, asks.Ask{Ask: rawAsk(t, ask2)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -714,8 +715,8 @@ func TestTellerTakesAChangedHoldAnswerFromTheView(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
kept, _ := delivery.EncodeAnswer(delivery.Answer{Option: "keep", Warrant: ask.ID + "@" + time.Now().Add(-time.Hour).UTC().Format(time.RFC3339Nano), Words: "the operator chose keep"})
|
||||
if _, err := answers.Put(b.ctx, delivery.AnswerKey(key), kept); err != nil {
|
||||
kept, _ := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "keep", Warrant: ask.ID + "@" + time.Now().Add(-time.Hour).UTC().Format(time.RFC3339Nano), Words: "the operator chose keep"})
|
||||
if _, err := answers.Put(b.ctx, answerKey(key), kept); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
askKV, err := assigner.KeyValue(b.ctx, assignerAsks)
|
||||
@@ -738,8 +739,8 @@ func TestTellerTakesAChangedHoldAnswerFromTheView(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for deadline := time.Now().Add(10 * time.Second); time.Now().Before(deadline); time.Sleep(50 * time.Millisecond) {
|
||||
if e, err := answers.Get(b.ctx, delivery.AnswerKey(key)); err == nil {
|
||||
if a, err := delivery.DecodeAnswer(e.Value()); err == nil && a.Option == "release" && answeredAsking(e.Value()) == ask.ID {
|
||||
if e, err := answers.Get(b.ctx, answerKey(key)); err == nil {
|
||||
if a, err := meshdeliveryclient.DecodeAnswer(e.Value()); err == nil && a.Option == "release" && answeredAsking(e.Value()) == ask.ID {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,8 +25,6 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
"mesh-delivery-teller/internal/ends"
|
||||
)
|
||||
|
||||
@@ -51,7 +49,7 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
gate, err := js.KeyValue(ctx, delivery.AsksBucket)
|
||||
gate, err := js.KeyValue(ctx, gateAsksBucket)
|
||||
if errors.Is(err, jetstream.ErrBucketNotFound) {
|
||||
logger.Print(waiting)
|
||||
<-ctx.Done()
|
||||
@@ -59,11 +57,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
}
|
||||
if err != nil {
|
||||
// Essential: the gate's asks are what the teller tells (issue 1945: the error names the grant).
|
||||
return process.EssentialErr("teller", delivery.AsksBucket, "tell any of the gate's asks", err)
|
||||
return process.EssentialErr("teller", gateAsksBucket, "tell any of the gate's asks", err)
|
||||
}
|
||||
// The answers it writes and what it asked: its own work, so each stops the start, naming its grant.
|
||||
var kvs [2]jetstream.KeyValue
|
||||
for i, name := range []string{delivery.AnswersBucket, askedBucket} {
|
||||
for i, name := range []string{answersBucket, askedBucket} {
|
||||
if kvs[i], err = process.EssentialBucket(ctx, js, "teller", name, "write an answer or remember what it asked"); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -74,11 +72,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// Essential: the router's warrants are how an answer reaches the teller.
|
||||
return fmt.Errorf("the consumer %s on %s cannot be read (%v): the teller hears no answer without it, so it stops; where the bus refused it or did not answer, this module's consumer (made with its grant for the seat it uses) has not reached the bus yet", cred.EventConsumerName(), process.EventsStream, err)
|
||||
}
|
||||
warrants := &decidedHandler{}
|
||||
client := &occ.Client{Module: delivery.TellerModule, State: state, Publish: publish,
|
||||
Record: recordVia(ctx, conn, delivery.TellerModule), Subscribe: decidedVia(warrants.hear, logger.Printf)}
|
||||
// The module has one durable consumer of EVENTS: one hub pulls it for the router's warrants and the views reader,
|
||||
// started once both are registered.
|
||||
hub := process.NewEventHub(logger.Printf)
|
||||
defer hub.Stop()
|
||||
client := &occ.Client{Module: tellerModule, State: state, Publish: publish,
|
||||
Record: recordVia(ctx, conn, tellerModule), Subscribe: decidedVia(hub.Handle, logger.Printf)}
|
||||
t := &Teller{Asker: client, Store: state, Answers: kvAnswers{ctx: ctx, kv: kvs[0]}, Gate: kvGate{ctx: ctx, kv: gate},
|
||||
Say: sayVia(ctx, js, delivery.TellerModule), Logf: logger.Printf, Publish: publish}
|
||||
Say: sayVia(ctx, js, tellerModule), Logf: logger.Printf, Publish: publish}
|
||||
// Hear the warrants first, so none that comes while the asks are read is missed.
|
||||
if err := client.Listen(t.Answer, t.Said); err != nil {
|
||||
return fmt.Errorf("hearing the router's warrants: %w", err)
|
||||
@@ -93,16 +94,20 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return fmt.Errorf("the teller's connection for its tool calls: %w", err)
|
||||
}
|
||||
defer calls.Close()
|
||||
reader := &views.ProcessReader{Module: delivery.TellerModule, Call: calls.Call, Logf: logger.Printf, Other: warrants.other}
|
||||
reader := &views.ProcessReader{Module: tellerModule, Call: calls.Call, Logf: logger.Printf, Hub: hub}
|
||||
findings := process.AwaitBucket(ctx, js, conn, findingsBucket, "a first view unanswered past its bound is said in the log alone", logger.Printf)
|
||||
reader.Findings = func() views.FindingWriter { return sdkfindings.Writer(ctx, findings.KV) }
|
||||
asks, err := reader.Follow(delivery.AssignerModule, assignerAsksState, delivery.AssignerModule+".assigner_read_asks")
|
||||
asks, err := reader.Follow(assignerModule, assignerAsksState, assignerModule+".assigner_read_asks")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
assignerKV := jetstream.KeyValue(asks)
|
||||
reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "teller_first_views reads nothing and teller_first_view_again is not heard", logger.Printf))
|
||||
viewKV := process.OptionalBucket(ctx, js, delivery.ViewAnswersBucket, teller, logger.Printf)
|
||||
// Every part has registered: the hub pulls the module's durable.
|
||||
if err := hub.Start(ctx, js, cred); err != nil {
|
||||
return fmt.Errorf("the consumer %s on %s cannot be read (%v): the teller hears no answer without it, so it stops", cred.EventConsumerName(), process.EventsStream, err)
|
||||
}
|
||||
viewKV := process.OptionalBucket(ctx, js, viewAnswersBucket, teller, logger.Printf)
|
||||
// The assigner's asks are raised on the operator channel whether or not the view's answers can be read (issue 1906).
|
||||
if viewKV != nil {
|
||||
t.View = kvGate{ctx: ctx, kv: viewKV}
|
||||
@@ -146,16 +151,16 @@ const teller = "the asks and answers it holds are not carried until the teller i
|
||||
|
||||
// askedBucket is the teller's state bucket asked, `<module>_<name>`: what it asked of the router and which condition it
|
||||
// raised (manifest state entry asked, novox/hq ADR 0437 §4).
|
||||
const askedBucket = delivery.TellerModule + "_asked"
|
||||
const askedBucket = tellerModule + "_asked"
|
||||
|
||||
// The teller's own states of its first view of the assigner's asks (views.StatusState) and of its findings, where the
|
||||
// condition keeper raises a first view unanswered past its bound (ADR 0615 §6).
|
||||
var (
|
||||
firstViewsBucket = delivery.TellerModule + "_" + views.StatusState
|
||||
findingsBucket = delivery.TellerModule + "_findings"
|
||||
firstViewsBucket = tellerModule + "_" + views.StatusState
|
||||
findingsBucket = tellerModule + "_findings"
|
||||
)
|
||||
|
||||
// assignerAsksState is the assigner's state of its asks (delivery.AssignerAsksBucket).
|
||||
// assignerAsksState is the assigner's state of its asks (assignerAsksBucket).
|
||||
const assignerAsksState = "asks"
|
||||
|
||||
// waiting is said once at start.
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package main
|
||||
|
||||
// The holders and buckets the teller names, taken from its own manifest (module.json: its name, its `reads`) and the
|
||||
// states it keeps itself; a unit row checks them against it (TestTheNamesAreTheManifests). No contract names a holder
|
||||
// (novox/hq ADR 0625 §4, ADR 0634 §1).
|
||||
const (
|
||||
tellerModule = "mesh-delivery-teller"
|
||||
assignerModule = "mesh-delivery-assigner"
|
||||
gateModule = "mesh-delivery-gate"
|
||||
viewModule = "mesh-delivery-view"
|
||||
|
||||
// gateAsksBucket is the gate's state asks, read by the teller (reads: mesh-delivery-gate.asks).
|
||||
gateAsksBucket = gateModule + "_asks"
|
||||
// viewAnswersBucket is the view's state answers, read by the teller (reads: mesh-delivery-view.answers).
|
||||
viewAnswersBucket = viewModule + "_answers"
|
||||
// answersBucket is the teller's own state answers, which the gate reads.
|
||||
answersBucket = tellerModule + "_answers"
|
||||
// assignerAsksBucket is the assigner's state asks (the first view of assigner_read_asks stands in for it).
|
||||
assignerAsksBucket = assignerModule + "_asks"
|
||||
|
||||
// The revisions each keeps per key.
|
||||
gateAsksHistory = 5
|
||||
answersHistory = 5
|
||||
assignerAsksHistory = 5
|
||||
viewAnswersHistory = 5
|
||||
)
|
||||
|
||||
// answerKey is the key, in the teller's answers, of its answer to the ask at askKey: the same string.
|
||||
func answerKey(askKey string) string { return askKey }
|
||||
@@ -0,0 +1,42 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"slices"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestTheNamesAreTheManifests: the holders and buckets the teller names are its manifest's (its module, its `reads`, its
|
||||
// own state answers), since no contract names a holder (novox/hq ADR 0634 §1).
|
||||
func TestTheNamesAreTheManifests(t *testing.T) {
|
||||
raw, err := os.ReadFile("../../module.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m struct {
|
||||
Module string `json:"module"`
|
||||
Reads []string `json:"reads"`
|
||||
State []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"state"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if m.Module != tellerModule {
|
||||
t.Errorf("the manifest's module is %q, the teller names itself %q", m.Module, tellerModule)
|
||||
}
|
||||
for _, read := range []string{gateModule + ".asks", viewModule + ".answers"} {
|
||||
if !slices.Contains(m.Reads, read) {
|
||||
t.Errorf("the teller reads %s, its manifest's reads are %v", read, m.Reads)
|
||||
}
|
||||
}
|
||||
var own []string
|
||||
for _, s := range m.State {
|
||||
own = append(own, tellerModule+"_"+s.Name)
|
||||
}
|
||||
if !slices.Contains(own, answersBucket) {
|
||||
t.Errorf("the teller writes %s, its manifest's states are %v", answersBucket, own)
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
)
|
||||
|
||||
// roomRefusal is the router's words when an asker already holds occ.MostOpen open asks, as the live router said them to
|
||||
@@ -17,8 +17,8 @@ import (
|
||||
const roomRefusal = "its asker already has 3 open asks"
|
||||
|
||||
// headsN is one pull request's head, distinct per n, so each n is an ask of its own key.
|
||||
func headsN(n int) []delivery.Head {
|
||||
return []delivery.Head{{Repository: "novox/mesh-delivery", Number: 10 + n, Head: strings.Repeat(string(rune('a'+n)), 40)}}
|
||||
func headsN(n int) []meshdeliveryclient.Head {
|
||||
return []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 10 + n, Head: strings.Repeat(string(rune('a'+n)), 40)}}
|
||||
}
|
||||
|
||||
// askOf puts the gate's key for n and hands it to the teller, as the watch would.
|
||||
@@ -36,19 +36,19 @@ func (b *bed) refuse(t *testing.T, id, words string) {
|
||||
t.Helper()
|
||||
delete(b.seenIDs, id)
|
||||
delete(b.records, id)
|
||||
b.deliver(t, oc.Warrant{Ask: id, Asker: delivery.TellerModule, Outcome: occ.OutcomeRefused, Words: words, At: occ.FormatTime(now)})
|
||||
b.deliver(t, oc.Warrant{Ask: id, Asker: tellerModule, Outcome: occ.OutcomeRefused, Words: words, At: occ.FormatTime(now)})
|
||||
}
|
||||
|
||||
// expire is the router's word that an ask of the teller's lapsed with nobody's answer: one of its asks ended.
|
||||
func (b *bed) expire(t *testing.T, ga gateAsked) {
|
||||
t.Helper()
|
||||
b.deliver(t, oc.Warrant{Ask: ga.Ask.ID, Asker: delivery.TellerModule, Outcome: occ.OutcomeExpired, At: occ.FormatTime(now), AskDigest: occ.Digest(ga.Ask)})
|
||||
b.deliver(t, oc.Warrant{Ask: ga.Ask.ID, Asker: tellerModule, Outcome: occ.OutcomeExpired, At: occ.FormatTime(now), AskDigest: occ.Digest(ga.Ask)})
|
||||
}
|
||||
|
||||
// askedIDs is the router ids of every ask the bus carried, in order.
|
||||
func (b *bed) askedIDs() []string {
|
||||
var out []string
|
||||
for _, p := range b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule)) {
|
||||
for _, p := range b.askedOf(oc.AcceptSubjectAsk(tellerModule)) {
|
||||
var a struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
@@ -78,7 +78,7 @@ func TestAnAskRefusedForWantOfRoomIsAskedAgainWhenAnotherEnds(t *testing.T) { //
|
||||
if len(got) != 5 || got[4] != fourth.Ask.ID {
|
||||
t.Fatalf("asks = %v, want the refused %s asked again once the first was answered", got, fourth.Ask.ID)
|
||||
}
|
||||
pubs := b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))
|
||||
pubs := b.askedOf(oc.AcceptSubjectAsk(tellerModule))
|
||||
if pubs[4].id == pubs[3].id {
|
||||
t.Errorf("asked again under the bus message id %s of its first asking: the bus drops it as a duplicate", pubs[4].id)
|
||||
}
|
||||
@@ -86,9 +86,9 @@ func TestAnAskRefusedForWantOfRoomIsAskedAgainWhenAnotherEnds(t *testing.T) { //
|
||||
t.Errorf("bus dropped %v, router dropped %v, conditions %+v", b.busDropped, b.dropped, b.conds)
|
||||
}
|
||||
// Taken this time: an answer to it writes the answer as for any ask.
|
||||
key4, _ := delivery.MergeAskKey("g", headsN(4))
|
||||
key4, _ := meshdeliveryclient.MergeAskKey("g", headsN(4))
|
||||
b.deliver(t, warrantFor(fourth.Ask, "approve"))
|
||||
if _, ok := b.answers.written[delivery.AnswerKey(key4)]; !ok {
|
||||
if _, ok := b.answers.written[answerKey(key4)]; !ok {
|
||||
t.Errorf("the answer to the ask asked again was not written; logs:\n%s", b.logs.String())
|
||||
}
|
||||
// And it is asked no more: its answer ends its wait.
|
||||
|
||||
@@ -13,8 +13,8 @@ import (
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
// The teller carries the gate's merge asks to the operator and writes the operator's answer back (novox/hq ADR 0437
|
||||
@@ -24,30 +24,35 @@ import (
|
||||
// warrant that does not hold writes no answer and is said as a condition. It holds no timer and serves no request:
|
||||
// every reaction is a revision of the gate's key or a warrant.
|
||||
|
||||
// gateAsked is the gate's ask with the router's ask decoded from its JSON (delivery.Ask.Ask is operator-channel's ask as
|
||||
// gateAsked is the gate's ask with the router's ask decoded from its JSON (the seat's ask value carries operator-channel's ask as
|
||||
// JSON, novox/hq ADR 0627 §4).
|
||||
type gateAsked struct {
|
||||
Group string
|
||||
Heads []delivery.Head
|
||||
Heads []meshdeliveryclient.Head
|
||||
Asking int
|
||||
Ask oc.Ask
|
||||
}
|
||||
|
||||
// decodeGateAsk reads the gate's ask value and the router's ask in it, as delivery.DecodeAsk did before the ask was JSON.
|
||||
// decodeGateAsk reads the gate's ask value and the router's ask in it, as meshdeliveryclient.DecodeAsk did before the ask was JSON.
|
||||
func decodeGateAsk(value []byte) (gateAsked, error) {
|
||||
ga, err := delivery.DecodeAsk(value)
|
||||
ga, err := meshdeliveryclient.DecodeAsk(value)
|
||||
if err != nil {
|
||||
return gateAsked{}, err
|
||||
}
|
||||
key, err := delivery.MergeAskKey(ga.Group, ga.Heads)
|
||||
heads := meshdeliveryclient.HeadsOf(ga.Heads)
|
||||
key, err := meshdeliveryclient.MergeAskKey(ga.Group, heads)
|
||||
if err != nil {
|
||||
return gateAsked{}, err
|
||||
}
|
||||
a, err := occ.Carried(ga.Ask, delivery.AskingID(key, ga.Asking))
|
||||
raw, err := json.Marshal(ga.Ask)
|
||||
if err != nil {
|
||||
return gateAsked{}, err
|
||||
}
|
||||
return gateAsked{Group: ga.Group, Heads: ga.Heads, Asking: ga.Asking, Ask: a}, nil
|
||||
a, err := occ.Carried(raw, meshdeliveryclient.AskingID(key, int(ga.Asking)))
|
||||
if err != nil {
|
||||
return gateAsked{}, err
|
||||
}
|
||||
return gateAsked{Group: ga.Group, Heads: heads, Asking: int(ga.Asking), Ask: a}, nil
|
||||
}
|
||||
|
||||
// store is where the teller keeps what it asked: the asker's own records (`ask.<id>`) and, beside them, which gate key
|
||||
@@ -189,7 +194,7 @@ func (t *Teller) OnAsk(key string, rev uint64, value []byte) {
|
||||
t.logf("the gate's key %s at revision %d is not an ask, and is left: %v", key, rev, err)
|
||||
return
|
||||
}
|
||||
if want, err := delivery.MergeAskKey(ga.Group, ga.Heads); err != nil || want != key {
|
||||
if want, err := meshdeliveryclient.MergeAskKey(ga.Group, ga.Heads); err != nil || want != key {
|
||||
t.logf("the gate's key %s at revision %d holds the ask of %q, not its own, and is left (%v)", key, rev, want, err)
|
||||
return
|
||||
}
|
||||
@@ -277,7 +282,7 @@ func (t *Teller) cancelOpen(key, id string) {
|
||||
func (t *Teller) OnAskGone(key string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if _, _, _, err := delivery.ParseAskKey(key); err != nil {
|
||||
if _, _, _, err := meshdeliveryclient.ParseAskKey(key); err != nil {
|
||||
t.logf("the gate's key %s was taken away and is no ask key, and is left: %v", key, err)
|
||||
return
|
||||
}
|
||||
@@ -313,7 +318,7 @@ func (t *Teller) catchUp(id string) {
|
||||
t.said(h, *w, err)
|
||||
}
|
||||
|
||||
// mergeAct is what the approve option of the gate's ask binds: the ask's verb (delivery.AskVerb: mesh-delivery.merge, or
|
||||
// mergeAct is what the approve option of the gate's ask binds: the ask's verb (meshdeliveryclient.AskVerb: mesh-delivery.merge, or
|
||||
// mesh-delivery.settle for a person's settle ask, novox/hq ADR 0556 clause 5), this ask, the answer, and the heads it covers.
|
||||
// It is spelled as the gate composes it (mesh-catalog modules/mesh-delivery-gate approval.go mergeAct, from
|
||||
// modules/mesh-delivery approval.go:63-65) and as mesh-controller verifies it (cmd/mesh-controller/moved.go:98-106).
|
||||
@@ -329,7 +334,7 @@ func mergeAct(verb, ask, option string, heads map[string]string) occ.Act {
|
||||
// headsOf is the heads an ask covers as an act names them: a delivery's id, `owner/repository@<twelve characters of its
|
||||
// commit>` with the repository in lower case, as the gate's IDOf spells it (mesh-catalog modules/mesh-delivery-gate model.go),
|
||||
// and its whole commit.
|
||||
func headsOf(heads []delivery.Head) map[string]string {
|
||||
func headsOf(heads []meshdeliveryclient.Head) map[string]string {
|
||||
out := map[string]string{}
|
||||
for _, h := range heads {
|
||||
short := h.Head
|
||||
@@ -344,7 +349,7 @@ func headsOf(heads []delivery.Head) map[string]string {
|
||||
// answeredAsking is the router id of the asking an answer is for: its warrant's id before the "@". An answer that cannot be
|
||||
// read answers no asking.
|
||||
func answeredAsking(raw []byte) string {
|
||||
a, err := delivery.DecodeAnswer(raw)
|
||||
a, err := meshdeliveryclient.DecodeAnswer(raw)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
@@ -395,10 +400,10 @@ func (t *Teller) Answer(o oc.Option, w oc.Warrant) error {
|
||||
return fmt.Errorf("the gate's ask %s is no longer the ask the operator answered (its digest is %s, the warrant's %s)",
|
||||
s.Key, occ.Digest(ga.Ask), w.AskDigest)
|
||||
}
|
||||
if err := occ.Performs(o, mergeAct(delivery.AskVerb(ga.Group), w.Ask, o.ID, headsOf(ga.Heads))); err != nil {
|
||||
if err := occ.Performs(o, mergeAct(meshdeliveryclient.AskVerb(ga.Group), w.Ask, o.ID, headsOf(ga.Heads))); err != nil {
|
||||
return err
|
||||
}
|
||||
body, err := delivery.EncodeAnswer(delivery.Answer{Option: o.ID, Warrant: warrantID(w), Words: occ.Says(w)})
|
||||
body, err := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: o.ID, Warrant: warrantID(w), Words: occ.Says(w)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -409,7 +414,7 @@ func (t *Teller) Answer(o oc.Option, w oc.Warrant) error {
|
||||
// updated at the revision read when it holds an earlier asking's answer (the reader ignores that one); left when it holds this
|
||||
// asking's.
|
||||
func (t *Teller) writeAnswer(key, askID string, body []byte, words string) error {
|
||||
akey := delivery.AnswerKey(key)
|
||||
akey := answerKey(key)
|
||||
cur, crev, found, err := t.Answers.Get(akey)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the answer under %s could not be read: %w", key, err)
|
||||
@@ -484,7 +489,7 @@ func oneLine(err error) string {
|
||||
}
|
||||
|
||||
// conditionKey is the key of the condition about an ask.
|
||||
func conditionKey(id string) string { return delivery.TellerModule + ".merge-ask." + id }
|
||||
func conditionKey(id string) string { return tellerModule + ".merge-ask." + id }
|
||||
|
||||
// askWords are the words the teller's conditions use for one kind of ask: the gate's merge ask, or one of the assigner's.
|
||||
type askWords struct {
|
||||
@@ -791,7 +796,7 @@ func (t *Teller) stillAsked(x waitingAsk) (oc.Ask, string) {
|
||||
if ga.Ask.ID != x.ID {
|
||||
return oc.Ask{}, "the gate asks again under " + ga.Ask.ID
|
||||
}
|
||||
if cur, _, has, err := t.Answers.Get(delivery.AnswerKey(x.Key)); err == nil && has && answeredAsking(cur) == x.ID {
|
||||
if cur, _, has, err := t.Answers.Get(answerKey(x.Key)); err == nil && has && answeredAsking(cur) == x.ID {
|
||||
return oc.Ask{}, "it is answered"
|
||||
}
|
||||
if err := occ.Check(ga.Ask, t.now()); err != nil {
|
||||
@@ -811,7 +816,7 @@ func (t *Teller) stillAskedByAssigner(x waitingAsk) (oc.Ask, string) {
|
||||
case a.Ask.ID != x.ID:
|
||||
return oc.Ask{}, "the assigner asks again under " + a.Ask.ID
|
||||
}
|
||||
if cur, _, has, err := t.Answers.Get(delivery.AnswerKey(x.Key)); err == nil && has && answeredAsking(cur) == x.ID {
|
||||
if cur, _, has, err := t.Answers.Get(answerKey(x.Key)); err == nil && has && answeredAsking(cur) == x.ID {
|
||||
return oc.Ask{}, "it is answered"
|
||||
}
|
||||
if err := occ.Check(a.Ask, t.now()); err != nil {
|
||||
@@ -832,7 +837,7 @@ func (t *Teller) askAgain(a oc.Ask, msgID string) error {
|
||||
if _, err := t.Store.Create(keptKey(a.ID), occ.Kept{Ask: a, Asked: t.now()}); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := t.Publish(oc.AcceptSubjectAsk(delivery.TellerModule), a, msgID); err != nil {
|
||||
if err := t.Publish(oc.AcceptSubjectAsk(tellerModule), a, msgID); err != nil {
|
||||
_ = t.Store.Delete(keptKey(a.ID))
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -14,8 +14,9 @@ import (
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
meshdelivery "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -137,7 +138,7 @@ func newBed(t *testing.T) *bed {
|
||||
t.Helper()
|
||||
b := &bed{store: &memStore{entries: map[string]*stdio.StateEntry{}}, answers: &memAnswers{written: map[string][]byte{}, revs: map[string]uint64{}},
|
||||
gate: memGate{}, records: map[string]string{}, seenIDs: map[string]bool{}, msgIDs: map[string]bool{}}
|
||||
b.client = &occ.Client{Module: delivery.TellerModule, State: b.store, Now: func() time.Time { return now },
|
||||
b.client = &occ.Client{Module: tellerModule, State: b.store, Now: func() time.Time { return now },
|
||||
Publish: func(subject string, body any, id string) error {
|
||||
raw, err := json.Marshal(body)
|
||||
if b.msgIDs[id] {
|
||||
@@ -146,7 +147,7 @@ func newBed(t *testing.T) *bed {
|
||||
return err
|
||||
}
|
||||
b.msgIDs[id] = true
|
||||
if subject == oc.AcceptSubjectAsk(delivery.TellerModule) {
|
||||
if subject == oc.AcceptSubjectAsk(tellerModule) {
|
||||
var a struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
@@ -188,26 +189,26 @@ func newBed(t *testing.T) *bed {
|
||||
// `mesh-delivery.merge` for this ask and the heads (mesh-catalog modules/mesh-delivery approval.go:123-191 composeMergeAsk
|
||||
// and :63-65 mergeAct), under the id its key gives (mesh-sdk go/delivery AskID). boundTo are the heads the options bind,
|
||||
// which are the ask's own unless a test says another.
|
||||
func gateAsk(t *testing.T, heads, boundTo []delivery.Head) (key string, value []byte, ask gateAsked) {
|
||||
func gateAsk(t *testing.T, heads, boundTo []meshdeliveryclient.Head) (key string, value []byte, ask gateAsked) {
|
||||
t.Helper()
|
||||
return gateAskN(t, heads, boundTo, 1)
|
||||
}
|
||||
|
||||
// gateAskN is the asking-th writing of the group's ask at these heads: its router id is AskingID(key, asking) (mesh-sdk
|
||||
// go/delivery Ask.Asking), the first asking's the key's own.
|
||||
func gateAskN(t *testing.T, heads, boundTo []delivery.Head, asking int) (key string, value []byte, ask gateAsked) {
|
||||
func gateAskN(t *testing.T, heads, boundTo []meshdeliveryclient.Head, asking int) (key string, value []byte, ask gateAsked) {
|
||||
t.Helper()
|
||||
return gateAskOf(t, "g", "mesh-delivery.merge", heads, boundTo, asking)
|
||||
}
|
||||
|
||||
// gateAskOf is gateAskN for a group whose options bind the verb given.
|
||||
func gateAskOf(t *testing.T, group, verb string, heads, boundTo []delivery.Head, asking int) (key string, value []byte, ask gateAsked) {
|
||||
func gateAskOf(t *testing.T, group, verb string, heads, boundTo []meshdeliveryclient.Head, asking int) (key string, value []byte, ask gateAsked) {
|
||||
t.Helper()
|
||||
key, err := delivery.MergeAskKey(group, heads)
|
||||
key, err := meshdeliveryclient.MergeAskKey(group, heads)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id := delivery.AskingID(key, asking)
|
||||
id := meshdeliveryclient.AskingID(key, asking)
|
||||
a := oc.Ask{ID: id, Headline: "Merge into main: novox/mesh-delivery#12", Explanation: "Merges two pull requests.\nReview: approved.",
|
||||
Who: occ.Operator, Expires: occ.FormatTime(now.Add(occ.ApproveLasts - 10*time.Minute)), OnExpiry: "the merge stays held", About: "delivery.mesh-delivery.12"}
|
||||
for _, o := range []struct{ id, label, does string }{{"approve", "Approve", "merges"}, {"decline", "Decline", "merges nothing"}} {
|
||||
@@ -221,7 +222,7 @@ func gateAskOf(t *testing.T, group, verb string, heads, boundTo []delivery.Head,
|
||||
return key, encodeGate(t, ask), ask
|
||||
}
|
||||
|
||||
// rawAsk is the router's ask as the JSON a gate or assigner value carries (delivery.Ask.Ask).
|
||||
// rawAsk is the router's ask as the JSON a gate or assigner value carries (the seat's ask value).
|
||||
func rawAsk(t *testing.T, a oc.Ask) json.RawMessage {
|
||||
t.Helper()
|
||||
b, err := json.Marshal(a)
|
||||
@@ -235,27 +236,27 @@ func rawAsk(t *testing.T, a oc.Ask) json.RawMessage {
|
||||
// (an id other than the one they give, no option), then carried as JSON.
|
||||
func encodeGate(t *testing.T, ga gateAsked) []byte {
|
||||
t.Helper()
|
||||
key, err := delivery.MergeAskKey(ga.Group, ga.Heads)
|
||||
key, err := meshdeliveryclient.MergeAskKey(ga.Group, ga.Heads)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if want := delivery.AskingID(key, ga.Asking); ga.Ask.ID != want || len(ga.Ask.Options) == 0 {
|
||||
if want := meshdeliveryclient.AskingID(key, ga.Asking); ga.Ask.ID != want || len(ga.Ask.Options) == 0 {
|
||||
t.Fatalf("the ask's id %q, want %q, with %d options", ga.Ask.ID, want, len(ga.Ask.Options))
|
||||
}
|
||||
value, err := delivery.EncodeAsk(delivery.Ask{Group: ga.Group, Heads: ga.Heads, Asking: ga.Asking, Ask: rawAsk(t, ga.Ask)})
|
||||
value, err := gateValue(ga.Group, ga.Heads, ga.Asking, rawAsk(t, ga.Ask))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func twoHeads() []delivery.Head {
|
||||
return []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, {Repository: "novox/mesh-sdk", Number: 4, Head: headB}}
|
||||
func twoHeads() []meshdeliveryclient.Head {
|
||||
return []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: headA}, {Repository: "novox/mesh-sdk", Number: 4, Head: headB}}
|
||||
}
|
||||
|
||||
// warrantFor is the router's warrant for a chosen option of an ask, as the router verified the operator.
|
||||
func warrantFor(a oc.Ask, option string) oc.Warrant {
|
||||
return oc.Warrant{Ask: a.ID, Asker: delivery.TellerModule, Outcome: occ.OutcomeChosen, Option: option, Label: "Approve", Level: occ.Approve,
|
||||
return oc.Warrant{Ask: a.ID, Asker: tellerModule, Outcome: occ.OutcomeChosen, Option: option, Label: "Approve", Level: occ.Approve,
|
||||
By: &oc.Person{Who: occ.Operator, Kind: "telegram", Identity: "42", Verified: "user id"}, Channel: "telegram",
|
||||
At: occ.FormatTime(now.Add(time.Hour)), AskDigest: occ.Digest(a)}
|
||||
}
|
||||
@@ -263,7 +264,7 @@ func warrantFor(a oc.Ask, option string) oc.Warrant {
|
||||
func (b *bed) deliver(t *testing.T, w oc.Warrant) {
|
||||
t.Helper()
|
||||
raw, _ := json.Marshal(w)
|
||||
if err := b.hear(stdio.Envelope{Key: oc.Seat + ".decided." + delivery.TellerModule, Body: raw}); err != nil {
|
||||
if err := b.hear(stdio.Envelope{Key: oc.Seat + ".decided." + tellerModule, Body: raw}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
@@ -293,30 +294,30 @@ func TestAnAsksKeyBecomesOneRouterAskAndDeletingItCancelsIt(t *testing.T) { // 1
|
||||
key, value, ga := gateAsk(t, twoHeads(), twoHeads())
|
||||
b.gate[key] = value
|
||||
b.t.OnAsk(key, 3, value)
|
||||
asked := b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))
|
||||
asked := b.askedOf(oc.AcceptSubjectAsk(tellerModule))
|
||||
if len(asked) != 1 || asked[0].id != ga.Ask.ID {
|
||||
t.Fatalf("published %+v, want one ask under %s with id %s", b.pubs, delivery.TellerModule, ga.Ask.ID)
|
||||
t.Fatalf("published %+v, want one ask under %s with id %s", b.pubs, tellerModule, ga.Ask.ID)
|
||||
}
|
||||
var got oc.Ask
|
||||
if err := json.Unmarshal(asked[0].body, &got); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if occ.Digest(got) != occ.Digest(ga.Ask) || got.Headline != ga.Ask.Headline || got.Explanation != ga.Ask.Explanation || got.ID != delivery.AskID(key) {
|
||||
if occ.Digest(got) != occ.Digest(ga.Ask) || got.Headline != ga.Ask.Headline || got.Explanation != ga.Ask.Explanation || got.ID != meshdeliveryclient.AskID(key) {
|
||||
t.Errorf("the router's ask differs from the gate's: %+v", got)
|
||||
}
|
||||
b.t.OnAsk(key, 3, value) // the revision seen again, as at a restart
|
||||
b.t.OnAsk(key, 4, value) // the key written again with the same ask
|
||||
if n := len(b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))); n != 1 {
|
||||
if n := len(b.askedOf(oc.AcceptSubjectAsk(tellerModule))); n != 1 {
|
||||
t.Errorf("the ask was published %d times", n)
|
||||
}
|
||||
delete(b.gate, key)
|
||||
b.t.OnAskGone(key)
|
||||
cancels := b.askedOf(oc.AcceptSubjectCancel(delivery.TellerModule))
|
||||
cancels := b.askedOf(oc.AcceptSubjectCancel(tellerModule))
|
||||
if len(cancels) != 1 || string(cancels[0].body) != `{"id":"`+ga.Ask.ID+`"}` {
|
||||
t.Fatalf("cancels = %+v", cancels)
|
||||
}
|
||||
b.t.OnAskGone(key)
|
||||
if n := len(b.askedOf(oc.AcceptSubjectCancel(delivery.TellerModule))); n != 1 {
|
||||
if n := len(b.askedOf(oc.AcceptSubjectCancel(tellerModule))); n != 1 {
|
||||
t.Errorf("the ask was cancelled %d times", n)
|
||||
}
|
||||
// Issue 774 (2026-10-31): two merge asks of pull requests merged by another path held two of the router's three places,
|
||||
@@ -337,15 +338,15 @@ func TestAnAsksKeyBecomesOneRouterAskAndDeletingItCancelsIt(t *testing.T) { // 1
|
||||
_, fourth := b.askOf(t, 4, 4)
|
||||
b.refuse(t, fourth.Ask.ID, roomRefusal)
|
||||
r := b.t.EndAsk(c.by(k1, first), "its pull request merged without the Approve", "lead@shanks")
|
||||
cancels := b.askedOf(oc.AcceptSubjectCancel(delivery.TellerModule))
|
||||
cancels := b.askedOf(oc.AcceptSubjectCancel(tellerModule))
|
||||
if !r.Ended || r.ID != first.Ask.ID || r.Key != k1 || len(cancels) != 1 || string(cancels[0].body) != `{"id":"`+first.Ask.ID+`"}` {
|
||||
t.Fatalf("end = %+v, cancels %+v", r, cancels)
|
||||
}
|
||||
b.deliver(t, oc.Warrant{Ask: first.Ask.ID, Asker: delivery.TellerModule, Outcome: occ.OutcomeCancelled, At: occ.FormatTime(now), AskDigest: occ.Digest(first.Ask)})
|
||||
b.deliver(t, oc.Warrant{Ask: first.Ask.ID, Asker: tellerModule, Outcome: occ.OutcomeCancelled, At: occ.FormatTime(now), AskDigest: occ.Digest(first.Ask)})
|
||||
if got := b.askedIDs(); len(got) != 5 || got[4] != fourth.Ask.ID {
|
||||
t.Errorf("asks = %v, want the refused %s asked again once the ask ended by hand left", got, fourth.Ask.ID)
|
||||
}
|
||||
if again := b.t.EndAsk(k1, "again", "lead@shanks"); again.Ended || len(b.askedOf(oc.AcceptSubjectCancel(delivery.TellerModule))) != 1 {
|
||||
if again := b.t.EndAsk(k1, "again", "lead@shanks"); again.Ended || len(b.askedOf(oc.AcceptSubjectCancel(tellerModule))) != 1 {
|
||||
t.Errorf("a second end = %+v: an ended ask is left", again)
|
||||
}
|
||||
})
|
||||
@@ -357,7 +358,7 @@ func TestAnAsksKeyBecomesOneRouterAskAndDeletingItCancelsIt(t *testing.T) { // 1
|
||||
b.askOf(t, 3, 3)
|
||||
_, fourth := b.askOf(t, 4, 4)
|
||||
b.refuse(t, fourth.Ask.ID, roomRefusal)
|
||||
if r := b.t.EndAsk(fourth.Ask.ID, "asked about what no longer holds", "lead@shanks"); !r.Ended || len(b.askedOf(oc.AcceptSubjectCancel(delivery.TellerModule))) != 0 {
|
||||
if r := b.t.EndAsk(fourth.Ask.ID, "asked about what no longer holds", "lead@shanks"); !r.Ended || len(b.askedOf(oc.AcceptSubjectCancel(tellerModule))) != 0 {
|
||||
t.Fatalf("end of a waiting ask = %+v", r)
|
||||
}
|
||||
b.deliver(t, warrantFor(first.Ask, "approve"))
|
||||
@@ -377,11 +378,11 @@ func TestAWarrantThatHoldsWritesOneAnswerAndASecondWritesNone(t *testing.T) { //
|
||||
b.t.OnAsk(key, 1, value)
|
||||
w := warrantFor(ga.Ask, "approve")
|
||||
b.deliver(t, w)
|
||||
raw, ok := b.answers.written[delivery.AnswerKey(key)]
|
||||
raw, ok := b.answers.written[answerKey(key)]
|
||||
if !ok || len(b.answers.written) != 1 {
|
||||
t.Fatalf("answers = %v", b.answers.written)
|
||||
}
|
||||
a, err := delivery.DecodeAnswer(raw)
|
||||
a, err := meshdeliveryclient.DecodeAnswer(raw)
|
||||
if err != nil || a.Option != "approve" || a.Warrant != warrantID(w) || !strings.Contains(a.Words, "chose Approve") {
|
||||
t.Errorf("answer %+v err %v", a, err)
|
||||
}
|
||||
@@ -398,19 +399,19 @@ func TestAWarrantThatHoldsWritesOneAnswerAndASecondWritesNone(t *testing.T) { //
|
||||
// 1280 (ADR 0556 clause 5): a settle ask holds when its options bind mesh-delivery.settle, and not when they bind a merge.
|
||||
for verb, holds := range map[string]bool{"mesh-delivery.settle": true, "mesh-delivery.merge": false} {
|
||||
b := newBed(t)
|
||||
group := delivery.SettleGroup([]string{"novox/mesh-delivery@" + headA[:12]})
|
||||
group := meshdeliveryclient.SettleGroup([]string{"novox/mesh-delivery@" + headA[:12]})
|
||||
key, value, ga := gateAskOf(t, group, verb, twoHeads(), twoHeads(), 1)
|
||||
b.gate[key] = value
|
||||
b.t.OnAsk(key, 1, value)
|
||||
b.deliver(t, warrantFor(ga.Ask, "approve"))
|
||||
if _, written := b.answers.written[delivery.AnswerKey(key)]; written != holds {
|
||||
if _, written := b.answers.written[answerKey(key)]; written != holds {
|
||||
t.Errorf("a settle ask binding %s: answer written %v, want %v", verb, written, holds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAWarrantThatDoesNotHoldWritesNoAnswerAndRaisesTheCondition(t *testing.T) { // 1343/AC3
|
||||
other := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: headB}, {Repository: "novox/mesh-sdk", Number: 4, Head: headB}}
|
||||
other := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: headB}, {Repository: "novox/mesh-sdk", Number: 4, Head: headB}}
|
||||
cases := map[string]func(t *testing.T) (*bed, string, oc.Warrant){
|
||||
"heads that differ from the ones the option bound": func(t *testing.T) (*bed, string, oc.Warrant) {
|
||||
b := newBed(t)
|
||||
@@ -455,7 +456,7 @@ func TestAWarrantThatDoesNotHoldWritesNoAnswerAndRaisesTheCondition(t *testing.T
|
||||
|
||||
func TestAGoodAnswerClearsTheConditionOfAnAskAskedAgain(t *testing.T) { // 1343/AC3 (cleared when a good answer is written)
|
||||
b := newBed(t)
|
||||
other := []delivery.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: headB}, {Repository: "novox/mesh-sdk", Number: 4, Head: headB}}
|
||||
other := []meshdeliveryclient.Head{{Repository: "novox/mesh-delivery", Number: 12, Head: headB}, {Repository: "novox/mesh-sdk", Number: 4, Head: headB}}
|
||||
key, bad, gaBad := gateAsk(t, twoHeads(), other)
|
||||
b.gate[key] = bad
|
||||
b.t.OnAsk(key, 1, bad)
|
||||
@@ -467,11 +468,11 @@ func TestAGoodAnswerClearsTheConditionOfAnAskAskedAgain(t *testing.T) { // 1343/
|
||||
_, good, gaGood := gateAskN(t, twoHeads(), twoHeads(), 2)
|
||||
b.gate[key] = good
|
||||
b.t.OnAsk(key, 2, good)
|
||||
if n := len(b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))); n != 2 {
|
||||
if n := len(b.askedOf(oc.AcceptSubjectAsk(tellerModule))); n != 2 {
|
||||
t.Fatalf("the ask was published %d times, want again at the new revision", n)
|
||||
}
|
||||
b.deliver(t, warrantFor(gaGood.Ask, "approve"))
|
||||
if _, ok := b.answers.written[delivery.AnswerKey(key)]; !ok {
|
||||
if _, ok := b.answers.written[answerKey(key)]; !ok {
|
||||
t.Fatalf("no answer: %v\n%s", b.answers.written, b.logs.String())
|
||||
}
|
||||
if len(b.conds) != 2 || b.conds[1].State != occ.StateCleared || b.conds[1].Key != b.conds[0].Key {
|
||||
@@ -487,7 +488,7 @@ func TestAWarrantComingWhileTheTellerWasAwayIsReadFromTheRouter(t *testing.T) {
|
||||
w, _ := json.Marshal(warrantFor(ga.Ask, "approve"))
|
||||
b.records[ga.Ask.ID] = `{"state":"chosen","warrant":` + string(w) + `}`
|
||||
b.t.OnAsk(key, 1, value) // the teller starts again and reads the key at the same revision
|
||||
if _, ok := b.answers.written[delivery.AnswerKey(key)]; !ok {
|
||||
if _, ok := b.answers.written[answerKey(key)]; !ok {
|
||||
t.Errorf("no answer: %s", b.logs.String())
|
||||
}
|
||||
}
|
||||
@@ -503,11 +504,11 @@ func TestARewriteWithAskingTwoCancelsAskingOneAndAsksUnderTheNewID(t *testing.T)
|
||||
}
|
||||
b.gate[key] = two
|
||||
b.t.OnAsk(key, 2, two)
|
||||
cancels := b.askedOf(oc.AcceptSubjectCancel(delivery.TellerModule))
|
||||
cancels := b.askedOf(oc.AcceptSubjectCancel(tellerModule))
|
||||
if len(cancels) != 1 || string(cancels[0].body) != `{"id":"`+g1.Ask.ID+`"}` {
|
||||
t.Fatalf("cancels = %+v, want asking 1's router ask", cancels)
|
||||
}
|
||||
asked := b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))
|
||||
asked := b.askedOf(oc.AcceptSubjectAsk(tellerModule))
|
||||
if len(asked) != 2 || asked[1].id != g2.Ask.ID {
|
||||
t.Fatalf("asks = %+v, want the second under %s", asked, g2.Ask.ID)
|
||||
}
|
||||
@@ -517,14 +518,14 @@ func TestARewriteWithAskingTwoCancelsAskingOneAndAsksUnderTheNewID(t *testing.T)
|
||||
// The same revision again, and a warrant for the first asking (the gate has moved on), change nothing.
|
||||
b.t.OnAsk(key, 2, two)
|
||||
b.deliver(t, warrantFor(g1.Ask, "approve"))
|
||||
if len(b.answers.written) != 0 || len(b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))) != 2 {
|
||||
t.Errorf("answers %v asks %d", b.answers.written, len(b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))))
|
||||
if len(b.answers.written) != 0 || len(b.askedOf(oc.AcceptSubjectAsk(tellerModule))) != 2 {
|
||||
t.Errorf("answers %v asks %d", b.answers.written, len(b.askedOf(oc.AcceptSubjectAsk(tellerModule))))
|
||||
}
|
||||
// The warrant of the second names it: its answer's warrant is the router id and the time.
|
||||
w := warrantFor(g2.Ask, "approve")
|
||||
b.deliver(t, w)
|
||||
raw, ok := b.answers.written[delivery.AnswerKey(key)]
|
||||
a, err := delivery.DecodeAnswer(raw)
|
||||
raw, ok := b.answers.written[answerKey(key)]
|
||||
a, err := meshdeliveryclient.DecodeAnswer(raw)
|
||||
if !ok || err != nil || a.Warrant != warrantID(w) {
|
||||
t.Errorf("answer %+v (%v) err %v", a, ok, err)
|
||||
}
|
||||
@@ -536,27 +537,27 @@ func TestAnAskWhoseWordsChangedUnderTheSameIDIsCancelledAndNotAskedAgain(t *test
|
||||
b.t.OnAsk(key, 1, value)
|
||||
changed := ga
|
||||
changed.Ask.Explanation = "Merges two pull requests, now with a third point.\nReview: approved."
|
||||
v2, err := delivery.EncodeAsk(delivery.Ask{Group: changed.Group, Heads: changed.Heads, Asking: changed.Asking, Ask: rawAsk(t, changed.Ask)})
|
||||
v2, err := gateValue(changed.Group, changed.Heads, changed.Asking, rawAsk(t, changed.Ask))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b.t.OnAsk(key, 2, v2)
|
||||
if n := len(b.askedOf(oc.AcceptSubjectCancel(delivery.TellerModule))); n != 1 {
|
||||
if n := len(b.askedOf(oc.AcceptSubjectCancel(tellerModule))); n != 1 {
|
||||
t.Errorf("cancels = %d", n)
|
||||
}
|
||||
if n := len(b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))); n != 1 || len(b.dropped) != 0 {
|
||||
if n := len(b.askedOf(oc.AcceptSubjectAsk(tellerModule))); n != 1 || len(b.dropped) != 0 {
|
||||
t.Errorf("asks = %d dropped %v: the router would drop a second ask under the id", n, b.dropped)
|
||||
}
|
||||
}
|
||||
|
||||
func TestACapitalisedRepositoryIsSpelledInLowerCaseInTheAct(t *testing.T) { // T1 of review round 1
|
||||
b := newBed(t)
|
||||
heads := []delivery.Head{{Repository: "novox/Mesh-Delivery", Number: 12, Head: headA}}
|
||||
key, err := delivery.MergeAskKey("g", heads)
|
||||
heads := []meshdeliveryclient.Head{{Repository: "novox/Mesh-Delivery", Number: 12, Head: headA}}
|
||||
key, err := meshdeliveryclient.MergeAskKey("g", heads)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
id := delivery.AskID(key)
|
||||
id := meshdeliveryclient.AskID(key)
|
||||
// The act as the gate binds it: its IDOf lowercases the repository (mesh-catalog modules/mesh-delivery-gate model.go).
|
||||
binds, err := occ.ActDigest(occ.Act{"verb": "mesh-delivery.merge", "ask": id, "option": "approve", "heads": "novox/mesh-delivery@" + headA[:12] + "=" + headA})
|
||||
if err != nil {
|
||||
@@ -568,7 +569,7 @@ func TestACapitalisedRepositoryIsSpelledInLowerCaseInTheAct(t *testing.T) { // T
|
||||
b.gate[key] = value
|
||||
b.t.OnAsk(key, 1, value)
|
||||
b.deliver(t, warrantFor(a, "approve"))
|
||||
if _, ok := b.answers.written[delivery.AnswerKey(key)]; !ok {
|
||||
if _, ok := b.answers.written[answerKey(key)]; !ok {
|
||||
t.Errorf("no answer for the gate's spelling: %s", b.logs.String())
|
||||
}
|
||||
}
|
||||
@@ -587,7 +588,7 @@ func TestARefusedAskIsSaidAsACondition(t *testing.T) { // ADR 0437 §9
|
||||
b := newBed(t)
|
||||
key, value, ga := gateAsk(t, twoHeads(), twoHeads())
|
||||
b.t.OnAsk(key, 1, value)
|
||||
w := oc.Warrant{Ask: ga.Ask.ID, Asker: delivery.TellerModule, Outcome: occ.OutcomeRefused, Words: "no channel can send", At: occ.FormatTime(now)}
|
||||
w := oc.Warrant{Ask: ga.Ask.ID, Asker: tellerModule, Outcome: occ.OutcomeRefused, Words: "no channel can send", At: occ.FormatTime(now)}
|
||||
b.deliver(t, w)
|
||||
if len(b.conds) != 1 || b.conds[0].State != occ.StateRaised || !strings.Contains(b.conds[0].Summary, "no channel can send") {
|
||||
t.Errorf("conditions = %+v", b.conds)
|
||||
@@ -600,22 +601,22 @@ func TestAnAnswerToAnEarlierAskingDoesNotBlockTheNextAsking(t *testing.T) { // R
|
||||
b.gate[key] = one
|
||||
b.t.OnAsk(key, 1, one)
|
||||
b.deliver(t, warrantFor(g1.Ask, "approve"))
|
||||
first, ok := b.answers.written[delivery.AnswerKey(key)]
|
||||
if a, err := delivery.DecodeAnswer(first); !ok || err != nil || !strings.HasPrefix(a.Warrant, g1.Ask.ID+"@") {
|
||||
first, ok := b.answers.written[answerKey(key)]
|
||||
if a, err := meshdeliveryclient.DecodeAnswer(first); !ok || err != nil || !strings.HasPrefix(a.Warrant, g1.Ask.ID+"@") {
|
||||
t.Fatalf("asking 1's answer: %+v %v %v", a, ok, err)
|
||||
}
|
||||
// The gate rewrites the key at asking 2 (a review record changed). Asking 1's answer stays in the bucket.
|
||||
_, two, g2 := gateAskN(t, twoHeads(), twoHeads(), 2)
|
||||
b.gate[key] = two
|
||||
b.t.OnAsk(key, 2, two)
|
||||
asked := b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))
|
||||
asked := b.askedOf(oc.AcceptSubjectAsk(tellerModule))
|
||||
if len(asked) != 2 || asked[1].id != g2.Ask.ID {
|
||||
t.Fatalf("asks = %+v, want asking 2 asked under %s", asked, g2.Ask.ID)
|
||||
}
|
||||
w := warrantFor(g2.Ask, "approve")
|
||||
b.deliver(t, w)
|
||||
raw := b.answers.written[delivery.AnswerKey(key)]
|
||||
a, err := delivery.DecodeAnswer(raw)
|
||||
raw := b.answers.written[answerKey(key)]
|
||||
a, err := meshdeliveryclient.DecodeAnswer(raw)
|
||||
if err != nil || a.Warrant != warrantID(w) {
|
||||
t.Fatalf("the key holds %+v (%v), want asking 2's answer", a, err)
|
||||
}
|
||||
@@ -625,8 +626,8 @@ func TestAnAnswerToAnEarlierAskingDoesNotBlockTheNextAsking(t *testing.T) { // R
|
||||
b.deliver(t, w2)
|
||||
b.t.OnAsk(key, 2, two)
|
||||
b.t.OnAsk(key, 3, two)
|
||||
if b.answers.writes != 2 || len(b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))) != 2 || len(b.dropped) != 0 {
|
||||
t.Errorf("writes %d asks %d dropped %v", b.answers.writes, len(b.askedOf(oc.AcceptSubjectAsk(delivery.TellerModule))), b.dropped)
|
||||
if b.answers.writes != 2 || len(b.askedOf(oc.AcceptSubjectAsk(tellerModule))) != 2 || len(b.dropped) != 0 {
|
||||
t.Errorf("writes %d asks %d dropped %v", b.answers.writes, len(b.askedOf(oc.AcceptSubjectAsk(tellerModule))), b.dropped)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -634,3 +635,16 @@ func TestAnAnswerToAnEarlierAskingDoesNotBlockTheNextAsking(t *testing.T) { // R
|
||||
type refusingBus struct{ err error }
|
||||
|
||||
func (r refusingBus) KeyValue(context.Context, string) (jetstream.KeyValue, error) { return nil, r.err }
|
||||
|
||||
// gateValue is the gate's ask value for the group, heads and asking, carrying the router's ask as its JSON.
|
||||
func gateValue(group string, heads []meshdeliveryclient.Head, asking int, ask json.RawMessage) ([]byte, error) {
|
||||
var asked any
|
||||
if err := json.Unmarshal(ask, &asked); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var items []meshdelivery.AsksByMergeGroupHeadsValueHeadsItem
|
||||
for _, h := range heads {
|
||||
items = append(items, meshdelivery.AsksByMergeGroupHeadsValueHeadsItem{Repository: h.Repository, Number: int64(h.Number), Head: h.Head})
|
||||
}
|
||||
return meshdeliveryclient.EncodeAsk(meshdelivery.AsksByMergeGroupHeadsValue{Group: group, Heads: items, Asking: int64(asking), Ask: asked})
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
||||
)
|
||||
|
||||
// The view's answers (novox/hq ADR 0528 clause 6, ADR 0437 §10, issue 1297 B10). The operator may answer, in the view, the
|
||||
@@ -19,7 +21,7 @@ import (
|
||||
func (t *Teller) OnViewAnswer(key string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
askKey, err := delivery.ParseViewAnswerKey(key)
|
||||
askKey, err := viewstate.ParseViewAnswerKey(key)
|
||||
if err != nil {
|
||||
t.logf("the view's key %s is left: %v", key, err)
|
||||
return
|
||||
@@ -32,7 +34,7 @@ func (t *Teller) OnViewAnswer(key string) {
|
||||
func (t *Teller) OnAssignerAsk(key string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if _, err := delivery.ViewAnswerKey(key); err != nil {
|
||||
if _, err := viewstate.ViewAnswerKey(key); err != nil {
|
||||
t.logf("the assigner's key %s is left and never answered from the view: %v", key, err)
|
||||
return
|
||||
}
|
||||
@@ -49,7 +51,7 @@ func (t *Teller) fillFromView(askKey string) {
|
||||
if t.Assigner == nil || t.View == nil {
|
||||
return
|
||||
}
|
||||
viewKey, err := delivery.ViewAnswerKey(askKey) // refuses an ask that needs proof, naming the phone
|
||||
viewKey, err := viewstate.ViewAnswerKey(askKey) // refuses an ask that needs proof, naming the phone
|
||||
if err != nil {
|
||||
t.logf("%s is not answered from the view: %v", askKey, err)
|
||||
return
|
||||
@@ -70,7 +72,7 @@ func (t *Teller) fillFromView(askKey string) {
|
||||
if !found {
|
||||
return // the answer waits for its ask: OnAssignerAsk fills it when it comes
|
||||
}
|
||||
va, err := delivery.DecodeViewAnswer(rawAnswer)
|
||||
va, err := viewstate.DecodeViewAnswer(rawAnswer)
|
||||
if err != nil {
|
||||
t.logf("%s: the view's answer is not readable and fills nothing: %v", viewKey, err)
|
||||
return
|
||||
@@ -80,7 +82,7 @@ func (t *Teller) fillFromView(askKey string) {
|
||||
t.logf("%s: the assigner's ask is not readable and takes no answer: %v", askKey, err)
|
||||
return
|
||||
}
|
||||
if err := delivery.ViewAnswerFits(va, askKey, delivery.AssignerAsk{Asking: ask.Asking}); err != nil {
|
||||
if err := viewstate.ViewAnswerFits(va, askKey, asks.Ask{Asking: ask.Asking}); err != nil {
|
||||
t.logf("%s: nothing is written: %v", askKey, err)
|
||||
return
|
||||
}
|
||||
@@ -95,7 +97,7 @@ func (t *Teller) fillFromView(askKey string) {
|
||||
t.logf("%s: nothing is written: the view's answer chose %q, an option of level %s, which only a channel that proves who answered takes", askKey, va.Option, o.Level)
|
||||
return
|
||||
}
|
||||
akey := delivery.AnswerKey(askKey)
|
||||
akey := answerKey(askKey)
|
||||
cur, crev, has, err := t.Answers.Get(akey)
|
||||
if err != nil {
|
||||
t.logf("%s: the answer could not be read: %v", askKey, err)
|
||||
@@ -108,7 +110,7 @@ func (t *Teller) fillFromView(askKey string) {
|
||||
if w := strings.TrimSpace(va.Words); w != "" {
|
||||
words += ": " + w
|
||||
}
|
||||
body, err := delivery.EncodeAnswer(delivery.Answer{Option: va.Option, Warrant: ask.Ask.ID + "@" + at.UTC().Format(time.RFC3339Nano), Words: words})
|
||||
body, err := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: va.Option, Warrant: ask.Ask.ID + "@" + at.UTC().Format(time.RFC3339Nano), Words: words})
|
||||
if err != nil {
|
||||
t.logf("%s: the answer could not be made: %v", askKey, err)
|
||||
return
|
||||
@@ -148,12 +150,12 @@ func (t *Teller) viewAnswer(key string) ([]byte, time.Time, bool, error) {
|
||||
|
||||
// changesMind says whether the view's answer va, written at at, replaces the answer cur of the same asking of the hold ask at
|
||||
// askKey: it chose another option and was written after cur's warrant.
|
||||
func (t *Teller) changesMind(askKey string, cur []byte, va delivery.ViewAnswer, at time.Time) bool {
|
||||
k, err := delivery.ParseAnyAskKey(askKey)
|
||||
if err != nil || k.Kind != delivery.AskKindHold {
|
||||
func (t *Teller) changesMind(askKey string, cur []byte, va viewstate.ViewAnswer, at time.Time) bool {
|
||||
k, err := asks.ParseAnyAskKey(askKey)
|
||||
if err != nil || k.Kind != asks.AskKindHold {
|
||||
return false
|
||||
}
|
||||
a, err := delivery.DecodeAnswer(cur)
|
||||
a, err := meshdeliveryclient.DecodeAnswer(cur)
|
||||
if err != nil || a.Option == va.Option {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks"
|
||||
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate"
|
||||
)
|
||||
|
||||
const fpHeld = "src2:2222222222222222222222222222222222222222222222222222222222222222"
|
||||
@@ -17,14 +19,14 @@ const fpHeld = "src2:22222222222222222222222222222222222222222222222222222222222
|
||||
// takes it; the value is `{"v":1,"ask":<the router's ask>}`).
|
||||
func holdAsk(t *testing.T) (key string, value []byte, ask oc.Ask) {
|
||||
t.Helper()
|
||||
key, err := delivery.HoldAskKey("plex", fpHeld)
|
||||
key, err := asks.HoldAskKey("plex", fpHeld)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ask = oc.Ask{ID: delivery.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check.", Who: occ.Operator,
|
||||
ask = oc.Ask{ID: meshdeliveryclient.AskID(key), Headline: "plex is held after a failed health check", Explanation: "ace failed the check.", Who: occ.Operator,
|
||||
Expires: occ.FormatTime(now.Add(occ.ApproveLasts)), OnExpiry: "the fingerprint stays held",
|
||||
Options: []oc.Option{{ID: "keep", Label: "Keep it held", Does: "keeps it held", Level: occ.Acknowledge}, {ID: "release", Label: "Release it", Does: "lets the move go on", Level: occ.Acknowledge}}}
|
||||
value, err = delivery.EncodeAssignerAsk(key, delivery.AssignerAsk{Ask: rawAsk(t, ask)})
|
||||
value, err = asks.EncodeAsk(key, asks.Ask{Ask: rawAsk(t, ask)})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -32,13 +34,13 @@ func holdAsk(t *testing.T) (key string, value []byte, ask oc.Ask) {
|
||||
}
|
||||
|
||||
func holdKey() string {
|
||||
k, _ := delivery.HoldAskKey("plex", fpHeld)
|
||||
k, _ := asks.HoldAskKey("plex", fpHeld)
|
||||
return k
|
||||
}
|
||||
|
||||
func viewAnswer(t *testing.T, option, words string) []byte {
|
||||
t.Helper()
|
||||
v, err := delivery.EncodeViewAnswer(delivery.ViewAnswer{Asking: delivery.AskID(holdKey()), Option: option, Words: words})
|
||||
v, err := viewstate.EncodeViewAnswer(viewstate.ViewAnswer{Asking: meshdeliveryclient.AskID(holdKey()), Option: option, Words: words})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -63,7 +65,7 @@ func TestAViewAnswerFillsTheAskOnce(t *testing.T) { // 1297/B10 AC
|
||||
if !ok {
|
||||
t.Fatalf("no answer under %s; log:\n%s", key, b.logs.String())
|
||||
}
|
||||
a, err := delivery.DecodeAnswer(raw)
|
||||
a, err := meshdeliveryclient.DecodeAnswer(raw)
|
||||
if err != nil || a.Option != "release" || answeredAsking(raw) != ask.ID || !strings.Contains(a.Words, "view") || !strings.Contains(a.Words, "the check was a flake") {
|
||||
t.Errorf("answer = %+v, %v", a, err)
|
||||
}
|
||||
@@ -86,7 +88,7 @@ func TestAViewAnswerHeardBeforeItsAskFillsItWhenTheAskComes(t *testing.T) { // 1
|
||||
}
|
||||
assigner[key] = value
|
||||
b.t.OnAssignerAsk(key)
|
||||
if a, err := delivery.DecodeAnswer(b.answers.written[key]); err != nil || a.Option != "keep" {
|
||||
if a, err := meshdeliveryclient.DecodeAnswer(b.answers.written[key]); err != nil || a.Option != "keep" {
|
||||
t.Errorf("answer = %+v, %v", a, err)
|
||||
}
|
||||
}
|
||||
@@ -129,7 +131,7 @@ func TestAViewAnswerNeverReplacesTheAnswerOfTheSameAsking(t *testing.T) { // 129
|
||||
key, value, ask := holdAsk(t)
|
||||
assigner[key] = value
|
||||
view[key+".view"] = viewAnswer(t, "keep", "")
|
||||
early, err := delivery.EncodeAnswer(delivery.Answer{Option: "release", Warrant: ask.ID + "@2026-10-20T09:00:00Z", Words: "answered elsewhere"})
|
||||
early, err := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "release", Warrant: ask.ID + "@2026-10-20T09:00:00Z", Words: "answered elsewhere"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -139,7 +141,7 @@ func TestAViewAnswerNeverReplacesTheAnswerOfTheSameAsking(t *testing.T) { // 129
|
||||
t.Errorf("the answer of the same asking was replaced")
|
||||
}
|
||||
// An earlier asking's answer (another router id) does not stop this one.
|
||||
older, _ := delivery.EncodeAnswer(delivery.Answer{Option: "release", Warrant: "an-older-id@2026-10-19T09:00:00Z", Words: "old"})
|
||||
older, _ := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "release", Warrant: "an-older-id@2026-10-19T09:00:00Z", Words: "old"})
|
||||
b.answers.written[key], b.answers.revs[key] = older, 8
|
||||
b.t.OnViewAnswer(key + ".view")
|
||||
if b.answers.writes != 1 || answeredAsking(b.answers.written[key]) != ask.ID {
|
||||
@@ -169,7 +171,7 @@ func TestAStaleViewAnswerOfAnotherAskingFillsNothing(t *testing.T) { // 1297/B10
|
||||
assigner, view := withView(b)
|
||||
key, value, _ := holdAsk(t)
|
||||
assigner[key] = value
|
||||
stale, err := delivery.EncodeViewAnswer(delivery.ViewAnswer{Asking: "an-earlier-asking", Option: "release"})
|
||||
stale, err := viewstate.EncodeViewAnswer(viewstate.ViewAnswer{Asking: "an-earlier-asking", Option: "release"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -203,7 +205,7 @@ func TestALaterViewAnswerChangesAHoldsAnswer(t *testing.T) {
|
||||
b.t.Assigner, b.t.View = assigner, view
|
||||
key, value, ask := holdAsk(t)
|
||||
assigner[key] = value
|
||||
kept, _ := delivery.EncodeAnswer(delivery.Answer{Option: "keep", Warrant: ask.ID + "@" + now.Add(time.Hour).UTC().Format(time.RFC3339Nano), Words: "the operator, via telegram, chose keep"})
|
||||
kept, _ := meshdeliveryclient.EncodeAnswer(meshdeliveryclient.Answer{Option: "keep", Warrant: ask.ID + "@" + now.Add(time.Hour).UTC().Format(time.RFC3339Nano), Words: "the operator, via telegram, chose keep"})
|
||||
b.answers.written[key], b.answers.revs[key] = kept, 3
|
||||
view.memGate[key+".view"] = viewAnswer(t, option, "it was a flake")
|
||||
view.at[key+".view"] = viewAt
|
||||
@@ -212,7 +214,7 @@ func TestALaterViewAnswerChangesAHoldsAnswer(t *testing.T) {
|
||||
}
|
||||
later := now.Add(2 * time.Hour)
|
||||
b, key, ask := setup(later, "release")
|
||||
a, err := delivery.DecodeAnswer(b.answers.written[key])
|
||||
a, err := meshdeliveryclient.DecodeAnswer(b.answers.written[key])
|
||||
if err != nil || b.answers.writes != 1 || a.Option != "release" || a.Warrant != ask.ID+"@"+later.UTC().Format(time.RFC3339Nano) {
|
||||
t.Fatalf("writes %d, answer %+v, %v; log:\n%s", b.answers.writes, a, err, b.logs.String())
|
||||
}
|
||||
@@ -226,7 +228,7 @@ func TestALaterViewAnswerChangesAHoldsAnswer(t *testing.T) {
|
||||
}
|
||||
// A restore ask answered stays answered: only a hold's answer may be changed.
|
||||
bb := newBed(t)
|
||||
if bb.t.changesMind("restore.ace.plex.2222222222222222222222222222222222222222222222222222222222222222", b.answers.written[key], delivery.ViewAnswer{Option: "keep"}, later.Add(time.Hour)) {
|
||||
if bb.t.changesMind("restore.ace.plex.2222222222222222222222222222222222222222222222222222222222222222", b.answers.written[key], viewstate.ViewAnswer{Option: "keep"}, later.Add(time.Hour)) {
|
||||
t.Error("a restore ask's answer was changed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,10 @@ module mesh-delivery-teller
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdelivery v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-assigner/asks v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/modules/mesh-delivery-view/viewstate v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-delivery/sdk v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
@@ -14,14 +14,16 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/clients/meshdeliveryclient"
|
||||
)
|
||||
|
||||
const (
|
||||
Tool = "teller_end_ask"
|
||||
// module is the teller's own name (its manifest's module), the holder of the bucket.
|
||||
module = "mesh-delivery-teller"
|
||||
Tool = "teller_end_ask"
|
||||
// State is the teller's state of ends asked by hand: the bucket mesh-delivery-teller_ends.
|
||||
State = "ends"
|
||||
Bucket = delivery.TellerModule + "_" + State
|
||||
Bucket = module + "_" + State
|
||||
)
|
||||
|
||||
// Result is the teller's answer to an end: which ask it found (its router id and the gate's or the assigner's key it was
|
||||
@@ -64,7 +66,7 @@ func Decode(data []byte) (End, error) {
|
||||
}
|
||||
|
||||
// New is the value teller_end_ask writes: its key and the value. The key is the router's spelling of the name given
|
||||
// (delivery.AskID), which a bucket key takes whatever the name holds, and is the same for the same name, so a second end
|
||||
// (meshdeliveryclient.AskID), which a bucket key takes whatever the name holds, and is the same for the same name, so a second end
|
||||
// of one ask overwrites the first.
|
||||
func New(ask, why, by string, at time.Time) (string, []byte, error) {
|
||||
ask, why = strings.TrimSpace(ask), strings.TrimSpace(why)
|
||||
@@ -75,5 +77,5 @@ func New(ask, why, by string, at time.Time) (string, []byte, error) {
|
||||
return "", nil, errors.New("say why the ask is ended (why), in plain words: it is logged with who ended it")
|
||||
}
|
||||
data, err := json.Marshal(End{V: 1, Ask: ask, Why: why, By: by, At: at.UTC()})
|
||||
return delivery.AskID(ask), data, err
|
||||
return meshdeliveryclient.AskID(ask), data, err
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
@@ -16,7 +17,7 @@ import (
|
||||
"mesh-delivery-watchman/internal/watch"
|
||||
)
|
||||
|
||||
// **What the watchman consumes** (its manifest's `consumes`; novox/hq ADR 0595 §4, design 25 § 12.2; DLV-W1): the indexer's
|
||||
// **What the watchman consumes** (its manifest's `consumes`; novox/hq ADR 0595 §4, design 25 § 12.2; DLV-W1): the seat mesh-catalog's
|
||||
// manifest-accepted and the seat mesh-broker's created and refused, all on its durable consumer on EVENTS (the seat's
|
||||
// events on the seat's subjects like every seat's: the lead's ruling, registry milestone 13:0x); beside it any module
|
||||
// stream its membership names (Hears). Each consumer is the one the grants part makes; one
|
||||
@@ -34,7 +35,7 @@ const (
|
||||
// and refused. Anything else is left.
|
||||
func consumed(w *watch.Watchman, subject string, body []byte, logf func(string, ...any)) {
|
||||
switch {
|
||||
case strings.HasSuffix(subject, ".event."+eventAccepted) && strings.HasPrefix(subject, "mesh.mod."):
|
||||
case subject == meshcatalog.SubjectManifestAccepted:
|
||||
var a delivery.ManifestAccepted
|
||||
if err := json.Unmarshal(body, &a); err != nil || a.Module == "" {
|
||||
logf("a manifest-accepted that does not read is left: %v", err)
|
||||
@@ -58,7 +59,15 @@ func consumed(w *watch.Watchman, subject string, body []byte, logf func(string,
|
||||
|
||||
// hearConsumed opens the watchman's durable consumers: on EVENTS now, and on each module stream its membership names as
|
||||
// that membership arrives. A consumer the bus does not hold yet is said and tried again at the membership's next change.
|
||||
func hearConsumed(ctx context.Context, js jetstream.JetStream, cred process.Account, w *watch.Watchman, logf func(string, ...any)) {
|
||||
func hearConsumed(ctx context.Context, js jetstream.JetStream, cred process.Account, hub *process.EventHub, w *watch.Watchman, logf func(string, ...any)) {
|
||||
// The seats' events (manifest-accepted, the bus module's created and refused) are one handler of the module's one
|
||||
// pull of EVENTS, beside the take-in events and the views reader.
|
||||
if err := hub.Handle("mesh.seat.*.event.>", func(m jetstream.Msg) {
|
||||
consumed(w, m.Subject(), m.Data(), logf)
|
||||
_ = m.Ack()
|
||||
}); err != nil {
|
||||
w.NotHeard("EVENTS", "the seats' events cannot be handled", err)
|
||||
}
|
||||
var mu sync.Mutex
|
||||
opened := map[string]bool{}
|
||||
open := func(stream, name string) {
|
||||
@@ -69,6 +78,15 @@ func hearConsumed(ctx context.Context, js jetstream.JetStream, cred process.Acco
|
||||
}
|
||||
// Not bound, the stream is the finding family-not-heard (ADR 0615 §5) until it is, at a later change of the
|
||||
// membership or a later start.
|
||||
if stream == "EVENTS" {
|
||||
if err := hub.Start(ctx, js, cred); err != nil {
|
||||
w.NotHeard(stream, "what it carries is heard once the grants part has made the consumer "+name, err)
|
||||
return
|
||||
}
|
||||
w.Heard(stream)
|
||||
opened[stream] = true
|
||||
return
|
||||
}
|
||||
cons, err := js.Consumer(ctx, stream, name)
|
||||
if err != nil {
|
||||
w.NotHeard(stream, "what it carries is heard once the grants part has made the consumer "+name, err)
|
||||
|
||||
@@ -234,7 +234,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return fmt.Errorf("the watchman's connection for its tool calls: %w", err)
|
||||
}
|
||||
defer calls.Close()
|
||||
reader := &views.ProcessReader{Module: delivery.WatchmanModule, Call: calls.Call, Logf: logger.Printf,
|
||||
// The module has one durable consumer of EVENTS: one hub pulls it for the views reader, the indexer's take-in events
|
||||
// and the seats' events, started once every part is registered.
|
||||
hub := process.NewEventHub(logger.Printf)
|
||||
reader := &views.ProcessReader{Module: delivery.WatchmanModule, Call: calls.Call, Logf: logger.Printf, Hub: hub,
|
||||
Findings: func() views.FindingWriter {
|
||||
return sdkfindings.Writer(ctx, func() jetstream.KeyValue { return kvs.findings })
|
||||
}}
|
||||
@@ -450,17 +453,14 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
w.TakeInCaughtUp()
|
||||
}
|
||||
}
|
||||
hc, err := process.HearEvents(tc, func(m jetstream.Msg) { takeInHeard(w, m, logger.Printf) }, logger.Printf)
|
||||
if err != nil {
|
||||
if err := hub.Handle("mesh.mod."+delivery.IndexerModule+".event.take-in.*", func(m jetstream.Msg) { takeInHeard(w, m, logger.Printf) }); err != nil {
|
||||
return err
|
||||
}
|
||||
defer hc.Stop()
|
||||
}
|
||||
w.Reconcile()
|
||||
if err := hear(conn, w, dead, logger.Printf); err != nil {
|
||||
return err
|
||||
}
|
||||
hearConsumed(ctx, js, cred, w, logger.Printf)
|
||||
|
||||
errs := make(chan error, 16)
|
||||
if healthWatch != nil {
|
||||
@@ -468,6 +468,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
}
|
||||
// The first views of the wants and the modules, asked now and read each on its own goroutine once answered (ADR 0615).
|
||||
reader.Start(ctx, js, cred, process.AwaitBucket(ctx, js, conn, firstViewsBucket, "watchman_first_views reads nothing and watchman_first_view_again is not heard", logger.Printf))
|
||||
// Every part has registered: the hub pulls the module's durable, and the watchman's other consumers open.
|
||||
hearConsumed(ctx, js, cred, hub, w, logger.Printf)
|
||||
defer hub.Stop()
|
||||
// The indexer's modules (ADR 0578 §1): a module whose value carries a refusal of its manifest is a finding.
|
||||
verdictEach := func(e jetstream.KeyValueEntry) { w.ModuleVerdict(e.Key(), e.Value(), opOf(e)) }
|
||||
go func() { errs <- readLate(ctx, w, modules, delivery.ModulesBucket, verdictEach) }()
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestRunNeedsItsCredential(t *testing.T) { // 1135/AC6
|
||||
if !out.findings[watch.FindingKey(watch.ScopeModule, "m", watch.KindBusStateRefused+"-l")] {
|
||||
t.Fatalf("refused reached no finding: %v", out.findings)
|
||||
}
|
||||
consumed(w, "mesh.mod.mesh-delivery-indexer.event.manifest-accepted", []byte(`{"module":"m","digest":"d","bus-state":[]}`), say)
|
||||
consumed(w, "mesh.seat.mesh-catalog.event.manifest-accepted", []byte(`{"module":"m","digest":"d","bus-state":[]}`), say)
|
||||
if len(out.findings) != 0 {
|
||||
t.Fatalf("an accepted manifest no longer asking for l left %v", out.findings)
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ module mesh-delivery-watchman
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshbroker v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261104174359-05b3e2e1b94f
|
||||
|
||||
@@ -109,7 +109,7 @@ func TestManifestStateAndReads(t *testing.T) { // 1135/AC6
|
||||
t.Errorf("invokes = %v, want %v", m.Invokes, want)
|
||||
}
|
||||
// DLV-M5 its first views, 1585/T20 the indexer's take-in events, and DLV-W1 (ADR 0595 §4): the accepted verdicts and the bus module's created and refused, durably.
|
||||
if want := []string{"mesh-delivery-assigner.views.wants", "mesh-delivery-indexer.take-in.pending", "mesh-delivery-indexer.take-in.taken", "mesh-delivery-indexer.views.modules", "mesh-delivery-indexer.manifest-accepted", "mesh-broker.created", "mesh-broker.refused"}; !reflect.DeepEqual(m.Consumes, want) {
|
||||
if want := []string{"mesh-delivery-assigner.views.wants", "mesh-delivery-indexer.take-in.pending", "mesh-delivery-indexer.take-in.taken", "mesh-delivery-indexer.views.modules", "mesh-catalog.manifest-accepted", "mesh-broker.created", "mesh-broker.refused"}; !reflect.DeepEqual(m.Consumes, want) {
|
||||
t.Errorf("consumes = %v, want %v", m.Consumes, want)
|
||||
}
|
||||
// 1952/WM1: the watchman holds the seat mesh-watchdogs at mesh scope, its first seat (novox/hq ADR 0564 §1, §4).
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"mesh-delivery-indexer.take-in.pending",
|
||||
"mesh-delivery-indexer.take-in.taken",
|
||||
"mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.manifest-accepted",
|
||||
"mesh-catalog.manifest-accepted",
|
||||
"mesh-broker.created",
|
||||
"mesh-broker.refused"
|
||||
],
|
||||
|
||||
@@ -189,6 +189,22 @@ func TestTheGrantsPartWhole(t *testing.T) {
|
||||
}
|
||||
t.Cleanup(func() { _ = osub.Unsubscribe() })
|
||||
}
|
||||
// D-30 (novox/hq ADR 0636 §1): the seat mesh-catalog's holder answers the seat list with no trunk seat named, so the reader
|
||||
// has heard the seat and holds the bootstrapped definitions, and every module is composed. (The unit case holds a module
|
||||
// while the list is unread.)
|
||||
var seatAnswering atomic.Bool
|
||||
seatAnswering.Store(true)
|
||||
for _, subject := range []string{"mesh.seat.mesh-catalog.call.seat-definitions", "mesh.seat.mesh-catalog.call.seat-definitions.>"} {
|
||||
if _, err := nc.Subscribe(subject, func(m *nats.Msg) {
|
||||
if !seatAnswering.Load() {
|
||||
_ = m.Respond([]byte(`{"error":"not connected yet","node":"a"}`))
|
||||
return
|
||||
}
|
||||
answer(m, map[string]any{"revision": 1, "seats": []any{}})
|
||||
}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// A membership is never said on its subject any more (issue 1126).
|
||||
var said atomic.Int32
|
||||
if _, err := nc.Subscribe("mesh.assignment.>", func(*nats.Msg) { said.Add(1) }); err != nil {
|
||||
|
||||
@@ -32,11 +32,12 @@ import (
|
||||
"github.com/nats-io/nats.go"
|
||||
"github.com/nats-io/nats.go/jetstream"
|
||||
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/announce"
|
||||
"git.novox.be/novox/mesh-sdk/go/process"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"mesh-grants/internal/grants"
|
||||
@@ -107,6 +108,11 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return err
|
||||
}
|
||||
bus := newBusReader(ctx, js)
|
||||
// The module has one durable consumer of EVENTS: one hub pulls it and hands each message to the part that hears it (the
|
||||
// owners' views events, the seat mesh-catalog's verdicts and seat list events, the other seats' events the follower hears),
|
||||
// started once all are registered.
|
||||
hub := process.NewEventHub(logger.Printf)
|
||||
defer hub.Stop()
|
||||
// The owners' five states by their first views (ADR 0615, ADR 0622): asked through the part's own account, then
|
||||
// followed on the owners' views families, which the hearing below hands them.
|
||||
sayCond := sayVia(ctx, js, account.Module)
|
||||
@@ -124,6 +130,10 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
Objects: sealedObjects{ctx: ctx, js: js}, Logf: logger.Printf,
|
||||
Bus: grants.BusAddress{URL: account.URL, Fingerprint: account.Fingerprint}}
|
||||
f.Watch = fv
|
||||
// The seat list: read over the connection and the hub, given to catalogue at every change, and a pass composes again then.
|
||||
gate := &seatGate{}
|
||||
gate.follow(seatlist.ProcessBus(conn.Call, hub), f.Wake, logger.Printf)
|
||||
f.Part.SeatList = gate
|
||||
f.Part.Say = sayCond
|
||||
fv.Start(ctx, f.Wake)
|
||||
f.Asked = func(wake func()) error {
|
||||
@@ -142,41 +152,35 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
// dropped carry the bindings: ADR 0595 §1, §7), each on the durable consumer its own membership names; one not
|
||||
// there yet is opened at the next change, and one opened is never opened twice.
|
||||
opened := map[string]bool{}
|
||||
registered := false
|
||||
f.Hear = func(handle func(string, []byte)) error {
|
||||
consume := func(stream, name string) error {
|
||||
if opened[stream] {
|
||||
return nil
|
||||
if !registered {
|
||||
registered = true
|
||||
// An owner's views event goes to the follower of its state, which acknowledges it (firstviews.go).
|
||||
if err := hub.Handle(views.EventsPattern, hearViews); err != nil {
|
||||
return err
|
||||
}
|
||||
cons, err := js.Consumer(ctx, stream, name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the consumer %s on %s: %w", name, stream, err)
|
||||
}
|
||||
if _, err := cons.Consume(func(m jetstream.Msg) {
|
||||
// An owner's views event goes to the follower of its state, which acknowledges it (firstviews.go).
|
||||
if strings.Contains(m.Subject(), ".event."+views.EventPrefix) {
|
||||
hearViews(m)
|
||||
return
|
||||
}
|
||||
// The indexer's verdict events go to the verdicts' first view (ADR 0622 §2), which takes each at its revision.
|
||||
if event, ok := strings.CutPrefix(m.Subject(), "mesh.mod."+grants.IndexerModule+".event."); ok &&
|
||||
(event == delivery.ManifestAcceptedEvent || event == delivery.ManifestRefusedEvent) {
|
||||
if err := fv.HearVerdict(ctx, event, m.Data(), logger.Printf); err != nil {
|
||||
// The seat mesh-catalog's verdict events go to the verdicts' first view (ADR 0622 §2), which takes each at its revision.
|
||||
for _, event := range []string{meshcatalog.EventManifestAccepted, meshcatalog.EventManifestRefused} {
|
||||
if err := hub.Handle("mesh.seat."+meshcatalog.Seat+".event."+event, func(m jetstream.Msg) {
|
||||
if err := fv.HearVerdict(ctx, strings.TrimPrefix(m.Subject(), "mesh.seat."+meshcatalog.Seat+".event."), m.Data(), logger.Printf); err != nil {
|
||||
logger.Printf("views: %s is not a verdict event, dropped: %v", m.Subject(), err)
|
||||
_ = m.Term()
|
||||
return
|
||||
}
|
||||
_ = m.Ack()
|
||||
return
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
// The other seats' events the part follows its passes by (Consumes).
|
||||
for _, subject := range grants.SeatEventSubjects() {
|
||||
if err := hub.Handle(subject, func(m jetstream.Msg) { handle(m.Subject(), m.Data()); _ = m.Ack() }); err != nil {
|
||||
return err
|
||||
}
|
||||
handle(m.Subject(), m.Data())
|
||||
_ = m.Ack()
|
||||
}, jetstream.ConsumeErrHandler(func(_ jetstream.ConsumeContext, err error) { logger.Printf("hearing the bus: %v", err) })); err != nil {
|
||||
return err
|
||||
}
|
||||
opened[stream] = true
|
||||
return nil
|
||||
}
|
||||
if err := consume("EVENTS", account.Node+"_"+account.Module); err != nil {
|
||||
if err := hub.Start(ctx, js, account); err != nil {
|
||||
return err
|
||||
}
|
||||
hears, err := grants.OwnHears(ownStore{conn}, account.Node)
|
||||
@@ -184,9 +188,20 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro
|
||||
return err
|
||||
}
|
||||
for _, h := range hears {
|
||||
if err := consume(h.Stream, h.Consumer); err != nil {
|
||||
if opened[h.Stream] {
|
||||
continue
|
||||
}
|
||||
cons, err := js.Consumer(ctx, h.Stream, h.Consumer)
|
||||
if err != nil {
|
||||
return fmt.Errorf("the consumer %s on %s: %w", h.Consumer, h.Stream, err)
|
||||
}
|
||||
if _, err := cons.Consume(func(m jetstream.Msg) {
|
||||
handle(m.Subject(), m.Data())
|
||||
_ = m.Ack()
|
||||
}, jetstream.ConsumeErrHandler(func(_ jetstream.ConsumeContext, err error) { logger.Printf("hearing the bus: %v", err) })); err != nil {
|
||||
return err
|
||||
}
|
||||
opened[h.Stream] = true
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"mesh-grants/internal/grants"
|
||||
"sync/atomic"
|
||||
|
||||
stdio "git.novox.be/novox/mesh-sdk/go"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
)
|
||||
|
||||
// The grants part composes from the seat list it reads (novox/hq ADR 0635 §1, ADR 0636 §1, D-30): through the seat
|
||||
// mesh-catalog's verb seat-definitions and its two events, from the bootstrapped definitions until the seat has answered. What
|
||||
// the reader holds is given to catalogue at every change, which composes every membership and grant, and a module that names a
|
||||
// seat the list does not yet is held back, as a manifest waiting for a judge is, and what runs is composed from its running
|
||||
// build's manifest. The process reads over its connection and the event hub; the tools' bundle over the runtime.
|
||||
|
||||
// seatListTools are the reader's two tools (ADR 0636 §7): every operator's session and the healers may read again.
|
||||
var seatListAgainCallers = []string{stdio.AnyPerson, "mesh-healers"}
|
||||
|
||||
// seatGate is the reader the part gates by, set once it is started; before, it acts on nothing new.
|
||||
type seatGate struct {
|
||||
reader atomic.Pointer[seatlist.Reader]
|
||||
}
|
||||
|
||||
func (g *seatGate) MayAct(seats ...string) bool {
|
||||
r := g.reader.Load()
|
||||
return r != nil && r.MayAct(seats...)
|
||||
}
|
||||
|
||||
// follow starts the reader over bus: what it holds is given to catalogue at once and at every change, and changed is called
|
||||
// then (a pass composes again).
|
||||
func (g *seatGate) follow(bus seatlist.Bus, changed func(), logf func(string, ...any)) *seatlist.Reader {
|
||||
reader, err := seatlist.Start(seatlist.Config{Bus: bus, Bootstrap: seatlist.Bootstrapped(),
|
||||
OnChange: func(seats []catalogue.Seat) {
|
||||
catalogue.UseSeats(seats)
|
||||
if changed != nil {
|
||||
changed()
|
||||
}
|
||||
}})
|
||||
if err != nil {
|
||||
logf("the grants part cannot follow its seat list, so it composes only the modules the bootstrapped seats name: %v", err)
|
||||
}
|
||||
if reader != nil {
|
||||
catalogue.UseSeats(reader.Seats())
|
||||
g.reader.Store(reader)
|
||||
}
|
||||
return reader
|
||||
}
|
||||
|
||||
// tools are the two tools over whatever reader the gate holds at the call.
|
||||
func (g *seatGate) tools() []stdio.Tool {
|
||||
names := []string{grants.ToolSeatList, grants.ToolSeatListAgain}
|
||||
shape := seatlist.Tools(&seatlist.Reader{}, names[0], names[1], seatListAgainCallers)
|
||||
for i := range shape {
|
||||
run := i
|
||||
shape[i].Run = func(args map[string]any) (any, error) {
|
||||
r := g.reader.Load()
|
||||
if r == nil {
|
||||
return nil, errors.New("the seat list reader has not started yet: the runtime has not served this module")
|
||||
}
|
||||
return seatlist.Tools(r, names[0], names[1], seatListAgainCallers)[run].Run(args)
|
||||
}
|
||||
}
|
||||
return shape
|
||||
}
|
||||
@@ -57,7 +57,7 @@
|
||||
"mesh-delivery-indexer.indexer_bus_state"
|
||||
],
|
||||
"consumes": [
|
||||
"mesh-delivery-indexer.manifest-accepted"
|
||||
"mesh-catalog.manifest-accepted"
|
||||
],
|
||||
"uses": [
|
||||
"operator-channel"
|
||||
|
||||
@@ -5,11 +5,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/seatlist"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
|
||||
"git.novox.be/novox/mesh-contracts/health-and-repair/go/clients/meshhandactsclient"
|
||||
@@ -254,8 +256,14 @@ func recordHandAct(what, why string) error {
|
||||
// serveTools serves the tools to the runtime until it stops.
|
||||
func serveTools() error {
|
||||
tv := &toolViews{}
|
||||
p := &grants.Part{Read: runtimeReader{views: tv}, Call: runtimeBus{}, Store: runtimeStore{}, Emit: runtimeBus{}}
|
||||
tools := toolsOf(p)
|
||||
gate := &seatGate{}
|
||||
p := &grants.Part{Read: runtimeReader{views: tv}, Call: runtimeBus{}, Store: runtimeStore{}, Emit: runtimeBus{}, SeatList: gate}
|
||||
// The reader asks through the runtime, which serves once Serve has its channel.
|
||||
go func() {
|
||||
<-stdio.Served()
|
||||
gate.follow(seatlist.StdioBus(), nil, func(f string, a ...any) { fmt.Fprintf(os.Stderr, "mesh-grants: "+f+"\n", a...) })
|
||||
}()
|
||||
tools := append(toolsOf(p), gate.tools()...)
|
||||
for i := range tools {
|
||||
run := tools[i].Run
|
||||
tools[i].Run = func(a map[string]any) (any, error) { tv.fresh(); return run(a) }
|
||||
|
||||
@@ -3,6 +3,7 @@ module mesh-grants
|
||||
go 1.26.0
|
||||
|
||||
require (
|
||||
git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/seats/meshbroker v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/go/seats/nodeengine v0.0.0-00010101000000-000000000000
|
||||
git.novox.be/novox/mesh-contracts/identity-and-access/go/seats/meshnodeenroller v0.0.0-00010101000000-000000000000
|
||||
|
||||
@@ -2214,6 +2214,16 @@ func TestGrants(t *testing.T) {
|
||||
t.Errorf("the part consumes %s", c)
|
||||
}
|
||||
}
|
||||
// The hub's seat handlers are the seats' events of Consumes on the seats' own subjects, the verdicts' and views' apart.
|
||||
for _, subject := range grants.SeatEventSubjects() {
|
||||
seat, event, _ := strings.Cut(strings.TrimPrefix(subject, "mesh.seat."), ".event.")
|
||||
if !slices.Contains(grants.Consumes, seat+"."+event) || strings.Contains(subject, "mesh-catalog") {
|
||||
t.Errorf("%s is no seat event of Consumes beside the verdicts and views", subject)
|
||||
}
|
||||
}
|
||||
if n := len(grants.SeatEventSubjects()); n != 6 {
|
||||
t.Errorf("the part hears %d seats' events, want the keeper's two, the vault's one and the broker's three", n)
|
||||
}
|
||||
for _, c := range b.calls {
|
||||
if strings.Contains(c, "mesh-controller") {
|
||||
t.Errorf("the part called %s", c)
|
||||
@@ -2221,6 +2231,31 @@ func TestGrants(t *testing.T) {
|
||||
}
|
||||
})
|
||||
|
||||
// D-30 (novox/hq ADR 0636 §1): until the seat list has been read, a module whose seats are not all bootstrapped ones is held back
|
||||
// as a manifest waiting for a judge is, named `manifest of <module>: it waits for the seat list`, and composed once the
|
||||
// list names its seats; the bus module, which claims a bootstrapped seat, is composed throughout.
|
||||
t.Run("the seat list unread holds a module whose seats are not bootstrapped", func(t *testing.T) {
|
||||
b := fixture()
|
||||
p := partOn(b)
|
||||
gate := &seatGate{bootstrapped: []string{"mesh-broker"}}
|
||||
p.SeatList = gate
|
||||
p.Compose()
|
||||
refused := strings.Join(p.Last().Refused, "\n")
|
||||
for _, module := range []string{"mod-x", "mod-y"} {
|
||||
if !strings.Contains(refused, "manifest of "+module+": it waits for the seat list") {
|
||||
t.Errorf("%s is not held while the seat list is unread: %v", module, p.Last().Refused)
|
||||
}
|
||||
}
|
||||
if strings.Contains(refused, "manifest of nats") {
|
||||
t.Errorf("the bus module claims a bootstrapped seat and is held: %v", p.Last().Refused)
|
||||
}
|
||||
gate.read = true
|
||||
p.Compose()
|
||||
if r := p.Last().Refused; len(r) != 0 {
|
||||
t.Errorf("held after the seat list was read: %v", r)
|
||||
}
|
||||
})
|
||||
|
||||
// DLV-G1, 1966/AC32-AC33 (novox/hq ADR 0595 §1, §7; design 25 § 12.2): the bus module's created writes the binding into
|
||||
// the module's membership on every machine it is assigned, one assigned later included; recreated rewrites it; dropped
|
||||
// removes it; before created, on a refused, or from an emitter holding no mesh-broker, none.
|
||||
@@ -3270,3 +3305,25 @@ func waitFor(t *testing.T, ok func() bool) {
|
||||
}
|
||||
t.Fatal("never came")
|
||||
}
|
||||
|
||||
// seatGate is a seat list reader as a test gives it: unread, it lets a module act whose seats are all bootstrapped ones (and
|
||||
// at least one); read, any.
|
||||
type seatGate struct {
|
||||
bootstrapped []string
|
||||
read bool
|
||||
}
|
||||
|
||||
func (g *seatGate) MayAct(seats ...string) bool {
|
||||
if g.read {
|
||||
return true
|
||||
}
|
||||
if len(seats) == 0 {
|
||||
return false
|
||||
}
|
||||
for _, s := range seats {
|
||||
if !slices.Contains(g.bootstrapped, s) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -1097,7 +1097,7 @@ func theBusModulesProcessKeepsBusState(t *testing.T) {
|
||||
{Module: "node-tools", ToolRunner: true},
|
||||
{Module: "nats", SnapshotsTheBus: true, Holds: []Seat{{Name: meshbroker.Seat, Serves: BrokerVerbs(),
|
||||
Emits: []string{"created", "refused"}}},
|
||||
Consumes: []string{"mesh-delivery-indexer.manifest-accepted"}, Uses: []Seat{{Name: "operator-channel", Accepts: []string{"condition"}, Emits: []string{"decided"}, ByCaller: []string{"condition", "decided"}}},
|
||||
Watches: []Seat{{Name: "mesh-catalog", Emits: []string{"manifest-accepted"}}}, Uses: []Seat{{Name: "operator-channel", Accepts: []string{"condition"}, Emits: []string{"decided"}, ByCaller: []string{"condition", "decided"}}},
|
||||
Streams: []ModuleStream{{Module: "nats", Name: "BROKER_EVENTS", Events: []string{"created", "refused"}, MaxAgeDays: 30, MaxMiB: 256}},
|
||||
State: []Bucket{{Module: "nats", Name: "spent", ReadOnly: true}},
|
||||
Processes: []ProcessPrincipal{{Process: "keeper", State: []string{"asked", "spent"}, Keeps: true}, {Process: "other"},
|
||||
@@ -1214,7 +1214,7 @@ func theBusModulesProcessKeepsBusState(t *testing.T) {
|
||||
made := false
|
||||
for _, mc := range AccountConsumers(users) {
|
||||
if mc.Consumer.Stream == "EVENTS" && mc.Consumer.Name == "a_nats_keeper" &&
|
||||
slices.Equal(mc.Consumer.Filters, []string{"mesh.mod.mesh-delivery-indexer.event.manifest-accepted",
|
||||
slices.Equal(mc.Consumer.Filters, []string{"mesh.seat.mesh-catalog.event.manifest-accepted",
|
||||
"mesh.seat.operator-channel.event.decided.nats"}) {
|
||||
made = true
|
||||
}
|
||||
|
||||
@@ -93,6 +93,19 @@ func keeperConsumer(p Principal) (Consumer, bool) {
|
||||
filters = append(filters, s)
|
||||
}
|
||||
}
|
||||
// And the events of the seats its module watches (its consumes naming a seat's event, such as the seat mesh-catalog's
|
||||
// manifest-accepted), on the seat's own subjects.
|
||||
for _, w := range p.Watches {
|
||||
if w.Kinded {
|
||||
continue
|
||||
}
|
||||
for _, e := range w.Emits {
|
||||
if pattern, ok := machineEvent(w, e); ok {
|
||||
e = pattern
|
||||
}
|
||||
filters = append(filters, seatSubject(w, "event", e))
|
||||
}
|
||||
}
|
||||
// And the events of the seats its module uses that are said to it by name, the operator channel's `decided` for its own
|
||||
// asks among them (design 25 § 12.3: the process wakes on the warrant, heard on its durable consumer, never on a timer).
|
||||
for _, s := range SeatTrafficOf(p.Module, nil, p.Uses, nil).Subscribe {
|
||||
|
||||
@@ -249,6 +249,7 @@ func Users(r Records) ([]Principal, error) {
|
||||
k := &out[len(out)-1]
|
||||
k.KeepsBus, k.KeepsBusState = true, BusStateNames(r.BusState, r.Bound)
|
||||
k.Consumes, k.Streams = d.Consumes, d.Streams
|
||||
k.Watches = d.Watches
|
||||
k.SaysSeatEvents = brokerSeatEvents(d.Holds)
|
||||
}
|
||||
if d.SnapshotsTheBus && len(pr.Removes) > 0 {
|
||||
|
||||
@@ -200,7 +200,7 @@ func (p *Part) MatchConsumer(stream, name string) (any, error) {
|
||||
if stream == "" || name == "" {
|
||||
return nil, errors.New("consumer-match names a stream and a consumer")
|
||||
}
|
||||
in := ReadInputs(p.Read, &p.cache)
|
||||
in := p.inputs()
|
||||
recs, _ := Records(in)
|
||||
users, err := buscompose.Users(recs)
|
||||
if err != nil {
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
|
||||
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
|
||||
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
"git.novox.be/novox/mesh-sdk/go/views"
|
||||
)
|
||||
@@ -22,7 +23,7 @@ import (
|
||||
// DLV-R1). The part reads the assigner's placement and wants and the indexer's manifests, modules and verdicts no longer
|
||||
// from their buckets, but by each owner's first-view tool, asked once at its start, then from the owner's views family
|
||||
// alone (each event one changed key with its revision), asking once more only on a gap in revisions, never on a timer.
|
||||
// The verdicts alone are followed on the indexer's manifest-accepted and manifest-refused, which carry the revision
|
||||
// The verdicts alone are followed on the seat mesh-catalog's manifest-accepted and manifest-refused, which carry the revision
|
||||
// (ADR 0622 §2), not on its views family (HearVerdict).
|
||||
// Started while an owner is down it keeps asking and never reads the owner's state; past the bound (two minutes) it
|
||||
// raises FirstViewUnanswered naming the owner and the tool, cleared by the answer.
|
||||
@@ -260,11 +261,11 @@ func (v *Views) Again(ctx context.Context, state, why string) ([]string, error)
|
||||
return asked, nil
|
||||
}
|
||||
|
||||
// VerdictEvents are the indexer's two events the part follows the verdicts on after their first view (ADR 0622 §2), as its
|
||||
// VerdictEvents are the seat mesh-catalog's two events the part follows the verdicts on after their first view (ADR 0622 §2), as its
|
||||
// manifest's consumes name them.
|
||||
var VerdictEvents = []string{IndexerModule + "." + delivery.ManifestAcceptedEvent, IndexerModule + "." + delivery.ManifestRefusedEvent}
|
||||
var VerdictEvents = []string{meshcatalog.Seat + "." + meshcatalog.EventManifestAccepted, meshcatalog.Seat + "." + meshcatalog.EventManifestRefused}
|
||||
|
||||
// HearVerdict takes one of the indexer's manifest-accepted or manifest-refused (event, its name) into the verdicts' first
|
||||
// HearVerdict takes one of the seat mesh-catalog's manifest-accepted or manifest-refused (event, its name) into the verdicts' first
|
||||
// view, as one change of the verdicts state at the revision it carries (ADR 0622 §2): the manifest's digest its key, the
|
||||
// verdict it says its value. A revision past the one after the picture's is a gap, and so is an event carrying no
|
||||
// revision (an indexer that does not say it) or one saying no manifest above the picture's revision (the empty accepted
|
||||
|
||||
@@ -61,7 +61,10 @@ const (
|
||||
)
|
||||
|
||||
// Invokes is every tool the part calls.
|
||||
var Invokes = append([]string{CallUsers, CallUserCan, CallStreams, CallRootFree, CallHandAct}, firstViewTools()...)
|
||||
var Invokes = append([]string{CallUsers, CallUserCan, CallStreams, CallRootFree, CallHandAct, CallSeatDefinitions}, firstViewTools()...)
|
||||
|
||||
// CallSeatDefinitions reads the seat list (mesh-sdk seatlist): the verb of the seat mesh-catalog.
|
||||
const CallSeatDefinitions = "seat:mesh-catalog.seat-definitions"
|
||||
|
||||
// CallHandAct records a person's act (grants_first_view_again).
|
||||
const CallHandAct = "seat:mesh-hand-acts.hand-act"
|
||||
@@ -204,6 +207,30 @@ const BusModule = "nats"
|
||||
type Manifests struct {
|
||||
mu sync.Mutex
|
||||
parsed map[string]catalogue.Manifest
|
||||
// gate says whether a manifest naming some seats may be composed yet (the seat list reader, ADR 0636 §1); nil gates none.
|
||||
gate SeatGate
|
||||
}
|
||||
|
||||
// SeatGate is what the part asks of the seat list reader before it composes a module new to it: whether a module that
|
||||
// claims or invokes the given seats may be acted on (seatlist.Reader.MayAct). Until the seat list has been read only a module
|
||||
// whose seats are all bootstrapped ones may; the rest is held as a manifest waiting for a judge is, its running build's
|
||||
// manifest composing what runs, so nothing that runs is stopped (held.go).
|
||||
type SeatGate interface {
|
||||
MayAct(seats ...string) bool
|
||||
}
|
||||
|
||||
// SeatsOf are the seats a manifest claims or invokes, which the seat list must name before the part composes the module.
|
||||
func SeatsOf(m catalogue.Manifest) []string {
|
||||
var seats []string
|
||||
for _, c := range m.Claims {
|
||||
seats = append(seats, c.Name)
|
||||
}
|
||||
for _, in := range m.Invokes {
|
||||
if seat, _, ok := catalogue.SeatInvoke(in); ok {
|
||||
seats = append(seats, seat)
|
||||
}
|
||||
}
|
||||
return seats
|
||||
}
|
||||
|
||||
func (c *Manifests) get(r Reader, digest string) (catalogue.Manifest, error) {
|
||||
@@ -263,6 +290,9 @@ func each(r Reader, state string, gaps *[]Gap) map[string][]byte {
|
||||
|
||||
// ReadInputs reads what the shadow composes from. It never fails: what it cannot read is a gap.
|
||||
func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
cache.mu.Lock()
|
||||
gate := cache.gate
|
||||
cache.mu.Unlock()
|
||||
in := Inputs{Assigned: map[string][]string{}, Manifests: map[string]catalogue.Manifest{}, Digests: map[string]string{}, Fingerprints: map[string]string{},
|
||||
NoFingerprint: map[string]string{}, Running: map[string]catalogue.Manifest{},
|
||||
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
|
||||
@@ -392,6 +422,9 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
|
||||
if err == nil {
|
||||
m, err = cache.get(r, v.Manifest)
|
||||
}
|
||||
if err == nil && gate != nil && !gate.MayAct(SeatsOf(m)...) {
|
||||
err = errors.New("it waits for the seat list: the list has not been read yet and the manifest names seats the bootstrapped list does not (novox/hq ADR 0636 §1)")
|
||||
}
|
||||
if err != nil {
|
||||
// Every manifest, assigned or not: one refused declares no seat, so its holders compose as placed nowhere.
|
||||
g := Gap{Input: "manifest of " + module, Why: err.Error()}
|
||||
|
||||
@@ -162,6 +162,9 @@ type Part struct {
|
||||
// Say says a condition to the operator: the process's; nil in the tools.
|
||||
Say func(oc.Condition) error
|
||||
Now func() time.Time
|
||||
// SeatList is the seat list reader, which gates a module whose seats the list has not named (inputs.go, SeatGate); nil
|
||||
// gates none.
|
||||
SeatList SeatGate
|
||||
|
||||
cache Manifests
|
||||
mu sync.Mutex
|
||||
@@ -279,7 +282,7 @@ func (p *Part) Compose() Result {
|
||||
}()
|
||||
// A first view's condition the condition keeper did not take is said again at every pass until it lands.
|
||||
p.Views.SayUnsaid()
|
||||
in := ReadInputs(p.Read, &p.cache)
|
||||
in := p.inputs()
|
||||
p.keepBrokers(in)
|
||||
if bound, err := p.boundStreams(); err != nil {
|
||||
res.Errors = append(res.Errors, err.Error())
|
||||
@@ -542,3 +545,11 @@ func names(us []User) []string {
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// inputs reads what the part composes from, a module whose seats the seat list has not named held back (SeatGate).
|
||||
func (p *Part) inputs() Inputs {
|
||||
p.cache.mu.Lock()
|
||||
p.cache.gate = p.SeatList
|
||||
p.cache.mu.Unlock()
|
||||
return ReadInputs(p.Read, &p.cache)
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
|
||||
"git.novox.be/novox/mesh-delivery/contracts/go/seats/meshcatalog"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
@@ -42,8 +43,27 @@ const EventAccountIssued = "mesh-vault." + buscompose.AccountIssuedEvent
|
||||
// Consumes is every event the part hears, as its manifest's consumes name them.
|
||||
// The seat mesh-broker's created, recreated and dropped are heard as every seat's event, `<seat>.<event>` on EVENTS (the
|
||||
// lead's ruling, registry milestone 13:0x), never on the bus module's own stream.
|
||||
var Consumes = append([]string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued, meshbroker.Seat + "." + BrokerCreated,
|
||||
meshbroker.Seat + "." + BrokerDropped, meshbroker.Seat + "." + BrokerRecreated}, viewsEvents()...)
|
||||
var Consumes = append(append(slices.Clone(seatEvents), seatListEvents...), viewsEvents()...)
|
||||
|
||||
// seatListEvents are the two events of the seat mesh-catalog the seat list reader follows (mesh-sdk seatlist), heard by it on the
|
||||
// module's event hub, so no handler of the hub takes them besides.
|
||||
var seatListEvents = []string{meshcatalog.Seat + "." + meshcatalog.EventSeatDefinitionChanged, meshcatalog.Seat + "." + meshcatalog.EventSeatDefinitionRemoved}
|
||||
|
||||
// seatEvents are the events of seats the part follows its passes by, as `<seat>.<event>`.
|
||||
var seatEvents = []string{EventKeeperRaised, EventKeeperCleared, EventAccountIssued, meshbroker.Seat + "." + BrokerCreated,
|
||||
meshbroker.Seat + "." + BrokerDropped, meshbroker.Seat + "." + BrokerRecreated}
|
||||
|
||||
// SeatEventSubjects are the subjects of the seats' events the part follows its passes by besides the verdicts' and the
|
||||
// views': the keeper's condition events, the vault's account-issued and the seat mesh-broker's, each `<seat>.<event>` of
|
||||
// Consumes on the seat's own subject.
|
||||
func SeatEventSubjects() []string {
|
||||
var out []string
|
||||
for _, c := range seatEvents {
|
||||
seat, event, _ := strings.Cut(c, ".")
|
||||
out = append(out, "mesh.seat."+seat+".event."+event)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// viewsEvents are the owners' events the part follows its first views by: `<owner>.views.<state>`, but the verdicts by
|
||||
// the indexer's manifest-accepted and manifest-refused (VerdictEvents, ADR 0622 §2).
|
||||
|
||||
@@ -26,11 +26,14 @@ const (
|
||||
// ToolFirstViews and ToolFirstViewAgain read and ask again the part's first views (firstviews.go).
|
||||
ToolFirstViews = "grants_first_views"
|
||||
ToolFirstViewAgain = "grants_first_view_again"
|
||||
// ToolSeatList and ToolSeatListAgain read and read again the seat list the part composes from (mesh-sdk seatlist.Tools).
|
||||
ToolSeatList = "grants_seat_list"
|
||||
ToolSeatListAgain = "grants_seat_list_again"
|
||||
)
|
||||
|
||||
// Tools is every tool, in the manifest's order.
|
||||
var Tools = []string{ToolGrants, ToolMembership, ToolUserList, ToolComposeNow, ToolAssert, ToolAccounts, ToolConsumerMatch,
|
||||
ToolAcceptHashes, ToolFirstViews, ToolFirstViewAgain}
|
||||
ToolAcceptHashes, ToolFirstViews, ToolFirstViewAgain, ToolSeatList, ToolSeatListAgain}
|
||||
|
||||
// WritesGrants is what grants says of the part's own grants: written once the list the bus loaded carries a build's
|
||||
// accounts, and the one key the assigner reads (ADR 0571 clause 9, which changes ADR 0547 clause 5).
|
||||
@@ -105,7 +108,7 @@ func (p *Part) MembershipOf(node, module string) (any, error) {
|
||||
if node == "" || module == "" {
|
||||
return nil, errors.New("membership names a node and a module, or neither for every membership held back")
|
||||
}
|
||||
in := ReadInputs(p.Read, &p.cache)
|
||||
in := p.inputs()
|
||||
recs, left := Records(in)
|
||||
notFree := WithRootFree(&recs, p.Call)
|
||||
m, ok := Memberships(recs)[node+"."+module]
|
||||
@@ -277,7 +280,7 @@ func (p *Part) AssertOne(module string) (any, error) {
|
||||
held, heldErr := HeldStreams(p.Call)
|
||||
var nodes []string
|
||||
if module == buscompose.EngineModule {
|
||||
nodes = ReadInputs(p.Read, &p.cache).Nodes // every node's bucket of compositions (ADR 0593 §6)
|
||||
nodes = p.inputs().Nodes // every node's bucket of compositions (ADR 0593 §6)
|
||||
}
|
||||
a := Assert(module, v.Manifest, m, nodes, held, heldErr)
|
||||
out := map[string]any{"assertion": a}
|
||||
|
||||
@@ -21,8 +21,10 @@
|
||||
"mesh-delivery-assigner.views.wants",
|
||||
"mesh-delivery-indexer.views.manifests",
|
||||
"mesh-delivery-indexer.views.modules",
|
||||
"mesh-delivery-indexer.manifest-accepted",
|
||||
"mesh-delivery-indexer.manifest-refused"
|
||||
"mesh-catalog.manifest-accepted",
|
||||
"mesh-catalog.manifest-refused",
|
||||
"mesh-catalog.seat-definition-changed",
|
||||
"mesh-catalog.seat-definition-removed"
|
||||
],
|
||||
"uses": [
|
||||
"operator-channel"
|
||||
@@ -37,6 +39,7 @@
|
||||
"mesh-delivery-indexer.manifests",
|
||||
"mesh-delivery-indexer.modules",
|
||||
"mesh-delivery-indexer.verdicts",
|
||||
"seat:mesh-catalog.seat-definitions",
|
||||
"seat:mesh-hand-acts.hand-act"
|
||||
],
|
||||
"emits": [
|
||||
@@ -56,7 +59,9 @@
|
||||
"grants_consumer_match",
|
||||
"grants_accept_hashes",
|
||||
"grants_first_views",
|
||||
"grants_first_view_again"
|
||||
"grants_first_view_again",
|
||||
"grants_seat_list",
|
||||
"grants_seat_list_again"
|
||||
],
|
||||
"state": [
|
||||
{
|
||||
|
||||
+5
-3
@@ -11,6 +11,8 @@ import (
|
||||
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
|
||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
)
|
||||
|
||||
// Version is the version of a node's value this package writes and the only one it reads.
|
||||
@@ -595,16 +597,16 @@ func OfManifests(manifests map[string]catalogue.Manifest) Graph {
|
||||
// (servesOf answers the verbs of a module's build at a fingerprint, ServesOf of its manifest, and whether it was read).
|
||||
// A build whose manifest cannot be read counts what fallback answers of its module (the graph's Serves, so a build read
|
||||
// late never holds its callers for good, or nothing), and is named in unread, sorted, so its reader says so.
|
||||
func Served(haves map[string]map[string]delivery.Have, servesOf func(module, fp string) ([]string, bool),
|
||||
func Served(haves map[string]map[string]nodeengine.HaveByNodeModuleValue, servesOf func(module, fp string) ([]string, bool),
|
||||
fallback func(module string) []string) (served func(invoke string) bool, unread []string) {
|
||||
set := map[string]bool{}
|
||||
for _, node := range slices.Sorted(maps.Keys(haves)) {
|
||||
for _, module := range slices.Sorted(maps.Keys(haves[node])) {
|
||||
h := haves[node][module]
|
||||
if h.Phase != delivery.PhaseRunning || h.Health != delivery.HealthOK {
|
||||
if h.Phase != nodeengine.HaveByNodeModuleValuePhaseRunning || h.Health != nodeengine.HaveByNodeModuleValueHealthOk {
|
||||
continue
|
||||
}
|
||||
serves, ok := servesOf(module, h.FP)
|
||||
serves, ok := servesOf(module, h.Fp)
|
||||
if !ok {
|
||||
unread = append(unread, module)
|
||||
if fallback != nil {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
nodeengine "git.novox.be/novox/mesh-contracts/go/seats/nodeengine"
|
||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
"git.novox.be/novox/mesh-sdk/go/delivery"
|
||||
)
|
||||
@@ -16,6 +17,7 @@ import (
|
||||
func TestGraphEdges(t *testing.T) {
|
||||
t.Run("a node's value", nodeValues)
|
||||
t.Run("a reader of the seat list waits for its holder", readersOfTheSeatList)
|
||||
t.Run("a verb is served by a running healthy have of the node-engine contract", servedByHaves)
|
||||
fp := "src2:" + strings.Repeat("b", 64)
|
||||
node := func(repository, path string, edit func(*Node)) Node {
|
||||
n := Node{FP: fp, Repository: repository, Path: path}
|
||||
@@ -291,3 +293,22 @@ func readersOfTheSeatList(t *testing.T) {
|
||||
t.Errorf("the node-agent waits for %v, want nothing: the bootstrap carries it", got)
|
||||
}
|
||||
}
|
||||
|
||||
// servedByHaves: Served counts the node-engine contract's haves, running and healthy only; an unread build is named.
|
||||
func servedByHaves(t *testing.T) {
|
||||
running := nodeengine.HaveByNodeModuleValue{Fp: "a", Phase: nodeengine.HaveByNodeModuleValuePhaseRunning, Health: nodeengine.HaveByNodeModuleValueHealthOk}
|
||||
failed := nodeengine.HaveByNodeModuleValue{Fp: "b", Phase: nodeengine.HaveByNodeModuleValuePhaseRunning, Health: nodeengine.HaveByNodeModuleValueHealthFailed}
|
||||
haves := map[string]map[string]nodeengine.HaveByNodeModuleValue{"n1": {"m1": running, "m2": failed, "m3": running}}
|
||||
served, unread := Served(haves, func(module, fp string) ([]string, bool) {
|
||||
if module == "m3" {
|
||||
return nil, false
|
||||
}
|
||||
return []string{module + ".verb"}, true
|
||||
}, nil)
|
||||
if !served("m1.verb") || served("m2.verb") {
|
||||
t.Fatalf("a running healthy have serves, a failed one does not: m1 %v, m2 %v", served("m1.verb"), served("m2.verb"))
|
||||
}
|
||||
if !reflect.DeepEqual(unread, []string{"m3"}) {
|
||||
t.Fatalf("unread = %v, want [m3]", unread)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user