From cf5590c7599d3ee3b61c3aa66c88a939ecb41130 Mon Sep 17 00:00:00 2001 From: jochen Date: Wed, 11 Nov 2026 22:37:57 +0100 Subject: [PATCH] =?UTF-8?q?bus5=20K27=20(=C2=A7681):=20mesh-broker's=20sna?= =?UTF-8?q?pshot-get,=20snapshot-restore,=20snapshot-restore-cancel,=20sna?= =?UTF-8?q?pshot-remove=20and=20snapshot-restored;=20the=20format's=20verb?= =?UTF-8?q?=20word=20chunks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Builds: design 47 G §681, §691, A-44 --- format/seat-definition.schema.json | 4 + go/seats/meshbroker/contract.go | 156 ++++++++++++++-- go/seats/meshbroker/contract_test.go | 34 ++++ seats/mesh-broker/fixtures/events.json | 11 ++ seats/mesh-broker/fixtures/verbs.json | 97 ++++++++++ seats/mesh-broker/seat.json | 244 +++++++++++++++++++++++++ ts/seats/meshbroker/index.d.ts | 105 +++++++++++ ts/seats/meshbroker/index.js | 13 ++ 8 files changed, 645 insertions(+), 19 deletions(-) diff --git a/format/seat-definition.schema.json b/format/seat-definition.schema.json index 504c2d5..cc9aa31 100644 --- a/format/seat-definition.schema.json +++ b/format/seat-definition.schema.json @@ -354,6 +354,10 @@ "by-caller": { "type": "boolean", "description": "the holder's process answers the verb itself and tells callers apart by the account the bus proved (ADR 0259 3; design 47 G 554, 621, 656)" + }, + "chunks": { + "type": "boolean", + "description": "the holder's process sends the answer's file in chunks on mesh.seat..chunk..., each acknowledged by the caller on mesh.seat..ack...; only with by-caller (design 47 G 681)" } } }, diff --git a/go/seats/meshbroker/contract.go b/go/seats/meshbroker/contract.go index 50f6366..6dc0efb 100644 --- a/go/seats/meshbroker/contract.go +++ b/go/seats/meshbroker/contract.go @@ -3,40 +3,47 @@ // Package meshbroker is the contract of the seat "mesh-broker", defined whole (seats/mesh-broker/seat.json): The mesh's bus, and the bus state modules ask of it in requires (design 25 § 12). create, apply, recreate and drop are granted to the bus module's own credential alone. package meshbroker +import "time" + // Seat is the seat's name. const Seat = "mesh-broker" const ( - VerbList = "list" - VerbShow = "show" - VerbCheck = "check" - VerbOrphans = "orphans" - VerbCreate = "create" - VerbApply = "apply" - VerbRecreate = "recreate" - VerbDrop = "drop" + VerbList = "list" + VerbShow = "show" + VerbCheck = "check" + VerbOrphans = "orphans" + VerbCreate = "create" + VerbApply = "apply" + VerbRecreate = "recreate" + VerbDrop = "drop" + VerbSnapshotGet = "snapshot-get" + VerbSnapshotRestore = "snapshot-restore" + VerbSnapshotRestoreCancel = "snapshot-restore-cancel" + VerbSnapshotRemove = "snapshot-remove" ) const ( - EventCreated = "created" - EventRefused = "refused" - EventDrifted = "drifted" - EventDriftCleared = "drift-cleared" - EventFull = "full" - EventFullCleared = "full-cleared" - EventOrphaned = "orphaned" - EventRecreated = "recreated" - EventDropped = "dropped" + EventCreated = "created" + EventRefused = "refused" + EventDrifted = "drifted" + EventDriftCleared = "drift-cleared" + EventFull = "full" + EventFullCleared = "full-cleared" + EventOrphaned = "orphaned" + EventRecreated = "recreated" + EventDropped = "dropped" + EventSnapshotRestored = "snapshot-restored" ) // Verbs is every verb the seat serves, in its definition's order: a new copy each call. func Verbs() []string { - return []string{VerbList, VerbShow, VerbCheck, VerbOrphans, VerbCreate, VerbApply, VerbRecreate, VerbDrop} + return []string{VerbList, VerbShow, VerbCheck, VerbOrphans, VerbCreate, VerbApply, VerbRecreate, VerbDrop, VerbSnapshotGet, VerbSnapshotRestore, VerbSnapshotRestoreCancel, VerbSnapshotRemove} } // Events is every event published on the seat's subject: a new copy each call. func Events() []string { - return []string{EventCreated, EventRefused, EventDrifted, EventDriftCleared, EventFull, EventFullCleared, EventOrphaned, EventRecreated, EventDropped} + return []string{EventCreated, EventRefused, EventDrifted, EventDriftCleared, EventFull, EventFullCleared, EventOrphaned, EventRecreated, EventDropped, EventSnapshotRestored} } // Family is where the seat's events are kept: the shared events stream, filtered to the seat's subjects. @@ -72,6 +79,17 @@ const SubjectRecreated = "mesh.seat.mesh-broker.event.recreated" // SubjectDropped is where dropped is published. const SubjectDropped = "mesh.seat.mesh-broker.event.dropped" +// StreamSnapshotRestored is the stream snapshot-restored is kept in. +const StreamSnapshotRestored = "SEAT_mesh-broker_snapshot-restored" + +// SubjectSnapshotRestored is where snapshot-restored is published, on mesh.seat.mesh-broker.event.snapshot-restored... +func SubjectSnapshotRestored(request, act string) string { + return "mesh.seat.mesh-broker.event.snapshot-restored." + request + "." + act +} + +// SubjectSnapshotRestoredAll hears every snapshot-restored. +const SubjectSnapshotRestoredAll = "mesh.seat.mesh-broker.event.snapshot-restored.*.*" + // Binding is the binding a module receives in its membership for one entry (ADR 0595 §1, §7): the names on the bus of the entry's objects, one form in the events, in list, show and orphans, and in the membership. type Binding struct { // a key-value bucket's name @@ -364,6 +382,91 @@ type DropAnswer struct { Snapshot string `json:"snapshot,omitempty"` } +// SnapshotGetInput is what snapshot-get takes. +type SnapshotGetInput struct { + // 128 random bits the caller chooses: the subject token of this send's chunks and acknowledgements + Call string `json:"call"` + // the snapshot's request: the id of the request the keeper took it for, in its state requests + Request string `json:"request"` +} + +// SnapshotGetAnswer is what snapshot-get answers. +type SnapshotGetAnswer struct { + // when the snapshot was taken, RFC 3339 + At *time.Time `json:"at,omitempty"` + // how many chunks are sent; 0 when the caller is granted no chunk subjects + Chunks int64 `json:"chunks,omitempty"` + // sha256: of the file's bytes + Digest string `json:"digest,omitempty"` + // why not: snapshot damaged, snapshot missing, snapshot protected or snapshot taken before milestone G; set alone + Error string `json:"error,omitempty"` + // the entry's local name + Local string `json:"local,omitempty"` + Module string `json:"module,omitempty"` + Request string `json:"request,omitempty"` + Size int64 `json:"size,omitempty"` + // the stream's name on the bus + Stream string `json:"stream,omitempty"` +} + +// SnapshotRestoreInput is what snapshot-restore takes. +type SnapshotRestoreInput struct { + // the snapshot's request: the id of the request the keeper took it for, in its state requests + Request string `json:"request"` + // check the file against its digest and restore nothing + Verify bool `json:"verify,omitempty"` + // why, which the hand-act log keeps + Why string `json:"why"` +} + +// SnapshotRestoreAnswer is what snapshot-restore answers. +type SnapshotRestoreAnswer struct { + // accepted: the act's id + Accepted string `json:"accepted,omitempty"` + // busy: the id of the act that runs; it keeps no queue + Busy string `json:"busy,omitempty"` + // why not; set alone + Error string `json:"error,omitempty"` +} + +// SnapshotRestoreCancelInput is what snapshot-restore-cancel takes. +type SnapshotRestoreCancelInput struct { + // the restore's act id + Act string `json:"act"` + // why, which the hand-act log keeps + Why string `json:"why"` +} + +// SnapshotRestoreCancelAnswer is what snapshot-restore-cancel answers. +type SnapshotRestoreCancelAnswer struct { + // accepted: the act's id + Accepted string `json:"accepted,omitempty"` + // busy: the id of the act that runs; it keeps no queue + Busy string `json:"busy,omitempty"` + // why not; set alone + Error string `json:"error,omitempty"` +} + +// SnapshotRemoveInput is what snapshot-remove takes. +type SnapshotRemoveInput struct { + // true: every snapshot past snapshot-keep-days + Expired bool `json:"expired,omitempty"` + // the snapshot's request; or expired true, not both + Request string `json:"request,omitempty"` + // why, which the hand-act log keeps + Why string `json:"why"` +} + +// SnapshotRemoveAnswer is what snapshot-remove answers. +type SnapshotRemoveAnswer struct { + // accepted: the act's id + Accepted string `json:"accepted,omitempty"` + // busy: the id of the act that runs; it keeps no queue + Busy string `json:"busy,omitempty"` + // why not; set alone + Error string `json:"error,omitempty"` +} + // CreatedEvent is the body of the event created. type CreatedEvent = Said @@ -391,5 +494,20 @@ type RecreatedEvent = Said // DroppedEvent is the body of the event dropped. type DroppedEvent = Said +// SnapshotRestoredEvent is the body of the event snapshot-restored. +type SnapshotRestoredEvent struct { + Act string `json:"act"` + // when failed + Error string `json:"error,omitempty"` + Outcome string `json:"outcome"` + Request string `json:"request"` +} + +// The values of SnapshotRestoredEvent's outcome. +const ( + SnapshotRestoredEventOutcomeDone = "done" + SnapshotRestoredEventOutcomeFailed = "failed" +) + // BusStateForm is the form of the provision bus-state. type BusStateForm = Binding diff --git a/go/seats/meshbroker/contract_test.go b/go/seats/meshbroker/contract_test.go index 59c281f..c181a58 100644 --- a/go/seats/meshbroker/contract_test.go +++ b/go/seats/meshbroker/contract_test.go @@ -37,6 +37,8 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v RecreatedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.recreated$")}, {name: "dropped on the seat's subject", subject: "mesh.seat.mesh-broker.event.dropped", given: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", wire: "{\"asked\":{\"max-mib\":64},\"bus-name\":\"mesh-knowledge_ledger\",\"fill\":{\"bytes\":2048,\"cap\":67108864,\"count\":10},\"held\":{\"max-mib\":64},\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"storage-type\":\"ledger\",\"template\":\"ledger\",\"why\":\"the bus refused it\"}", decode: func(b []byte) (any, error) { var v DroppedEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.dropped$")}, + {name: "snapshot-restored is said under its request and act", subject: "mesh.seat.mesh-broker.event.snapshot-restored.r-1.a-20261111t100000", given: "{\"act\":\"a-20261111t100000\",\"error\":\"names live\",\"outcome\":\"failed\",\"request\":\"r-1\"}", wire: "{\"act\":\"a-20261111t100000\",\"error\":\"names live\",\"outcome\":\"failed\",\"request\":\"r-1\"}", + decode: func(b []byte) (any, error) { var v SnapshotRestoredEvent; err := json.Unmarshal(b, &v); return v, err }, subjectIs: regexp.MustCompile("^mesh\\.seat\\.mesh-broker\\.event\\.snapshot-restored\\.[^.*> ]+\\.[^.*> ]+$")}, {name: "create one entry", subject: "", given: "{\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"options\":{\"max-mib\":64},\"storage-type\":\"ledger\",\"template\":\"ledger\"}", wire: "{\"local-name\":\"ledger\",\"module\":\"mesh-knowledge\",\"options\":{\"max-mib\":64},\"storage-type\":\"ledger\",\"template\":\"ledger\"}", decode: func(b []byte) (any, error) { var v CreateInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "drop on the operator's warrant", subject: "", given: "{\"local-name\":\"old\",\"module\":\"mesh-knowledge\",\"warrant\":\"w-1\",\"why\":\"the operator approved the drop\"}", wire: "{\"local-name\":\"old\",\"module\":\"mesh-knowledge\",\"warrant\":\"w-1\",\"why\":\"the operator approved the drop\"}", @@ -49,6 +51,38 @@ func TestFixtures(t *testing.T) { decode: func(b []byte) (any, error) { var v ListAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, {name: "list answers a sum with no limit, which is unlimited", subject: "", given: "{\"caps\":{\"sum-mib\":64},\"entries\":[]}", wire: "{\"caps\":{\"sum-mib\":64},\"entries\":[]}", decode: func(b []byte) (any, error) { var v ListAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "snapshot-get asks a snapshot by its request", subject: "", given: "{\"call\":\"0123456789abcdef0123456789abcdef\",\"request\":\"r-1\"}", wire: "{\"call\":\"0123456789abcdef0123456789abcdef\",\"request\":\"r-1\"}", + decode: func(b []byte) (any, error) { var v SnapshotGetInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "snapshot-get answers the fields and the chunks", subject: "", given: "{\"at\":\"2026-11-11T10:00:00Z\",\"chunks\":2,\"digest\":\"sha256:00\",\"local\":\"ledger\",\"module\":\"mesh-knowledge\",\"request\":\"r-1\",\"size\":1048576,\"stream\":\"mesh-knowledge_ledger\"}", wire: "{\"at\":\"2026-11-11T10:00:00Z\",\"chunks\":2,\"digest\":\"sha256:00\",\"local\":\"ledger\",\"module\":\"mesh-knowledge\",\"request\":\"r-1\",\"size\":1048576,\"stream\":\"mesh-knowledge_ledger\"}", + decode: func(b []byte) (any, error) { var v SnapshotGetAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "snapshot-get answers a protected snapshot refused", subject: "", given: "{\"error\":\"snapshot protected\"}", wire: "{\"error\":\"snapshot protected\"}", + decode: func(b []byte) (any, error) { var v SnapshotGetAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "snapshot-restore asks a verify", subject: "", given: "{\"request\":\"r-1\",\"verify\":true,\"why\":\"the file is read before the live data is set aside\"}", wire: "{\"request\":\"r-1\",\"verify\":true,\"why\":\"the file is read before the live data is set aside\"}", + decode: func(b []byte) (any, error) { var v SnapshotRestoreInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "snapshot-restore answers accepted with the act", subject: "", given: "{\"accepted\":\"a-20261111t100000\"}", wire: "{\"accepted\":\"a-20261111t100000\"}", + decode: func(b []byte) (any, error) { var v SnapshotRestoreAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "snapshot-restore answers busy with the act that runs", subject: "", given: "{\"busy\":\"a-20261111t095900\"}", wire: "{\"busy\":\"a-20261111t095900\"}", + decode: func(b []byte) (any, error) { var v SnapshotRestoreAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "snapshot-restore answers names live", subject: "", given: "{\"error\":\"names live\"}", wire: "{\"error\":\"names live\"}", + decode: func(b []byte) (any, error) { var v SnapshotRestoreAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "snapshot-restore-cancel names the act", subject: "", given: "{\"act\":\"a-20261111t100000\",\"why\":\"the restore passed its bound\"}", wire: "{\"act\":\"a-20261111t100000\",\"why\":\"the restore passed its bound\"}", + decode: func(b []byte) (any, error) { + var v SnapshotRestoreCancelInput + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "snapshot-restore-cancel answers accepted", subject: "", given: "{\"accepted\":\"a-20261111t100000\"}", wire: "{\"accepted\":\"a-20261111t100000\"}", + decode: func(b []byte) (any, error) { + var v SnapshotRestoreCancelAnswer + err := json.Unmarshal(b, &v) + return v, err + }, subjectIs: nil}, + {name: "snapshot-remove names one snapshot", subject: "", given: "{\"request\":\"r-1\",\"why\":\"the data is restored\"}", wire: "{\"request\":\"r-1\",\"why\":\"the data is restored\"}", + decode: func(b []byte) (any, error) { var v SnapshotRemoveInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "snapshot-remove names every expired one", subject: "", given: "{\"expired\":true,\"why\":\"the days are past\"}", wire: "{\"expired\":true,\"why\":\"the days are past\"}", + decode: func(b []byte) (any, error) { var v SnapshotRemoveInput; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, + {name: "snapshot-remove answers accepted", subject: "", given: "{\"accepted\":\"a-20261111t100000\"}", wire: "{\"accepted\":\"a-20261111t100000\"}", + decode: func(b []byte) (any, error) { var v SnapshotRemoveAnswer; err := json.Unmarshal(b, &v); return v, err }, subjectIs: nil}, } for _, c := range cases { v, err := c.decode([]byte(c.given)) diff --git a/seats/mesh-broker/fixtures/events.json b/seats/mesh-broker/fixtures/events.json index b9f6a8f..652e2d6 100644 --- a/seats/mesh-broker/fixtures/events.json +++ b/seats/mesh-broker/fixtures/events.json @@ -265,5 +265,16 @@ }, "why": "the bus refused it" } + }, + { + "name": "snapshot-restored is said under its request and act", + "of": "event:snapshot-restored", + "subject": "mesh.seat.mesh-broker.event.snapshot-restored.r-1.a-20261111t100000", + "given": { + "request": "r-1", + "act": "a-20261111t100000", + "outcome": "failed", + "error": "names live" + } } ] diff --git a/seats/mesh-broker/fixtures/verbs.json b/seats/mesh-broker/fixtures/verbs.json index 5cf98ac..6e8f330 100644 --- a/seats/mesh-broker/fixtures/verbs.json +++ b/seats/mesh-broker/fixtures/verbs.json @@ -136,5 +136,102 @@ "sum-mib": 64 } } + }, + { + "name": "snapshot-get asks a snapshot by its request", + "of": "verb:snapshot-get.input", + "given": { + "request": "r-1", + "call": "0123456789abcdef0123456789abcdef" + } + }, + { + "name": "snapshot-get answers the fields and the chunks", + "of": "verb:snapshot-get.answer", + "given": { + "request": "r-1", + "module": "mesh-knowledge", + "local": "ledger", + "stream": "mesh-knowledge_ledger", + "size": 1048576, + "digest": "sha256:00", + "at": "2026-11-11T10:00:00Z", + "chunks": 2 + } + }, + { + "name": "snapshot-get answers a protected snapshot refused", + "of": "verb:snapshot-get.answer", + "given": { + "error": "snapshot protected" + } + }, + { + "name": "snapshot-restore asks a verify", + "of": "verb:snapshot-restore.input", + "given": { + "request": "r-1", + "verify": true, + "why": "the file is read before the live data is set aside" + } + }, + { + "name": "snapshot-restore answers accepted with the act", + "of": "verb:snapshot-restore.answer", + "given": { + "accepted": "a-20261111t100000" + } + }, + { + "name": "snapshot-restore answers busy with the act that runs", + "of": "verb:snapshot-restore.answer", + "given": { + "busy": "a-20261111t095900" + } + }, + { + "name": "snapshot-restore answers names live", + "of": "verb:snapshot-restore.answer", + "given": { + "error": "names live" + } + }, + { + "name": "snapshot-restore-cancel names the act", + "of": "verb:snapshot-restore-cancel.input", + "given": { + "act": "a-20261111t100000", + "why": "the restore passed its bound" + } + }, + { + "name": "snapshot-restore-cancel answers accepted", + "of": "verb:snapshot-restore-cancel.answer", + "given": { + "accepted": "a-20261111t100000" + } + }, + { + "name": "snapshot-remove names one snapshot", + "of": "verb:snapshot-remove.input", + "given": { + "request": "r-1", + "why": "the data is restored" + } + }, + { + "name": "snapshot-remove names every expired one", + "of": "verb:snapshot-remove.input", + "given": { + "expired": true, + "why": "the days are past" + } + }, + { + "name": "snapshot-remove answers accepted", + "of": "verb:snapshot-remove.answer", + "given": { + "accepted": "a-20261111t100000" + } } ] diff --git a/seats/mesh-broker/seat.json b/seats/mesh-broker/seat.json index fc42bdf..4fab316 100644 --- a/seats/mesh-broker/seat.json +++ b/seats/mesh-broker/seat.json @@ -209,6 +209,214 @@ }, "optional": true, "class": "warrant" + }, + { + "name": "snapshot-get", + "description": "Send the stream snapshot the bus module took before a drop or recreate: its fields, then the file in chunks of at most 512 KiB on the chunk subject of the call, each acknowledged by the caller on the ack subject within 30 seconds. A caller granted no chunk subjects is answered the fields alone, with chunks 0. Refuses snapshot damaged, snapshot missing, snapshot protected and snapshot taken before milestone G (design 47 G 681).", + "by-caller": true, + "chunks": true, + "class": "read", + "input": { + "type": "object", + "properties": { + "request": { + "type": "string", + "description": "the snapshot's request: the id of the request the keeper took it for, in its state requests", + "minLength": 1 + }, + "call": { + "type": "string", + "description": "128 random bits the caller chooses: the subject token of this send's chunks and acknowledgements", + "minLength": 1 + } + }, + "required": [ + "request", + "call" + ], + "additionalProperties": false + }, + "answer": { + "type": "object", + "properties": { + "request": { + "type": "string" + }, + "module": { + "type": "string" + }, + "local": { + "type": "string", + "description": "the entry's local name" + }, + "stream": { + "type": "string", + "description": "the stream's name on the bus" + }, + "size": { + "type": "integer", + "minimum": 0 + }, + "digest": { + "type": "string", + "description": "sha256: of the file's bytes" + }, + "at": { + "type": "string", + "format": "date-time", + "description": "when the snapshot was taken, RFC 3339" + }, + "chunks": { + "type": "integer", + "minimum": 0, + "description": "how many chunks are sent; 0 when the caller is granted no chunk subjects" + }, + "error": { + "type": "string", + "description": "why not: snapshot damaged, snapshot missing, snapshot protected or snapshot taken before milestone G; set alone" + } + } + }, + "optional": true + }, + { + "name": "snapshot-restore", + "description": "Restore in place the snapshot of a protected stream the bus module took before a drop or recreate, under the stream's own name: with verify true it only checks the file against its recorded digest and restores nothing; it refuses names live while a live object of the stream's name exists. Answers accepted, busy or error at once; the outcome comes on the event snapshot-restored (design 47 G 681).", + "by-caller": true, + "class": "act", + "input": { + "type": "object", + "properties": { + "request": { + "type": "string", + "description": "the snapshot's request: the id of the request the keeper took it for, in its state requests", + "minLength": 1 + }, + "verify": { + "type": "boolean", + "description": "check the file against its digest and restore nothing" + }, + "why": { + "type": "string", + "description": "why, which the hand-act log keeps" + } + }, + "required": [ + "request", + "why" + ], + "additionalProperties": false + }, + "answer": { + "type": "object", + "description": "the act accepted, or busy with the act that runs, or the error; exactly one of the three is set (design 47 G 681, 691, A-44)", + "properties": { + "accepted": { + "type": "string", + "description": "accepted: the act's id" + }, + "busy": { + "type": "string", + "description": "busy: the id of the act that runs; it keeps no queue" + }, + "error": { + "type": "string", + "description": "why not; set alone" + } + } + }, + "optional": true + }, + { + "name": "snapshot-restore-cancel", + "description": "Cancel a running restore: it removes the keeper's own partial stream, says snapshot-restored with outcome failed under that act and answers accepted, or answers error when that act has ended (design 47 G 681).", + "by-caller": true, + "class": "act", + "input": { + "type": "object", + "properties": { + "act": { + "type": "string", + "description": "the restore's act id", + "minLength": 1 + }, + "why": { + "type": "string", + "description": "why, which the hand-act log keeps" + } + }, + "required": [ + "act", + "why" + ], + "additionalProperties": false + }, + "answer": { + "type": "object", + "description": "the act cancelled, or busy with the act that runs, or the error; exactly one of the three is set (design 47 G 681, 691, A-44)", + "properties": { + "accepted": { + "type": "string", + "description": "accepted: the act's id" + }, + "busy": { + "type": "string", + "description": "busy: the id of the act that runs; it keeps no queue" + }, + "error": { + "type": "string", + "description": "why not; set alone" + } + } + }, + "optional": true + }, + { + "name": "snapshot-remove", + "description": "Remove one snapshot and its record now, or every expired one, written in the hand-act log first; a snapshot of before milestone G only by request. Answers accepted, busy or error (design 47 G 681), A-44.", + "by-caller": true, + "class": "act", + "input": { + "type": "object", + "properties": { + "request": { + "type": "string", + "description": "the snapshot's request; or expired true, not both", + "minLength": 1 + }, + "expired": { + "type": "boolean", + "description": "true: every snapshot past snapshot-keep-days" + }, + "why": { + "type": "string", + "description": "why, which the hand-act log keeps" + } + }, + "required": [ + "why" + ], + "additionalProperties": false + }, + "answer": { + "type": "object", + "description": "the act accepted, or busy with the act that runs, or the error; exactly one of the three is set (design 47 G 681, 691, A-44)", + "properties": { + "accepted": { + "type": "string", + "description": "accepted: the act's id" + }, + "busy": { + "type": "string", + "description": "busy: the id of the act that runs; it keeps no queue" + }, + "error": { + "type": "string", + "description": "why not; set alone" + } + } + }, + "optional": true } ], "events": [ @@ -265,6 +473,42 @@ "body": { "$ref": "#/defs/Said" } + }, + { + "name": "snapshot-restored", + "subject": "snapshot-restored..", + "keep": { + "max-age-days": 90, + "last-per-subject": true + }, + "body": { + "type": "object", + "description": "how one restore in place of a protected snapshot ended, under the act it was answered accepted with, so a retried restore is told apart from an earlier one (design 47 G 681)", + "properties": { + "request": { + "type": "string" + }, + "act": { + "type": "string" + }, + "outcome": { + "type": "string", + "enum": [ + "done", + "failed" + ] + }, + "error": { + "type": "string", + "description": "when failed" + } + }, + "required": [ + "request", + "act", + "outcome" + ] + } } ], "family": { diff --git a/ts/seats/meshbroker/index.d.ts b/ts/seats/meshbroker/index.d.ts index 68e5e83..5edf309 100644 --- a/ts/seats/meshbroker/index.d.ts +++ b/ts/seats/meshbroker/index.d.ts @@ -13,6 +13,10 @@ export declare const verbs: { readonly apply: "apply"; readonly recreate: "recreate"; readonly drop: "drop"; + readonly snapshotGet: "snapshot-get"; + readonly snapshotRestore: "snapshot-restore"; + readonly snapshotRestoreCancel: "snapshot-restore-cancel"; + readonly snapshotRemove: "snapshot-remove"; }; export declare const events: { @@ -25,6 +29,7 @@ export declare const events: { readonly orphaned: "orphaned"; readonly recreated: "recreated"; readonly dropped: "dropped"; + readonly snapshotRestored: "snapshot-restored"; }; export declare const family: { readonly name: "mesh-broker.events"; readonly maxAgeDays: 7 }; @@ -47,6 +52,12 @@ export declare const recreatedSubject: "mesh.seat.mesh-broker.event.recreated"; export declare const droppedSubject: "mesh.seat.mesh-broker.event.dropped"; +export declare const snapshotRestoredStream: "SEAT_mesh-broker_snapshot-restored"; + +export declare function snapshotRestoredSubject(request: string, act: string): string; + +export declare const snapshotRestoredSubjectAll: "mesh.seat.mesh-broker.event.snapshot-restored.*.*"; + /** the binding a module receives in its membership for one entry (ADR 0595 §1, §7): the names on the bus of the entry's objects, one form in the events, in list, show and orphans, and in the membership */ export interface Binding { /** a key-value bucket's name */ @@ -268,6 +279,91 @@ export interface DropAnswer { "snapshot"?: string; } +/** is what snapshot-get takes. */ +export interface SnapshotGetInput { + /** 128 random bits the caller chooses: the subject token of this send's chunks and acknowledgements */ + "call": string; + /** the snapshot's request: the id of the request the keeper took it for, in its state requests */ + "request": string; +} + +/** is what snapshot-get answers. */ +export interface SnapshotGetAnswer { + /** when the snapshot was taken, RFC 3339 */ + "at"?: string; + /** how many chunks are sent; 0 when the caller is granted no chunk subjects */ + "chunks"?: number; + /** sha256: of the file's bytes */ + "digest"?: string; + /** why not: snapshot damaged, snapshot missing, snapshot protected or snapshot taken before milestone G; set alone */ + "error"?: string; + /** the entry's local name */ + "local"?: string; + "module"?: string; + "request"?: string; + "size"?: number; + /** the stream's name on the bus */ + "stream"?: string; +} + +/** is what snapshot-restore takes. */ +export interface SnapshotRestoreInput { + /** the snapshot's request: the id of the request the keeper took it for, in its state requests */ + "request": string; + /** check the file against its digest and restore nothing */ + "verify"?: boolean; + /** why, which the hand-act log keeps */ + "why": string; +} + +/** is what snapshot-restore answers. */ +export interface SnapshotRestoreAnswer { + /** accepted: the act's id */ + "accepted"?: string; + /** busy: the id of the act that runs; it keeps no queue */ + "busy"?: string; + /** why not; set alone */ + "error"?: string; +} + +/** is what snapshot-restore-cancel takes. */ +export interface SnapshotRestoreCancelInput { + /** the restore's act id */ + "act": string; + /** why, which the hand-act log keeps */ + "why": string; +} + +/** is what snapshot-restore-cancel answers. */ +export interface SnapshotRestoreCancelAnswer { + /** accepted: the act's id */ + "accepted"?: string; + /** busy: the id of the act that runs; it keeps no queue */ + "busy"?: string; + /** why not; set alone */ + "error"?: string; +} + +/** is what snapshot-remove takes. */ +export interface SnapshotRemoveInput { + /** true: every snapshot past snapshot-keep-days */ + "expired"?: boolean; + /** the snapshot's request; or expired true, not both */ + "request"?: string; + /** why, which the hand-act log keeps */ + "why": string; +} + +/** is what snapshot-remove answers. */ +export interface SnapshotRemoveAnswer { + /** accepted: the act's id */ + "accepted"?: string; + /** busy: the id of the act that runs; it keeps no queue */ + "busy"?: string; + /** why not; set alone */ + "error"?: string; +} + /** is the body of the event created. */ export type CreatedEvent = Said; @@ -295,6 +391,15 @@ export type RecreatedEvent = Said; /** is the body of the event dropped. */ export type DroppedEvent = Said; +/** is the body of the event snapshot-restored. */ +export interface SnapshotRestoredEvent { + "act": string; + /** when failed */ + "error"?: string; + "outcome": "done" | "failed"; + "request": string; +} + /** is the form of the provision bus-state. */ export type BusStateForm = Binding; diff --git a/ts/seats/meshbroker/index.js b/ts/seats/meshbroker/index.js index 89d30fb..db58b3e 100644 --- a/ts/seats/meshbroker/index.js +++ b/ts/seats/meshbroker/index.js @@ -13,6 +13,10 @@ export const verbs = { apply: "apply", recreate: "recreate", drop: "drop", + snapshotGet: "snapshot-get", + snapshotRestore: "snapshot-restore", + snapshotRestoreCancel: "snapshot-restore-cancel", + snapshotRemove: "snapshot-remove", }; export const events = { @@ -25,6 +29,7 @@ export const events = { orphaned: "orphaned", recreated: "recreated", dropped: "dropped", + snapshotRestored: "snapshot-restored", }; export const family = { name: "mesh-broker.events", maxAgeDays: 7 }; @@ -47,3 +52,11 @@ export const recreatedSubject = "mesh.seat.mesh-broker.event.recreated"; export const droppedSubject = "mesh.seat.mesh-broker.event.dropped"; +export const snapshotRestoredStream = "SEAT_mesh-broker_snapshot-restored"; + +export function snapshotRestoredSubject(request, act) { + return "mesh.seat.mesh-broker.event.snapshot-restored." + request + "." + act; +} + +export const snapshotRestoredSubjectAll = "mesh.seat.mesh-broker.event.snapshot-restored.*.*"; +