Files
mesh-catalog/modules/nats/internal/keeper/restore.go
T
jschoubben 44cb7ad449 bus5 B5 (A-107): the keeper's process answers mesh-broker's snapshot verbs itself through AnswerSeat
snapshot-get, snapshot-restore, snapshot-restore-cancel and snapshot-remove are answered by
the keeper's process with the proved caller; it publishes snapshot-get's chunks and hears
their acks on its own subscription, and acts at once with no hand-over through requests
(the record of a running restore is written before it runs). The tools bundle keeps its
tools, nats_snapshots among them. The protected refusal and the whole test's case (send,
restore in place, removal) stay.

Builds: design 47 G A-107 (G-20-lead-answers), §681

Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-11 23:56:06 +01:00

236 lines
8.2 KiB
Go

package keeper
import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"time"
"git.novox.be/novox/mesh-contracts/go/seats/meshbroker"
)
// The keeper's restore in place of a snapshot it took (design 47 G §681, ADR 0675; the bus module hands the work through
// the state requests, A-45): the recovery module asks the seat mesh-broker's snapshot-restore for a protected snapshot, which
// is never handed out. The keeper checks the file against its recorded digest, refuses while a live stream of the snapshot's
// names exists, and restores them under their own names within RestoreBound, one restore at a time. It says the outcome on
// the event snapshot-restored under the request and the act.
// Verbs a request names for the snapshot restore.
const (
VerbSnapshotRestore = meshbroker.VerbSnapshotRestore
VerbSnapshotRestoreCancel = meshbroker.VerbSnapshotRestoreCancel
VerbSnapshotRemove = meshbroker.VerbSnapshotRemove
)
// RestoreBound is the longest a restore runs: past it the keeper ends it as failed.
const RestoreBound = 60 * time.Minute
// SnapshotRestored is the body of the event snapshot-restored.
type SnapshotRestored = meshbroker.SnapshotRestoredEvent
// restoreRun is the restore that runs: its act and how to end it.
type restoreRun struct {
act string
request string
streams []string
cancel context.CancelFunc
cancelled bool
}
// snapshotOf reads the recorded snapshot of request id: the refusals §681 words, or the record.
func (k *Keeper) snapshotOf(id string) (Request, error) {
raw, _, found, err := k.Store.Get(StateRequests, id)
if err != nil {
return Request{}, fmt.Errorf("the snapshot of the request %s cannot be read: %w", id, err)
}
var rec Request
if !found || json.Unmarshal(raw, &rec) != nil || rec.Verb == "" {
return Request{}, fmt.Errorf("no snapshot of the request %s is held", id)
}
if !rec.Recorded() {
return Request{}, fmt.Errorf("snapshot taken before milestone G")
}
return rec, nil
}
// checked reads the snapshot file against its record: missing, or damaged.
func (k *Keeper) checked(rec Request) error {
size, digest, err := k.files().Digest(rec.Path)
if errors.Is(err, os.ErrNotExist) {
return fmt.Errorf("snapshot missing")
}
if err != nil {
return fmt.Errorf("the snapshot %s cannot be read: %w", rec.Path, err)
}
if digest != rec.Digest || size != rec.Size {
return fmt.Errorf("snapshot damaged: request %s", rec.ID)
}
return nil
}
// startRestore answers a snapshot-restore at once: accepted with the act's id (the request's own id), busy with the id of the
// running one, or an error; verify says the file holds and restores nothing.
func (k *Keeper) startRestore(r *Request) (string, error) {
rec, err := k.snapshotOf(r.Of)
if err != nil {
return "error", err
}
if err := k.checked(rec); err != nil {
return "error", err
}
if r.Verify {
return "verified", nil
}
ctx, cancel := context.WithTimeout(context.Background(), RestoreBound)
streams, err := k.Bus.SnapshotStreams(rec.Path)
if err != nil {
cancel()
return "error", fmt.Errorf("the snapshot's streams cannot be read: %w", err)
}
for _, name := range streams {
live, err := k.Bus.Live(ctx, name)
if err != nil {
cancel()
return "error", fmt.Errorf("whether %s is live cannot be read: %w", name, err)
}
if live {
cancel()
return "error", fmt.Errorf("names live: the stream %s exists, and a restore goes over nothing", name)
}
}
k.mu.Lock()
if run := k.restoring; run != nil {
k.mu.Unlock()
cancel()
r.ActID = run.act
return "busy", nil
}
run := &restoreRun{act: r.ID, request: rec.ID, streams: streams, cancel: cancel}
k.restoring = run
k.mu.Unlock()
r.ActID = run.act
// The act's record is written before the restore runs, as the end of the restore and a restart's cut-off read it; a record that
// cannot be written ends the start, so no restore runs that nothing could find again.
held := *r
held.Done, held.Outcome = k.now(), "accepted"
data, _ := json.Marshal(held)
if _, err := k.Store.Put(StateRequests, r.ID, data); err != nil {
k.mu.Lock()
k.restoring = nil
k.mu.Unlock()
cancel()
return "error", fmt.Errorf("the restore's record cannot be written, so nothing is restored: %w", err)
}
k.record(r, fmt.Sprintf("restores the snapshot of the request %s (%s) in place", rec.ID, rec.Path))
go k.restore(ctx, run, rec, r.Caller, r.Why)
return "accepted", nil
}
// restore runs the restore to its end. A restore that failed, passed its bound or was cancelled removes the keeper's own
// partial stream (it held no live data, names live having come first) and says snapshot-restored failed before the keeper
// takes another restore.
func (k *Keeper) restore(ctx context.Context, run *restoreRun, rec Request, caller, why string) {
err := k.Bus.Restore(ctx, rec.Path)
k.mu.Lock()
cancelled := run.cancelled
k.mu.Unlock()
switch {
case cancelled:
err = fmt.Errorf("cancelled")
case errors.Is(ctx.Err(), context.DeadlineExceeded):
err = fmt.Errorf("not finished within %s: %v", RestoreBound, err)
}
run.cancel()
k.endRestore(run.request, run.act, run.streams, err)
k.mu.Lock()
if k.restoring == run {
k.restoring = nil
}
k.mu.Unlock()
}
// endRestore ends the act: a failure removes the partial streams first, then the event says it and the request's record
// holds how it ended.
func (k *Keeper) endRestore(request, act string, streams []string, failure error) {
result, text := meshbroker.SnapshotRestoredEventOutcomeDone, ""
if failure != nil {
result, text = meshbroker.SnapshotRestoredEventOutcomeFailed, failure.Error()
// Removing the partial stream does not depend on the restore's own context, which has ended.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
for _, name := range streams {
if err := k.Bus.DeleteStream(ctx, name); err != nil {
text += fmt.Sprintf("; the partial stream %s could not be removed: %v", name, err)
k.logf("the partial stream %s of the restore %s cannot be removed: %v", name, act, err)
}
}
cancel()
}
k.logf("the restore %s of the snapshot of the request %s ended %s %s", act, request, result, text)
if raw, _, found, err := k.Store.Get(StateRequests, act); err == nil && found {
var r Request
if json.Unmarshal(raw, &r) == nil {
r.Ended, r.Result = k.now(), result
if text != "" {
r.Error = text
}
data, _ := json.Marshal(r)
if _, err := k.Store.Put(StateRequests, act, data); err != nil {
k.logf("the end of the restore %s cannot be written: %v", act, err)
}
}
}
if k.Emit == nil {
return
}
s := Saying{Event: meshbroker.EventSnapshotRestored, ID: EventID(), At: k.now(),
Restored: &SnapshotRestored{Request: request, Act: act, Outcome: result, Error: text}}
if err := k.Emit(&s); err != nil {
s.Error = err.Error()
k.setStatus(func(st *Status) { st.Unsaid = append(st.Unsaid, s) })
k.logf("the event %s of the restore %s could not be said, said again at the next fill pass: %v", s.Event, act, err)
}
}
// cancelRestore cancels the restore of the act r.Of names: accepted with its id, or an error when it has ended.
func (k *Keeper) cancelRestore(r *Request) (string, error) {
k.mu.Lock()
run := k.restoring
if run == nil || run.act != r.Of {
k.mu.Unlock()
return "error", fmt.Errorf("the restore %s has ended or is none of this keeper's running", r.Of)
}
run.cancelled = true
k.mu.Unlock()
run.cancel()
r.ActID = run.act
k.record(r, fmt.Sprintf("cancelled the restore %s", run.act))
return "accepted", nil
}
// EndCutOffRestores ends, at the keeper's start, every restore a restart cut off: its partial stream removed and
// snapshot-restored said failed under its act.
func (k *Keeper) EndCutOffRestores() {
keys, err := k.Store.Keys(StateRequests)
if err != nil {
k.logf("the restores a restart cut off cannot be looked for: %v", err)
return
}
for _, key := range keys {
raw, _, found, err := k.Store.Get(StateRequests, key)
if err != nil || !found {
continue
}
var r Request
if json.Unmarshal(raw, &r) != nil || r.Verb != VerbSnapshotRestore || r.Outcome != "accepted" || !r.Ended.IsZero() {
continue
}
var streams []string
if rec, err := k.snapshotOf(r.Of); err == nil {
streams, _ = k.Bus.SnapshotStreams(rec.Path)
}
k.endRestore(r.Of, r.ID, streams, fmt.Errorf("cut off by the keeper's restart"))
}
}