Files
upstream-nats-server/server/stream_backup.go
upstream-mirror 709385cbaa
Some checks failed
NATS Server Tests / Lint (push) Failing after 3s
NATS Server Tests / Build (Latest Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2022) (push) Has been skipped
NATS Server Tests / Build (Minimum Go, windows-2025) (push) Has been skipped
NATS Server Tests / Test Stores (push) Has been skipped
NATS Server Tests / Test JetStream (push) Has been skipped
NATS Server Tests / Test Raft (push) Has been skipped
NATS Server Tests / Test JetStream Consumers (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 1 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 2 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 3 (push) Has been skipped
NATS Server Tests / Test JetStream Cluster 4 (push) Has been skipped
NATS Server Tests / Test JetStream Supercluster (push) Has been skipped
NATS Server Tests / Test No-Race 1 (push) Has been skipped
NATS Server Tests / Test No-Race 2 (push) Has been skipped
NATS Server Tests / Test MQTT (push) Has been skipped
NATS Server Tests / Test Message Tracing (push) Has been skipped
NATS Server Tests / Test JWT (push) Has been skipped
NATS Server Tests / Test Remaining Server Tests (push) Has been skipped
NATS Server Tests / Test Other Packages (push) Has been skipped
Vulnerability Scan / govulncheck (push) Failing after 12m2s
snapshot of https://github.com/nats-io/nats-server.git (main, 2026-09-15; blobs > 131072B omitted)
2026-09-15 20:16:01 +05:00

575 lines
18 KiB
Go

// Copyright 2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"encoding/json"
"errors"
"fmt"
"io"
"math"
"net"
"path"
"slices"
"strings"
"time"
"github.com/klauspost/compress/s2"
"github.com/nats-io/nats-server/v2/server/archive"
)
type SnapshotConsumerState struct {
*ConsumerConfig `json:"config"`
*ConsumerState `json:"state"`
}
// Create a snapshot of this stream and its consumer's state along with messages.
// sa is passed in when the stream is clustered, so we can find child consumer assignments.
func (js *jetStream) CreateStreamSnapshotV2(store StreamStore, deadline time.Duration, includeConsumers bool, sa *streamAssignment) (*SnapshotResult, error) {
pr, pw := net.Pipe()
// Set a write deadline here to protect ourselves.
if deadline > 0 {
pw.SetWriteDeadline(time.Now().Add(deadline))
}
// We can add to our stream while snapshotting but not "user" delete anything.
var state StreamState
store.FastState(&state)
// Stream in separate Go routine.
errCh := make(chan error, 1)
go js.streamSnapshotV2(store, &state, pw, includeConsumers, sa, errCh)
return &SnapshotResult{pr, state, errCh}, nil
}
// Stream our snapshot through S2 compression and the custom archive format.
func (js *jetStream) streamSnapshotV2(store StreamStore, state *StreamState, w io.WriteCloser, includeConsumers bool, sa *streamAssignment, errCh chan error) {
defer close(errCh)
defer w.Close()
enc := s2.NewWriter(w)
defer enc.Close()
tw := archive.NewWriter(enc)
defer tw.Close()
now := time.Now()
clustered := js.isClustered()
writeGeneric := func(name string, mod int64, seq uint64, headerSize, payloadSize int64, buf []byte) error {
hdr := &archive.Header{
Name: name,
Timestamp: mod,
Sequence: seq,
HeaderSize: headerSize,
PayloadSize: payloadSize,
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if _, err := tw.Write(buf); err != nil {
return err
}
// Need to wait for flush here as the tar/s2 writer is handing off to a
// flow-controlled publisher, it's important that we handle backpressure.
return tw.Flush()
}
writeStoreMsg := func(msg *StoreMsg) error {
return writeGeneric(
msg.subj,
msg.ts,
msg.seq,
int64(len(msg.hdr)),
int64(len(msg.msg)),
msg.buf,
)
}
writeConsumerMsg := func(scs SnapshotConsumerState) error {
// Bound the consumer state to the stream snapshot. Consumer sequence
// numbers are left intact since filtered consumers do not have a
// one-to-one mapping between consumer and stream sequences.
scs.Delivered.Stream = min(scs.Delivered.Stream, state.LastSeq)
scs.AckFloor.Stream = min(scs.AckFloor.Stream, state.LastSeq)
for seq := range scs.Pending {
if seq > state.LastSeq {
delete(scs.Pending, seq)
}
}
for seq := range scs.Redelivered {
if seq > state.LastSeq {
delete(scs.Redelivered, seq)
}
}
ssj, err := json.Marshal(scs)
if err != nil {
return err
}
return writeGeneric(
path.Join("consumers", scs.Name),
now.UnixNano(),
0,
0,
int64(len(ssj)),
ssj,
)
}
// If we aren't including consumers here then make sure the consumer count
// is set accordingly, this helps on the restore path.
var consumerAssignments map[string]*consumerAssignment
var consumerStores []ConsumerStore
var streamState = *state
if !includeConsumers {
streamState.Consumers = 0
} else if clustered {
if sa == nil {
errCh <- errors.New("stream assignment not present in clustered mode")
return
}
js.mu.RLock()
consumerAssignments = make(map[string]*consumerAssignment, len(sa.consumers))
for name, ca := range sa.consumers {
consumerAssignments[name] = ca.copyGroup()
}
streamState.Consumers = len(consumerAssignments)
js.mu.RUnlock()
} else {
consumerStores = slices.Collect(store.Consumers())
streamState.Consumers = len(consumerStores)
}
ssj, err := json.Marshal(streamState)
if err != nil {
errCh <- err
return
}
if err := writeGeneric("state.json", now.UnixNano(), 0, 0, int64(len(ssj)), ssj); err != nil {
errCh <- err
return
}
// Do consumers first, if the stream is interest/WQ then this may be
// important for message retention.
if includeConsumers {
consumerStateFromInfo := func(ci *ConsumerInfo) *ConsumerState {
state := &ConsumerState{
Delivered: SequencePair{
Consumer: ci.Delivered.Consumer,
Stream: ci.Delivered.Stream,
},
AckFloor: SequencePair{
Consumer: ci.AckFloor.Consumer,
Stream: ci.AckFloor.Stream,
},
}
if ci.NumAckPending > 0 {
// Cluster consumer info does not include the sparse pending state.
// Roll back to the ack floor so all possibly unacknowledged messages
// are delivered again instead of guessing which ones are pending.
state.Delivered = state.AckFloor
}
return state
}
if clustered {
for _, ca := range consumerAssignments {
ci, err := sysRequest[ConsumerInfo](js.srv, clusterConsumerInfoT, sa.Client.serviceAccount(), sa.Config.Name, ca.Name)
if err != nil || ci == nil {
errCh <- fmt.Errorf("failed to get consumer state for '%s > %s'", sa.Config.Name, ca.Name)
return
}
if err := writeConsumerMsg(SnapshotConsumerState{
ConsumerConfig: ca.Config,
ConsumerState: consumerStateFromInfo(ci),
}); err != nil {
errCh <- err
return
}
}
} else {
for _, o := range consumerStores {
config := o.GetConfig()
state, err := o.State()
if err != nil {
errCh <- fmt.Errorf("couldn't load consumer '%s' state: %s", config.Name, err)
return
}
if err := writeConsumerMsg(SnapshotConsumerState{
ConsumerConfig: config,
ConsumerState: state,
}); err != nil {
errCh <- err
return
}
}
}
}
var sm StoreMsg
for seq := state.FirstSeq - 1; seq < state.LastSeq; {
if _, seq, err = store.LoadNextMsg(fwcs, true, seq+1, &sm); err != nil {
if err == ErrStoreEOF {
break
}
errCh <- fmt.Errorf("couldn't load next message after seq %d: %s", seq+1, err)
return
}
// Concurrent deletions and publishes can move the next available
// message beyond the snapshot's original sequence range.
if seq > state.LastSeq {
break
}
if err = writeStoreMsg(&sm); err != nil {
errCh <- err
return
}
}
// End of backup sentinel. A clear marker makes it obvious when
// a backup has been truncated or not without having to count
// messages or from first/last sequence, which may not be possible
// during the rewrite of a large stream backup.
if err = writeGeneric(_EMPTY_, 0, 0, 0, 0, nil); err != nil {
errCh <- err
}
}
// RestoreStreamSnapshotV2 will restore a stream from a snapshot.
func (a *Account) RestoreStreamV2(ncfg *StreamConfig, r io.Reader) (retMset *stream, retErr error) {
dec := s2.NewReader(r)
tr := archive.NewReader(dec)
var nstate StreamState
// Load the stream state.
hdr, err := tr.Next()
if err != nil {
return nil, err
}
if hdr.Name != "state.json" {
return nil, fmt.Errorf("expected state.json first")
}
if err := json.NewDecoder(tr).Decode(&nstate); err != nil {
return nil, fmt.Errorf("error in state.json: %w", err)
}
if nstate.Consumers < 0 {
return nil, fmt.Errorf("invalid stream state: negative consumer count")
}
if nstate.FirstSeq == 0 {
if nstate.LastSeq != 0 || nstate.Msgs != 0 {
return nil, fmt.Errorf("invalid stream state: inconsistent message count or sequence range")
}
} else if nstate.FirstSeq > nstate.LastSeq {
// An empty, advanced stream has FirstSeq exactly one past LastSeq.
// Express this by subtracting from FirstSeq so a corrupt LastSeq of
// MaxUint64 can not overflow here.
if nstate.FirstSeq-1 != nstate.LastSeq || nstate.Msgs != 0 {
return nil, fmt.Errorf("invalid stream state: inconsistent message count or sequence range")
}
} else if nstate.Msgs > nstate.LastSeq-nstate.FirstSeq+1 {
return nil, fmt.Errorf("invalid stream state: message count exceeds sequence range")
}
s, jsa, err := a.checkForJetStream()
if err != nil {
return nil, err
}
js := jsa.js
if js == nil {
return nil, NewJSNotEnabledForAccountError()
}
if _, err := a.lookupStream(ncfg.Name); err == nil {
return nil, NewJSStreamNameExistRestoreFailedError()
}
cfg, apiErr := s.checkStreamCfg(ncfg, a, false)
if apiErr != nil {
return nil, apiErr
}
_, isClustered := jsa.jetStreamAndClustered()
jsa.usageMu.RLock()
selected, tier, hasTier := jsa.selectLimits(cfg.Replicas)
jsa.usageMu.RUnlock()
reserved := int64(0)
if hasTier {
if isClustered {
js.mu.RLock()
_, reserved = js.tieredStreamAndReservationCount(a.Name, tier, &cfg)
js.mu.RUnlock()
} else {
reserved = jsa.tieredReservation(tier, &cfg)
}
}
// Hold the full restore footprint as usage before creating the stream. As
// messages are stored, this temporary reservation is converted into actual
// usage so other streams cannot consume the capacity needed by the restore.
restoreBytes := nstate.Bytes
if restoreBytes > math.MaxInt64 {
if cfg.Storage == MemoryStorage {
return nil, NewJSMemoryResourcesExceededError()
}
return nil, NewJSStorageResourcesExceededError()
}
restoreRemaining := int64(restoreBytes)
jsa.updateUsage(tier, cfg.Storage, restoreRemaining)
defer func() {
// Inside a defer func() so that restoreRemaining is captured later.
jsa.updateUsage(tier, cfg.Storage, -restoreRemaining)
}()
releaseRestoreBytes := func(bytes int64) {
bytes = min(bytes, restoreRemaining)
jsa.updateUsage(tier, cfg.Storage, -bytes)
restoreRemaining -= bytes
}
checkUsageLimits := func() error {
lr := int64(max(1, cfg.Replicas))
if tier == _EMPTY_ {
lr = 1
}
jsa.usageMu.RLock()
usage := jsa.usage[tier]
var used, limit int64
if cfg.Storage == MemoryStorage {
limit = selected.MaxMemory
if usage != nil {
used = usage.total.mem
}
} else {
limit = selected.MaxStore
if usage != nil {
used = usage.total.store
}
}
jsa.usageMu.RUnlock()
if limit >= 0 && used > mulSaturate(limit, lr) {
if cfg.Storage == MemoryStorage {
return NewJSMemoryResourcesExceededError()
}
return NewJSStorageResourcesExceededError()
}
if js.limitsExceeded(cfg.Storage) {
if cfg.Storage == MemoryStorage {
return NewJSMemoryResourcesExceededError()
}
return NewJSStorageResourcesExceededError()
}
return nil
}
reserveCfg := cfg
reserveCfg.MaxBytes = max(reserveCfg.MaxBytes, restoreRemaining)
js.mu.RLock()
err = js.checkAllLimits(&selected, tier, &reserveCfg, reserved, 0)
js.mu.RUnlock()
if err != nil {
return nil, err
}
if err := checkUsageLimits(); err != nil {
return nil, err
}
mset, err := a.addStreamForRestore(&cfg)
if err != nil {
return nil, fmt.Errorf("error adding stream: %w", err)
}
defer func() {
var state StreamState
mset.store.FastState(&state)
mset.mu.Lock()
mset.lseq = state.LastSeq
mset.mu.Unlock()
if err := mset.completeRestore(); err != nil {
if err = fmt.Errorf("failed to activate stream %q: %w", cfg.Name, err); retErr == nil {
retErr = err
}
s.Warnf("JetStream stream restore for '%s > %s' failed to activate stream: %v", a.Name, cfg.Name, err)
}
}()
// Start off at the right sequence number. This is important in particular
// when the backup contains no messages or would restore to no interest.
if _, err = mset.store.Compact(nstate.FirstSeq); err != nil {
return nil, fmt.Errorf("error purging stream: %w", err)
}
var restoredConsumers, ephemerals []*consumer
defer func() {
// Consumers must be unconditionally converted and completed, otherwise
// a partial restore that fails midway through can leave assets that are
// unusable.
for _, o := range ephemerals {
o.switchToEphemeral()
}
for _, o := range restoredConsumers {
if err := o.completeRestore(); err != nil {
if err = fmt.Errorf("failed to activate consumer %q: %w", o.name, err); retErr == nil {
retErr = err
}
s.Warnf("JetStream stream restore for '%s > %s' failed to activate consumers: %v", a.Name, cfg.Name, err)
}
}
}()
for range nstate.Consumers {
hdr, err := tr.Next()
if err != nil {
return nil, err
}
name, found := strings.CutPrefix(hdr.Name, "consumers/")
if !found {
return nil, fmt.Errorf("expected consumer, found %q", hdr.Name)
}
var consumer SnapshotConsumerState
if err := json.NewDecoder(tr).Decode(&consumer); err != nil {
return nil, fmt.Errorf("failed to decode consumer %q state: %w", name, err)
}
if consumer.ConsumerConfig == nil {
return nil, fmt.Errorf("consumer %q is missing config", name)
}
if consumer.ConsumerState == nil {
return nil, fmt.Errorf("consumer %q is missing state", name)
}
isEphemeral := !isDurableConsumer(consumer.ConsumerConfig)
if isEphemeral {
// Keep ephemerals alive and interested until all messages have
// been restored, then start their normal inactivity lifecycle.
consumer.Durable = name
}
o, err := mset.addConsumerForRestore(consumer.ConsumerConfig)
if err != nil {
return nil, fmt.Errorf("failed to add consumer %q: %w", name, err)
}
if isEphemeral {
ephemerals = append(ephemerals, o)
}
restoredConsumers = append(restoredConsumers, o)
o.mu.Lock()
err = o.setStoreState(consumer.ConsumerState)
o.mu.Unlock()
if err != nil {
return nil, fmt.Errorf("failed to set consumer %q state: %w", name, err)
}
}
store := mset.store
lseq := nstate.FirstSeq - 1
eob := false
mp := int64(s.getOpts().MaxPayload)
for {
hdr, err := tr.Next()
if err != nil {
return nil, err
}
seq := hdr.Sequence
if seq == 0 {
// Sentinel "end of backup" if all fields are zero.
if hdr.Name == _EMPTY_ && hdr.Timestamp == 0 && hdr.HeaderSize == 0 && hdr.PayloadSize == 0 {
eob = true
break
}
return nil, fmt.Errorf("expected message sequence")
}
if hdr.HeaderSize < 0 || hdr.PayloadSize < 0 || hdr.PayloadSize > math.MaxInt64-hdr.HeaderSize {
return nil, fmt.Errorf("invalid message lengths for sequence %d", seq)
}
declaredSize := hdr.HeaderSize + hdr.PayloadSize
if hdr.HeaderSize > mp || hdr.PayloadSize > mp-hdr.HeaderSize {
return nil, fmt.Errorf("message sequence %d exceeds maximum payload size", seq)
}
if mms := int64(cfg.MaxMsgSize); mms >= 0 && (hdr.HeaderSize > mms || hdr.PayloadSize > mms-hdr.HeaderSize) {
return nil, fmt.Errorf("message sequence %d exceeds maximum message size", seq)
}
var storedSizeRaw uint64
switch cfg.Storage {
case MemoryStorage:
if storedSizeRaw = memStoreMsgSizeRaw(len(hdr.Name), int(hdr.HeaderSize), int(hdr.PayloadSize)); storedSizeRaw > math.MaxInt64 {
return nil, fmt.Errorf("snapshot message bytes exceed maximum store message size")
}
default:
if storedSizeRaw = fileStoreMsgSizeRaw(len(hdr.Name), int(hdr.HeaderSize), int(hdr.PayloadSize)); storedSizeRaw > rlBadThresh {
return nil, fmt.Errorf("snapshot message bytes exceed maximum store message size")
}
}
storedSize := int64(storedSizeRaw)
if additional := storedSize - restoreRemaining; additional > 0 {
jsa.updateUsage(tier, cfg.Storage, additional)
restoreRemaining += additional
if err := checkUsageLimits(); err != nil {
return nil, err
}
}
buf, err := io.ReadAll(io.LimitReader(tr, declaredSize))
if err != nil {
return nil, fmt.Errorf("failed to read message sequence %d: %w", seq, err)
}
if hdr.HeaderSize > int64(len(buf)) {
return nil, fmt.Errorf("failed to parse message sequence %d: invalid header length", seq)
}
if int64(len(buf)) != declaredSize {
return nil, fmt.Errorf("failed to read message sequence %d: unexpected payload size", seq)
}
subj := hdr.Name
mhdr := buf[:hdr.HeaderSize]
msg := buf[hdr.HeaderSize : hdr.HeaderSize+hdr.PayloadSize]
if seq <= lseq {
return nil, fmt.Errorf("message sequence %d out of order", seq)
}
// We could have deleted messages since the last message we stored, if so
// we should work out what the gap is and skip those sequences.
if gap := seq - lseq - 1; gap > 0 {
if err := store.SkipMsgs(lseq+1, gap); err != nil {
return nil, fmt.Errorf("failed to process gap: %w", err)
}
}
lseq = seq
ttl, err := getMessageTTL(mhdr)
if err != nil {
return nil, fmt.Errorf("failed to parse message TTL: %w", err)
}
hdrTime := time.Unix(0, hdr.Timestamp)
if ttl > 0 && time.Now().After(hdrTime.Add(time.Duration(ttl)*time.Second)) {
// If the TTL has exceeded then there isn't much point in storing the message,
// but we still need to preserve the sequence.
if err := store.SkipMsgs(seq, 1); err != nil {
return nil, fmt.Errorf("failed to process expired message sequence %d: %w", seq, err)
}
releaseRestoreBytes(storedSize)
continue
}
if err = store.StoreRawMsg(subj, mhdr, msg, seq, hdr.Timestamp, ttl, false); err != nil {
return nil, fmt.Errorf("failed to store message sequence %d: %w", seq, err)
}
releaseRestoreBytes(storedSize)
}
if !eob {
return mset, fmt.Errorf("backup was truncated")
}
// Need to make sure that we pad out with skip msgs to preserve the last
// sequence, otherwise trailing deleted messages could reuse sequence numbers.
if lseq < nstate.LastSeq {
gap := nstate.LastSeq - lseq
if err := store.SkipMsgs(lseq+1, gap); err != nil {
return nil, fmt.Errorf("failed to process trailing gap: %w", err)
}
}
return mset, nil
}