Move the restore of a bus archive into bussnap so the keeper restores a kept data too (milestone G, W14)
Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
This commit is contained in:
@@ -14,6 +14,8 @@ var (
|
|||||||
StreamNames = bussnap.StreamNames
|
StreamNames = bussnap.StreamNames
|
||||||
request = bussnap.Request
|
request = bussnap.Request
|
||||||
truncate = bussnap.Truncate
|
truncate = bussnap.Truncate
|
||||||
|
RestoreAll = bussnap.RestoreAll
|
||||||
|
heldTo = bussnap.HeldTo
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|||||||
@@ -4,10 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
@@ -20,118 +18,6 @@ import (
|
|||||||
"nats-tools/internal/busarchive"
|
"nats-tools/internal/busarchive"
|
||||||
)
|
)
|
||||||
|
|
||||||
// restoreChunk is what is sent at a time; the server answers each before the next goes.
|
|
||||||
const restoreChunk = 64 * 1024
|
|
||||||
|
|
||||||
// RestoreAll puts every stream of the archive into the server nc reaches, one at a time, and holds
|
|
||||||
// each restored stream's state to the manifest. **Nothing is restored over anything**: a server that
|
|
||||||
// already holds any of the streams is refused before the first is sent.
|
|
||||||
func RestoreAll(ctx context.Context, nc *nats.Conn, a *busarchive.Archive, log func(string, ...any)) error {
|
|
||||||
var present []string
|
|
||||||
for _, s := range a.Manifest.Streams {
|
|
||||||
err := request(ctx, nc, apiStreamInfo+s.Name, nil, nil)
|
|
||||||
var api *apiError
|
|
||||||
switch {
|
|
||||||
case err == nil:
|
|
||||||
present = append(present, s.Name)
|
|
||||||
case errors.As(err, &api) && api.Code == 404:
|
|
||||||
default:
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(present) > 0 {
|
|
||||||
return fmt.Errorf("the server already holds %s; nothing is restored over a live stream — restore into a new "+
|
|
||||||
"store (--into) and swap it in with the bus stopped", strings.Join(present, ", "))
|
|
||||||
}
|
|
||||||
for _, s := range a.Manifest.Streams {
|
|
||||||
started := time.Now()
|
|
||||||
if err := restoreStream(ctx, nc, a, s); err != nil {
|
|
||||||
return fmt.Errorf("stream %s: %w", s.Name, err)
|
|
||||||
}
|
|
||||||
log("%s: restored, at least %d message(s) to sequence %d, in %.2fs", s.Name, s.Messages, s.LastSeq, time.Since(started).Seconds())
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func restoreStream(ctx context.Context, nc *nats.Conn, a *busarchive.Archive, s busarchive.StreamEntry) error {
|
|
||||||
meta, archive, err := a.Open(s)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer archive.Close()
|
|
||||||
var req map[string]json.RawMessage
|
|
||||||
if err := json.Unmarshal(meta, &req); err != nil || req["config"] == nil {
|
|
||||||
return errors.New("its meta is not a stream's configuration and state")
|
|
||||||
}
|
|
||||||
var resp struct {
|
|
||||||
DeliverSubject string `json:"deliver_subject"`
|
|
||||||
}
|
|
||||||
if err := request(ctx, nc, apiStreamRestore+s.Name, req, &resp); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if resp.DeliverSubject == "" {
|
|
||||||
return errors.New("the server gave nowhere to send the archive")
|
|
||||||
}
|
|
||||||
buf := make([]byte, restoreChunk)
|
|
||||||
for {
|
|
||||||
n, err := archive.Read(buf)
|
|
||||||
if n > 0 {
|
|
||||||
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
|
||||||
reply, rerr := nc.RequestWithContext(cctx, resp.DeliverSubject, buf[:n])
|
|
||||||
cancel()
|
|
||||||
if rerr != nil {
|
|
||||||
return fmt.Errorf("sending the archive: %w", rerr)
|
|
||||||
}
|
|
||||||
if len(reply.Data) > 0 {
|
|
||||||
return fmt.Errorf("the server refused the archive: %s", truncate(reply.Data))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err == io.EOF {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// The end: an empty message, answered once the server has rebuilt the stream from what it was
|
|
||||||
// sent — which for a large stream takes a while.
|
|
||||||
fctx, cancel := context.WithTimeout(ctx, 30*time.Minute)
|
|
||||||
defer cancel()
|
|
||||||
final, err := nc.RequestWithContext(fctx, resp.DeliverSubject, nil)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("finishing the restore: %w", err)
|
|
||||||
}
|
|
||||||
var created struct {
|
|
||||||
Error *apiError `json:"error"`
|
|
||||||
State streamState `json:"state"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(final.Data, &created); err != nil {
|
|
||||||
return fmt.Errorf("the server's last answer is not JSON: %q", truncate(final.Data))
|
|
||||||
}
|
|
||||||
if created.Error != nil {
|
|
||||||
return created.Error
|
|
||||||
}
|
|
||||||
return heldTo(created.State, s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// heldTo is whether a restored stream holds at least what its snapshot said it held.
|
|
||||||
//
|
|
||||||
// **At least, not exactly — found by the restore test, not assumed.** The state the server answers a
|
|
||||||
// snapshot request with is taken when the snapshot starts; the stream goes on taking messages while
|
|
||||||
// its blocks are read out, and a block read a moment later carries some of what was appended in that
|
|
||||||
// moment. So a stream written during its snapshot restores with a later last sequence than the
|
|
||||||
// manifest says (90075 where it said 90024, with a writer publishing every 2ms through it), and of
|
|
||||||
// the messages in that tail some may be there and some not. Every message up to the manifest's last
|
|
||||||
// sequence is there, exactly; that is the snapshot's promise, and what the test holds it to message
|
|
||||||
// by message. What must never happen is a restored stream that ends before the manifest said it did.
|
|
||||||
func heldTo(restored streamState, s busarchive.StreamEntry) error {
|
|
||||||
if restored.LastSeq < s.LastSeq || (s.Messages > 0 && restored.Messages == 0) {
|
|
||||||
return fmt.Errorf("restored with %d message(s) up to sequence %d; the snapshot held %d up to %d",
|
|
||||||
restored.Messages, restored.LastSeq, s.Messages, s.LastSeq)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RestoreOffline fills a new store directory with the archive's streams, by a server of this
|
// RestoreOffline fills a new store directory with the archive's streams, by a server of this
|
||||||
// program's own: nats-server, the same binary the bus runs, on loopback, with the bus's one account
|
// program's own: nats-server, the same binary the bus runs, on loopback, with the bus's one account
|
||||||
// and a user that exists only for this run. It is stopped when the streams are in. What it leaves
|
// and a user that exists only for this run. It is stopped when the streams are in. What it leaves
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
package bussnap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/nats-io/nats.go"
|
||||||
|
"nats-tools/internal/busarchive"
|
||||||
|
)
|
||||||
|
|
||||||
|
// restoreChunk is what is sent at a time; the server answers each before the next goes.
|
||||||
|
const restoreChunk = 64 * 1024
|
||||||
|
|
||||||
|
// RestoreAll puts every stream of the archive into the server nc reaches, one at a time, and holds
|
||||||
|
// each restored stream's state to the manifest. **Nothing is restored over anything**: a server that
|
||||||
|
// already holds any of the streams is refused before the first is sent.
|
||||||
|
func RestoreAll(ctx context.Context, nc *nats.Conn, a *busarchive.Archive, log func(string, ...any)) error {
|
||||||
|
var present []string
|
||||||
|
for _, s := range a.Manifest.Streams {
|
||||||
|
err := Request(ctx, nc, APIStreamInfo+s.Name, nil, nil)
|
||||||
|
var api *APIError
|
||||||
|
switch {
|
||||||
|
case err == nil:
|
||||||
|
present = append(present, s.Name)
|
||||||
|
case errors.As(err, &api) && api.Code == 404:
|
||||||
|
default:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(present) > 0 {
|
||||||
|
return fmt.Errorf("the server already holds %s; nothing is restored over a live stream — restore into a new "+
|
||||||
|
"store (--into) and swap it in with the bus stopped", strings.Join(present, ", "))
|
||||||
|
}
|
||||||
|
for _, s := range a.Manifest.Streams {
|
||||||
|
started := time.Now()
|
||||||
|
if err := restoreStream(ctx, nc, a, s); err != nil {
|
||||||
|
return fmt.Errorf("stream %s: %w", s.Name, err)
|
||||||
|
}
|
||||||
|
log("%s: restored, at least %d message(s) to sequence %d, in %.2fs", s.Name, s.Messages, s.LastSeq, time.Since(started).Seconds())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func restoreStream(ctx context.Context, nc *nats.Conn, a *busarchive.Archive, s busarchive.StreamEntry) error {
|
||||||
|
meta, archive, err := a.Open(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer archive.Close()
|
||||||
|
var req map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(meta, &req); err != nil || req["config"] == nil {
|
||||||
|
return errors.New("its meta is not a stream's configuration and state")
|
||||||
|
}
|
||||||
|
var resp struct {
|
||||||
|
DeliverSubject string `json:"deliver_subject"`
|
||||||
|
}
|
||||||
|
if err := Request(ctx, nc, APIStreamRestore+s.Name, req, &resp); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if resp.DeliverSubject == "" {
|
||||||
|
return errors.New("the server gave nowhere to send the archive")
|
||||||
|
}
|
||||||
|
buf := make([]byte, restoreChunk)
|
||||||
|
for {
|
||||||
|
n, err := archive.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
cctx, cancel := context.WithTimeout(ctx, 30*time.Second)
|
||||||
|
reply, rerr := nc.RequestWithContext(cctx, resp.DeliverSubject, buf[:n])
|
||||||
|
cancel()
|
||||||
|
if rerr != nil {
|
||||||
|
return fmt.Errorf("sending the archive: %w", rerr)
|
||||||
|
}
|
||||||
|
if len(reply.Data) > 0 {
|
||||||
|
return fmt.Errorf("the server refused the archive: %s", Truncate(reply.Data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// The end: an empty message, answered once the server has rebuilt the stream from what it was
|
||||||
|
// sent — which for a large stream takes a while.
|
||||||
|
fctx, cancel := context.WithTimeout(ctx, 30*time.Minute)
|
||||||
|
defer cancel()
|
||||||
|
final, err := nc.RequestWithContext(fctx, resp.DeliverSubject, nil)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("finishing the restore: %w", err)
|
||||||
|
}
|
||||||
|
var created struct {
|
||||||
|
Error *APIError `json:"error"`
|
||||||
|
State StreamState `json:"state"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(final.Data, &created); err != nil {
|
||||||
|
return fmt.Errorf("the server's last answer is not JSON: %q", Truncate(final.Data))
|
||||||
|
}
|
||||||
|
if created.Error != nil {
|
||||||
|
return created.Error
|
||||||
|
}
|
||||||
|
return HeldTo(created.State, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HeldTo is whether a restored stream holds at least what its snapshot said it held.
|
||||||
|
//
|
||||||
|
// **At least, not exactly — found by the restore test, not assumed.** The state the server answers a
|
||||||
|
// snapshot request with is taken when the snapshot starts; the stream goes on taking messages while
|
||||||
|
// its blocks are read out, and a block read a moment later carries some of what was appended in that
|
||||||
|
// moment. So a stream written during its snapshot restores with a later last sequence than the
|
||||||
|
// manifest says (90075 where it said 90024, with a writer publishing every 2ms through it), and of
|
||||||
|
// the messages in that tail some may be there and some not. Every message up to the manifest's last
|
||||||
|
// sequence is there, exactly; that is the snapshot's promise, and what the test holds it to message
|
||||||
|
// by message. What must never happen is a restored stream that ends before the manifest said it did.
|
||||||
|
func HeldTo(restored StreamState, s busarchive.StreamEntry) error {
|
||||||
|
if restored.LastSeq < s.LastSeq || (s.Messages > 0 && restored.Messages == 0) {
|
||||||
|
return fmt.Errorf("restored with %d message(s) up to sequence %d; the snapshot held %d up to %d",
|
||||||
|
restored.Messages, restored.LastSeq, s.Messages, s.LastSeq)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user