mirror of
https://github.com/hanzomsg/nats-server.git
synced 2026-10-30 20:27:34 +00:00
2751 lines
82 KiB
Go
2751 lines
82 KiB
Go
// Copyright 2025 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.
|
|
|
|
//go:build !skip_js_tests
|
|
|
|
package server
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/klauspost/compress/s2"
|
|
"github.com/nats-io/nats.go"
|
|
)
|
|
|
|
func TestJetStreamAtomicBatchPublish(t *testing.T) {
|
|
test := func(
|
|
t *testing.T,
|
|
storage StorageType,
|
|
retention RetentionPolicy,
|
|
replicas int,
|
|
) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
var pubAck JSPubAckResponse
|
|
|
|
cfg := &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo.*"},
|
|
Storage: storage,
|
|
Retention: retention,
|
|
Replicas: replicas,
|
|
}
|
|
|
|
_, err := jsStreamCreate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
|
|
m := nats.NewMsg("foo.0")
|
|
m.Data = []byte("foo.0")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
|
|
// Publish with atomic publish disabled.
|
|
rmsg, err := nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_NotNil(t, pubAck.Error)
|
|
require_Error(t, pubAck.Error, NewJSAtomicPublishDisabledError())
|
|
|
|
// Enable atomic publish.
|
|
cfg.AllowAtomicPublish = true
|
|
_, err = jsStreamUpdate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
|
|
// Publish without batch sequence errors.
|
|
rmsg, err = nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
pubAck = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_Error(t, pubAck.Error, NewJSAtomicPublishMissingSeqError())
|
|
|
|
// Publish a batch, misses start.
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "2")
|
|
rmsg, err = nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
pubAck = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_Error(t, pubAck.Error, NewJSAtomicPublishIncompleteBatchError())
|
|
|
|
// Publish a "batch" which immediately commits.
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
rmsg, err = nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
pubAck = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_Equal(t, pubAck.Sequence, 1)
|
|
require_Equal(t, pubAck.BatchId, "uuid")
|
|
require_Equal(t, pubAck.BatchSize, 1)
|
|
|
|
// Reset commit.
|
|
m.Header.Del("Nats-Batch-Commit")
|
|
|
|
// Publish a "batch" which has gaps.
|
|
require_NoError(t, nc.PublishMsg(m))
|
|
m.Header.Set("Nats-Batch-Sequence", "3")
|
|
rmsg, err = nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
pubAck = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_Error(t, pubAck.Error, NewJSAtomicPublishIncompleteBatchError())
|
|
|
|
// Publish a batch of N messages.
|
|
m.Header.Del("Nats-Batch-Commit")
|
|
for seq, batch := uint64(1), uint64(5); seq <= batch; seq++ {
|
|
m.Subject = fmt.Sprintf("foo.%d", seq)
|
|
m.Data = []byte(m.Subject)
|
|
m.Header.Set("Nats-Batch-Sequence", strconv.FormatUint(seq, 10))
|
|
// If not commit.
|
|
if seq != batch {
|
|
require_NoError(t, nc.PublishMsg(m))
|
|
continue
|
|
}
|
|
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
rmsg, err = nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
|
|
pubAck = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_Equal(t, pubAck.Sequence, 6)
|
|
require_Equal(t, pubAck.BatchId, "uuid")
|
|
require_Equal(t, pubAck.BatchSize, 5)
|
|
}
|
|
|
|
// Validate stream contents.
|
|
if retention != InterestPolicy {
|
|
for i := 0; i < 6; i++ {
|
|
rsm, err := js.GetMsg("TEST", uint64(i+1))
|
|
require_NoError(t, err)
|
|
subj := fmt.Sprintf("foo.%d", i)
|
|
require_Equal(t, rsm.Subject, subj)
|
|
require_Equal(t, string(rsm.Data), subj)
|
|
}
|
|
}
|
|
}
|
|
|
|
for _, storage := range []StorageType{FileStorage, MemoryStorage} {
|
|
for _, retention := range []RetentionPolicy{LimitsPolicy, InterestPolicy, WorkQueuePolicy} {
|
|
for _, replicas := range []int{1, 3} {
|
|
t.Run(fmt.Sprintf("%s/%s/R%d", storage, retention, replicas), func(t *testing.T) {
|
|
test(t, storage, retention, replicas)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishEmptyAck(t *testing.T) {
|
|
test := func(
|
|
t *testing.T,
|
|
storage StorageType,
|
|
retention RetentionPolicy,
|
|
replicas int,
|
|
) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc := clientConnectToServer(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
var pubAck JSPubAckResponse
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo.*"},
|
|
Storage: storage,
|
|
Retention: retention,
|
|
Replicas: replicas,
|
|
AllowAtomicPublish: true,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
// Publish a batch of N messages.
|
|
for seq, batch := uint64(1), uint64(5); seq <= batch; seq++ {
|
|
m := nats.NewMsg(fmt.Sprintf("foo.%d", seq))
|
|
m.Data = []byte(m.Subject)
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", strconv.FormatUint(seq, 10))
|
|
commit := seq == batch
|
|
if commit {
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
}
|
|
|
|
rmsg, err := nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
// If not commit we should receive an empty ack for flow control.
|
|
if !commit {
|
|
require_Len(t, len(rmsg.Data), 0)
|
|
continue
|
|
}
|
|
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_Equal(t, pubAck.Sequence, 5)
|
|
require_Equal(t, pubAck.BatchId, "uuid")
|
|
require_Equal(t, pubAck.BatchSize, 5)
|
|
}
|
|
}
|
|
|
|
for _, storage := range []StorageType{FileStorage, MemoryStorage} {
|
|
for _, retention := range []RetentionPolicy{LimitsPolicy, InterestPolicy, WorkQueuePolicy} {
|
|
for _, replicas := range []int{1, 3} {
|
|
t.Run(fmt.Sprintf("%s/%s/R%d", storage, retention, replicas), func(t *testing.T) {
|
|
test(t, storage, retention, replicas)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishLimits(t *testing.T) {
|
|
streamMaxBatchInflightPerStream = 1
|
|
streamMaxBatchInflightTotal = 1
|
|
streamMaxBatchSize = 2
|
|
streamMaxBatchTimeout = 500 * time.Millisecond
|
|
defer func() {
|
|
streamMaxBatchInflightPerStream = streamDefaultMaxBatchInflightPerStream
|
|
streamMaxBatchInflightTotal = streamDefaultMaxBatchInflightTotal
|
|
streamMaxBatchSize = streamDefaultMaxBatchSize
|
|
streamMaxBatchTimeout = streamDefaultMaxBatchTimeout
|
|
}()
|
|
|
|
test := func(t *testing.T, replicas int) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc := clientConnectToServer(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
var pubAck JSPubAckResponse
|
|
|
|
cfg := &StreamConfig{
|
|
Name: "FOO",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
Retention: LimitsPolicy,
|
|
Replicas: replicas,
|
|
AllowAtomicPublish: true,
|
|
}
|
|
_, err := jsStreamCreate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
|
|
// For testing total server-wide maximum inflight batches.
|
|
cfg = &StreamConfig{
|
|
Name: "BAR",
|
|
Subjects: []string{"bar"},
|
|
Storage: FileStorage,
|
|
Retention: LimitsPolicy,
|
|
Replicas: replicas,
|
|
AllowAtomicPublish: true,
|
|
}
|
|
_, err = jsStreamCreate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
|
|
// A batch ID must not exceed the maximum length.
|
|
for _, length := range []int{64, 65} {
|
|
longBatchId := strings.Repeat("A", length)
|
|
m := nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", longBatchId)
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
rmsg, err := nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
pubAck = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
if length <= 64 {
|
|
require_True(t, pubAck.Error == nil)
|
|
} else {
|
|
require_NotNil(t, pubAck.Error)
|
|
require_Error(t, pubAck.Error, NewJSAtomicPublishInvalidBatchIDError())
|
|
}
|
|
}
|
|
|
|
// One batch is inflight.
|
|
m := nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
require_NoError(t, nc.PublishMsg(m))
|
|
|
|
// Another batch moves over the threshold and batch is denied.
|
|
m = nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "exceeds_threshold")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
rmsg, err := nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
pubAck = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_Error(t, pubAck.Error, NewJSAtomicPublishIncompleteBatchError())
|
|
|
|
// Another batch on a different stream moves over the server-wide threshold and batch is denied.
|
|
m = nats.NewMsg("bar")
|
|
m.Header.Set("Nats-Batch-Id", "bar")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
rmsg, err = nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
pubAck = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_Error(t, pubAck.Error, NewJSAtomicPublishIncompleteBatchError())
|
|
|
|
// The first batch should now time out.
|
|
sl := c.streamLeader(globalAccountName, "FOO")
|
|
mset, err := sl.globalAccount().lookupStream("FOO")
|
|
require_NoError(t, err)
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
mset.mu.RLock()
|
|
batches := mset.batches
|
|
mset.mu.RUnlock()
|
|
if batches == nil {
|
|
return errors.New("batches not found")
|
|
}
|
|
batches.mu.Lock()
|
|
groups := len(batches.group)
|
|
batches.mu.Unlock()
|
|
if groups != 0 {
|
|
return fmt.Errorf("expected 0 groups, got %d", groups)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Publishing to the batch should also error since it timed out.
|
|
m = nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "2")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
rmsg, err = nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
pubAck = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_Error(t, pubAck.Error, NewJSAtomicPublishIncompleteBatchError())
|
|
|
|
// Batch should be rejected if going over the max batch size.
|
|
for _, size := range []int{streamMaxBatchSize, streamMaxBatchSize + 1} {
|
|
for i := range size {
|
|
seq := i + 1
|
|
m = nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "exceeds_threshold")
|
|
m.Header.Set("Nats-Batch-Sequence", strconv.Itoa(seq))
|
|
commit := seq == size
|
|
if !commit {
|
|
require_NoError(t, nc.PublishMsg(m))
|
|
continue
|
|
}
|
|
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
rmsg, err = nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
pubAck = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
if size <= streamMaxBatchSize {
|
|
require_True(t, pubAck.Error == nil)
|
|
require_Equal(t, pubAck.BatchSize, size)
|
|
} else {
|
|
require_Error(t, pubAck.Error, NewJSAtomicPublishTooLargeBatchError(streamMaxBatchSize))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
t.Run("R1", func(t *testing.T) { test(t, 1) })
|
|
t.Run("R3", func(t *testing.T) { test(t, 3) })
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishDedupeNotAllowed(t *testing.T) {
|
|
test := func(
|
|
t *testing.T,
|
|
nc *nats.Conn,
|
|
js nats.JetStreamContext,
|
|
storage StorageType,
|
|
retention RetentionPolicy,
|
|
replicas int,
|
|
) {
|
|
cfg := &StreamConfig{
|
|
Name: "TEST",
|
|
Retention: retention,
|
|
Subjects: []string{"foo"},
|
|
Replicas: replicas,
|
|
Storage: storage,
|
|
AllowAtomicPublish: true,
|
|
}
|
|
|
|
_, err := jsStreamCreate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
|
|
_, err = js.Publish("foo", nil, nats.MsgId("pre-existing"))
|
|
require_NoError(t, err)
|
|
|
|
var pubAck JSPubAckResponse
|
|
m := nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Msg-Id", "pre-existing")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
rmsg, err := nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_NotNil(t, pubAck.Error)
|
|
require_Error(t, pubAck.Error, NewJSAtomicPublishContainsDuplicateMessageError())
|
|
|
|
m = nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
m.Header.Set("Nats-Msg-Id", "msgId1")
|
|
require_NoError(t, nc.PublishMsg(m))
|
|
|
|
pubAck = JSPubAckResponse{}
|
|
m.Header.Set("Nats-Batch-Sequence", "2")
|
|
m.Header.Set("Nats-Msg-Id", "msgId2")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
rmsg, err = nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_True(t, pubAck.Error == nil)
|
|
require_Equal(t, pubAck.BatchSize, 2)
|
|
require_Equal(t, pubAck.Sequence, 3)
|
|
require_Equal(t, pubAck.BatchId, "uuid")
|
|
}
|
|
|
|
for _, storage := range []StorageType{FileStorage, MemoryStorage} {
|
|
for _, retention := range []RetentionPolicy{LimitsPolicy, InterestPolicy, WorkQueuePolicy} {
|
|
for _, replicas := range []int{1, 3} {
|
|
t.Run(fmt.Sprintf("%s/%s/R%d", storage, retention, replicas), func(t *testing.T) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
test(t, nc, js, storage, retention, replicas)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishSourceAndMirror(t *testing.T) {
|
|
test := func(t *testing.T, replicas int) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
AllowAtomicPublish: true,
|
|
Replicas: replicas,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
for seq := uint64(1); seq <= 3; seq++ {
|
|
m := nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", strconv.FormatUint(seq, 10))
|
|
commit := seq == 3
|
|
if !commit {
|
|
require_NoError(t, nc.PublishMsg(m))
|
|
continue
|
|
}
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
|
|
rmsg, err := nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
var pubAck JSPubAckResponse
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_Equal(t, pubAck.Sequence, 3)
|
|
require_Equal(t, pubAck.BatchId, "uuid")
|
|
require_Equal(t, pubAck.BatchSize, 3)
|
|
}
|
|
|
|
require_NoError(t, js.DeleteMsg("TEST", 2))
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
return checkState(t, c, globalAccountName, "TEST")
|
|
})
|
|
|
|
// Mirror can source batched messages but can't do atomic batching itself.
|
|
_, err = jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "M-no-batch",
|
|
Storage: FileStorage,
|
|
Mirror: &StreamSource{Name: "TEST"},
|
|
Replicas: replicas,
|
|
AllowAtomicPublish: true,
|
|
})
|
|
require_Error(t, err, NewJSMirrorWithAtomicPublishError())
|
|
|
|
_, err = js.AddStream(&nats.StreamConfig{
|
|
Name: "M",
|
|
Mirror: &nats.StreamSource{Name: "TEST"},
|
|
Replicas: replicas,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
_, err = js.AddStream(&nats.StreamConfig{
|
|
Name: "S",
|
|
Sources: []*nats.StreamSource{{Name: "TEST"}},
|
|
Replicas: replicas,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
for _, name := range []string{"M", "S"} {
|
|
if si, err := js.StreamInfo(name); err != nil {
|
|
return err
|
|
} else if si.State.Msgs != 2 {
|
|
return fmt.Errorf("expected 2 messages for stream %q, got %d", name, si.State.Msgs)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Ensure the batching headers were removed when ingested into the source/mirror.
|
|
rsm, err := js.GetMsg("M", 1)
|
|
require_NoError(t, err)
|
|
require_Len(t, len(rsm.Header), 0)
|
|
|
|
rsm, err = js.GetMsg("M", 3)
|
|
require_NoError(t, err)
|
|
require_Len(t, len(rsm.Header), 0)
|
|
|
|
rsm, err = js.GetMsg("S", 1)
|
|
require_NoError(t, err)
|
|
require_Len(t, len(rsm.Header), 1)
|
|
require_Equal(t, rsm.Header.Get(JSStreamSource), "TEST 1 > > foo")
|
|
|
|
rsm, err = js.GetMsg("S", 2)
|
|
require_NoError(t, err)
|
|
require_Len(t, len(rsm.Header), 1)
|
|
require_Equal(t, rsm.Header.Get(JSStreamSource), "TEST 3 > > foo")
|
|
}
|
|
|
|
t.Run("R1", func(t *testing.T) { test(t, 1) })
|
|
t.Run("R3", func(t *testing.T) { test(t, 3) })
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishCleanup(t *testing.T) {
|
|
const (
|
|
Disable = iota
|
|
StepDown
|
|
Delete
|
|
Commit
|
|
)
|
|
|
|
test := func(t *testing.T, mode int) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
cfg := &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
AllowAtomicPublish: false,
|
|
Replicas: 3,
|
|
}
|
|
_, err := jsStreamCreate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
|
|
sl := c.streamLeader(globalAccountName, "TEST")
|
|
mset, err := sl.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
mset.mu.RLock()
|
|
batches := mset.batches
|
|
batch := mset.batchApply
|
|
mset.mu.RUnlock()
|
|
require_True(t, batches == nil)
|
|
require_True(t, batch == nil)
|
|
|
|
// Enabling doesn't need to populate the batching state.
|
|
cfg.AllowAtomicPublish = true
|
|
_, err = jsStreamUpdate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
mset.mu.RLock()
|
|
batches = mset.batches
|
|
batch = mset.batchApply
|
|
mset.mu.RUnlock()
|
|
require_True(t, batches == nil)
|
|
require_True(t, batch == nil)
|
|
|
|
// Publish a partial batch that needs to be cleaned up.
|
|
m := nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
require_NoError(t, nc.PublishMsg(m))
|
|
|
|
// Publish another batch that commits, state should be cleaned up by default.
|
|
m.Header.Set("Nats-Batch-Id", "commit")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
_, err = js.PublishMsg(m)
|
|
require_NoError(t, err)
|
|
|
|
mset.mu.RLock()
|
|
batches = mset.batches
|
|
batch = mset.batchApply
|
|
mset.mu.RUnlock()
|
|
require_NotNil(t, batches)
|
|
require_NotNil(t, batch)
|
|
batches.mu.Lock()
|
|
groups := len(batches.group)
|
|
b := batches.group["uuid"]
|
|
batches.mu.Unlock()
|
|
require_Len(t, groups, 1)
|
|
require_NotNil(t, b)
|
|
store := b.store
|
|
require_Equal(t, store.State().Msgs, 1)
|
|
clfs := mset.getCLFS()
|
|
|
|
// Should fully clean up the in-progress batch.
|
|
switch mode {
|
|
case Disable:
|
|
cfg.AllowAtomicPublish = false
|
|
_, err = jsStreamUpdate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
case StepDown:
|
|
require_NoError(t, sl.JetStreamStepdownStream(globalAccountName, "TEST"))
|
|
case Delete:
|
|
require_NoError(t, mset.delete())
|
|
case Commit:
|
|
m = nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "2")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
_, err = js.PublishMsg(m)
|
|
require_NoError(t, err)
|
|
}
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
mset.mu.RLock()
|
|
batches = mset.batches
|
|
mset.mu.RUnlock()
|
|
if batches != nil {
|
|
if mode != Commit {
|
|
return fmt.Errorf("expected no batches")
|
|
}
|
|
batches.mu.Lock()
|
|
groups = len(batches.group)
|
|
batches.mu.Unlock()
|
|
if groups > 0 {
|
|
return fmt.Errorf("expected 0 groups, got %d", groups)
|
|
}
|
|
}
|
|
if msgs := store.State().Msgs; msgs != 0 {
|
|
return fmt.Errorf("expected 0 messages, got %d", msgs)
|
|
}
|
|
return nil
|
|
})
|
|
// Should clean up the batch apply state.
|
|
if mode == Disable || mode == Delete {
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
mset.mu.RLock()
|
|
batch = mset.batchApply
|
|
mset.mu.RUnlock()
|
|
nclfs := mset.getCLFS()
|
|
if batch != nil {
|
|
return fmt.Errorf("expected no batch apply")
|
|
}
|
|
if clfs != nclfs {
|
|
return fmt.Errorf("expected no change in CLFS")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
|
|
t.Run("Disable", func(t *testing.T) { test(t, Disable) })
|
|
t.Run("StepDown", func(t *testing.T) { test(t, StepDown) })
|
|
t.Run("Delete", func(t *testing.T) { test(t, Delete) })
|
|
t.Run("Commit", func(t *testing.T) { test(t, Commit) })
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishConfigOpts(t *testing.T) {
|
|
// Defaults.
|
|
require_Equal(t, streamMaxBatchInflightPerStream, 50)
|
|
require_Equal(t, streamMaxBatchInflightTotal, 1000)
|
|
require_Equal(t, streamMaxBatchSize, 1000)
|
|
require_Equal(t, streamMaxBatchTimeout, 10*time.Second)
|
|
|
|
batchingConf := `
|
|
listen: 127.0.0.1:-1
|
|
jetstream {
|
|
store_dir: %q
|
|
limits {
|
|
batch {
|
|
max_inflight_per_stream: %d
|
|
max_inflight_total: %d
|
|
max_msgs: %d
|
|
timeout: %s
|
|
}
|
|
}
|
|
}`
|
|
|
|
cf := createConfFile(t, []byte(fmt.Sprintf(batchingConf, t.TempDir(), 10, 20, 100, "5s")))
|
|
s, _ := RunServerWithConfig(cf)
|
|
defer s.Shutdown()
|
|
|
|
opts := s.getOpts()
|
|
require_Equal(t, opts.JetStreamLimits.MaxBatchInflightPerStream, 10)
|
|
require_Equal(t, opts.JetStreamLimits.MaxBatchInflightTotal, 20)
|
|
require_Equal(t, opts.JetStreamLimits.MaxBatchSize, 100)
|
|
require_Equal(t, opts.JetStreamLimits.MaxBatchTimeout, 5*time.Second)
|
|
|
|
// Reloading is not supported, that would potentially mean dropping random batches when lowering limits.
|
|
changeCurrentConfigContentWithNewContent(t, cf, []byte(fmt.Sprintf(batchingConf, t.TempDir(), 20, 40, 200, "10s")))
|
|
require_Error(t, s.Reload(), fmt.Errorf("config reload not supported for JetStreamLimits"))
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishDenyHeaders(t *testing.T) {
|
|
test := func(t *testing.T, replicas int) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
cfg := &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
AllowAtomicPublish: true,
|
|
Replicas: replicas,
|
|
}
|
|
_, err := jsStreamCreate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
|
|
// We might support these headers later on, but for now error.
|
|
for key, value := range map[string]string{
|
|
"Nats-Expected-Last-Msg-Id": "msgId",
|
|
} {
|
|
t.Run(key, func(t *testing.T) {
|
|
m := nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
m.Header.Set(key, value)
|
|
_, err = js.PublishMsg(m)
|
|
require_Error(t, err, NewJSAtomicPublishUnsupportedHeaderBatchError(key))
|
|
})
|
|
}
|
|
}
|
|
|
|
t.Run("R1", func(t *testing.T) { test(t, 1) })
|
|
t.Run("R3", func(t *testing.T) { test(t, 3) })
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishStageAndCommit(t *testing.T) {
|
|
type BatchItem struct {
|
|
subject string
|
|
header nats.Header
|
|
msg []byte
|
|
err error
|
|
}
|
|
|
|
type BatchTest struct {
|
|
title string
|
|
allowRollup bool
|
|
denyPurge bool
|
|
allowTTL bool
|
|
allowMsgCounter bool
|
|
allowMsgSchedules bool
|
|
discardNew bool
|
|
discardNewPerSubj bool
|
|
init func(mset *stream)
|
|
batch []BatchItem
|
|
validate func(mset *stream, commit bool)
|
|
}
|
|
|
|
tests := []BatchTest{
|
|
{
|
|
title: "dedupe-distinct",
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSMsgId: {"foo"}}},
|
|
{subject: "bar", header: nats.Header{JSMsgId: {"bar"}}},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
require_Equal(t, mset.checkMsgId("foo") != nil, commit)
|
|
require_Equal(t, mset.checkMsgId("bar") != nil, commit)
|
|
},
|
|
},
|
|
{
|
|
title: "dedupe",
|
|
init: func(mset *stream) {
|
|
mset.storeMsgId(&ddentry{id: "foo", seq: 0, ts: time.Now().UnixNano()})
|
|
},
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSMsgId: {"foo"}}, err: errMsgIdDuplicate},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
require_NotNil(t, mset.checkMsgId("foo"))
|
|
},
|
|
},
|
|
{
|
|
title: "dedupe-staged",
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSMsgId: {"foo"}}},
|
|
{subject: "foo", header: nats.Header{JSMsgId: {"foo"}}, err: errMsgIdDuplicate},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
require_Equal(t, mset.checkMsgId("foo") != nil, commit)
|
|
},
|
|
},
|
|
{
|
|
title: "counter-single",
|
|
allowMsgCounter: true,
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSMessageIncr: {"1"}}},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
if !commit {
|
|
require_True(t, mset.clusteredCounterTotal["foo"] == nil)
|
|
} else {
|
|
counter := mset.clusteredCounterTotal["foo"]
|
|
require_NotNil(t, counter)
|
|
require_Equal(t, counter.ops, 1)
|
|
require_Equal(t, counter.total.String(), "1")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
title: "counter-multiple",
|
|
allowMsgCounter: true,
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSMessageIncr: {"1"}}},
|
|
{subject: "foo", header: nats.Header{JSMessageIncr: {"2"}}},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
if !commit {
|
|
require_True(t, mset.clusteredCounterTotal["foo"] == nil)
|
|
} else {
|
|
counter := mset.clusteredCounterTotal["foo"]
|
|
require_NotNil(t, counter)
|
|
require_Equal(t, counter.ops, 2)
|
|
require_Equal(t, counter.total.String(), "3")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
title: "counter-pre-init",
|
|
allowMsgCounter: true,
|
|
init: func(mset *stream) {
|
|
mset.clusteredCounterTotal = map[string]*msgCounterRunningTotal{
|
|
"foo": {total: big.NewInt(1), ops: 1},
|
|
}
|
|
},
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSMessageIncr: {"2"}}},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
counter := mset.clusteredCounterTotal["foo"]
|
|
require_NotNil(t, counter)
|
|
if !commit {
|
|
require_Equal(t, counter.ops, 1)
|
|
require_Equal(t, counter.total.String(), "1")
|
|
} else {
|
|
require_Equal(t, counter.ops, 2)
|
|
require_Equal(t, counter.total.String(), "3")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
title: "msg-schedules-disabled",
|
|
allowMsgSchedules: false,
|
|
batch: []BatchItem{
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{JSSchedulePattern: {"@at 1970-01-01T00:00:00Z"}},
|
|
err: NewJSMessageSchedulesDisabledError(),
|
|
},
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{JSSchedulePattern: {"disabled"}},
|
|
err: NewJSMessageSchedulesDisabledError(),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "msg-schedules-ttl-disabled",
|
|
allowMsgSchedules: true,
|
|
batch: []BatchItem{
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{
|
|
JSSchedulePattern: {"@at 1970-01-01T00:00:00Z"},
|
|
JSScheduleTTL: {"1s"},
|
|
},
|
|
err: errMsgTTLDisabled,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "msg-schedules-ttl-invalid",
|
|
allowMsgSchedules: true,
|
|
allowTTL: true,
|
|
batch: []BatchItem{
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{
|
|
JSSchedulePattern: {"@at 1970-01-01T00:00:00Z"},
|
|
JSScheduleTTL: {"invalid"},
|
|
},
|
|
err: NewJSMessageSchedulesTTLInvalidError(),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "msg-schedules-invalid-schedule",
|
|
allowMsgSchedules: true,
|
|
batch: []BatchItem{
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{JSSchedulePattern: {"invalid"}},
|
|
err: NewJSMessageSchedulesPatternInvalidError(),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "msg-schedules-target-mismatch",
|
|
allowMsgSchedules: true,
|
|
batch: []BatchItem{
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{
|
|
JSSchedulePattern: {"@at 1970-01-01T00:00:00Z"},
|
|
JSScheduleTarget: {"not.matching"},
|
|
},
|
|
err: NewJSMessageSchedulesTargetInvalidError(),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "msg-schedules-target-must-be-literal",
|
|
allowMsgSchedules: true,
|
|
batch: []BatchItem{
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{
|
|
JSSchedulePattern: {"@at 1970-01-01T00:00:00Z"},
|
|
JSScheduleTarget: {"foo.*"},
|
|
},
|
|
err: NewJSMessageSchedulesTargetInvalidError(),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "msg-schedules-target-must-be-unique",
|
|
allowMsgSchedules: true,
|
|
batch: []BatchItem{
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{
|
|
JSSchedulePattern: {"@at 1970-01-01T00:00:00Z"},
|
|
JSScheduleTarget: {"foo"},
|
|
},
|
|
err: NewJSMessageSchedulesTargetInvalidError(),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "msg-schedules-rollup-disabled",
|
|
allowMsgSchedules: true,
|
|
batch: []BatchItem{
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{
|
|
JSSchedulePattern: {"@at 1970-01-01T00:00:00Z"},
|
|
JSScheduleTarget: {"bar"},
|
|
JSMsgRollup: {JSMsgRollupSubject},
|
|
},
|
|
err: errors.New("rollup not permitted"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "msg-schedules",
|
|
allowMsgSchedules: true,
|
|
allowRollup: true,
|
|
batch: []BatchItem{
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{
|
|
JSSchedulePattern: {"@at 1970-01-01T00:00:00Z"},
|
|
JSScheduleTarget: {"baz"},
|
|
},
|
|
},
|
|
{
|
|
subject: "bar",
|
|
header: nats.Header{
|
|
JSSchedulePattern: {"@at 1970-01-01T00:00:00Z"},
|
|
JSScheduleTarget: {"baz"},
|
|
JSMsgRollup: {JSMsgRollupSubject},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "discard-new",
|
|
discardNew: true,
|
|
batch: []BatchItem{
|
|
{subject: "foo1"},
|
|
{subject: "foo2"},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
if !commit {
|
|
require_Len(t, len(mset.inflight), 0)
|
|
} else {
|
|
require_Len(t, len(mset.inflight), 2)
|
|
require_Equal(t, *mset.inflight["foo1"], inflightSubjectRunningTotal{bytes: 20, ops: 1})
|
|
require_Equal(t, *mset.inflight["foo2"], inflightSubjectRunningTotal{bytes: 20, ops: 1})
|
|
}
|
|
},
|
|
},
|
|
{
|
|
title: "discard-new-max-msgs",
|
|
discardNew: true,
|
|
batch: []BatchItem{
|
|
{subject: "foo"},
|
|
{subject: "foo"},
|
|
{subject: "foo", err: ErrMaxMsgs},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
if !commit {
|
|
require_Len(t, len(mset.inflight), 0)
|
|
} else {
|
|
require_Len(t, len(mset.inflight), 1)
|
|
require_Equal(t, *mset.inflight["foo"], inflightSubjectRunningTotal{bytes: 19 * 3, ops: 3})
|
|
}
|
|
},
|
|
},
|
|
{
|
|
title: "discard-new-max-bytes",
|
|
discardNew: true,
|
|
batch: []BatchItem{
|
|
{subject: "foo1", msg: bytes.Repeat([]byte("A"), 10)},
|
|
{subject: "foo2", msg: bytes.Repeat([]byte("A"), 11), err: ErrMaxBytes},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
if !commit {
|
|
require_Len(t, len(mset.inflight), 0)
|
|
} else {
|
|
require_Len(t, len(mset.inflight), 2)
|
|
require_Equal(t, *mset.inflight["foo1"], inflightSubjectRunningTotal{bytes: 30, ops: 1})
|
|
require_Equal(t, *mset.inflight["foo2"], inflightSubjectRunningTotal{bytes: 31, ops: 1})
|
|
}
|
|
},
|
|
},
|
|
{
|
|
title: "discard-new-max-msgs-per-subj",
|
|
discardNew: true,
|
|
discardNewPerSubj: true,
|
|
batch: []BatchItem{
|
|
{subject: "foo"},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
if !commit {
|
|
require_Len(t, len(mset.inflight), 0)
|
|
} else {
|
|
require_Len(t, len(mset.inflight), 1)
|
|
require_Equal(t, *mset.inflight["foo"], inflightSubjectRunningTotal{bytes: 19, ops: 1})
|
|
}
|
|
},
|
|
},
|
|
{
|
|
title: "discard-new-max-msgs-per-subj-duplicate",
|
|
discardNew: true,
|
|
discardNewPerSubj: true,
|
|
batch: []BatchItem{
|
|
{subject: "foo"},
|
|
{subject: "foo", err: ErrMaxMsgsPerSubject},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
require_Len(t, len(mset.inflight), 0)
|
|
},
|
|
},
|
|
{
|
|
title: "discard-new-max-msgs-per-subj-inflight",
|
|
discardNew: true,
|
|
discardNewPerSubj: true,
|
|
init: func(mset *stream) {
|
|
mset.inflight = map[string]*inflightSubjectRunningTotal{"foo": {bytes: 123, ops: 1}}
|
|
},
|
|
batch: []BatchItem{
|
|
{subject: "foo", err: ErrMaxMsgsPerSubject},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
require_Len(t, len(mset.inflight), 1)
|
|
require_Equal(t, *mset.inflight["foo"], inflightSubjectRunningTotal{bytes: 123, ops: 1})
|
|
},
|
|
},
|
|
{
|
|
title: "discard-new-max-msgs-per-subj-pre-existing",
|
|
discardNew: true,
|
|
discardNewPerSubj: true,
|
|
init: func(mset *stream) {
|
|
_, _, err := mset.store.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
},
|
|
batch: []BatchItem{
|
|
{subject: "foo", err: ErrMaxMsgsPerSubject},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
require_Len(t, len(mset.inflight), 0)
|
|
},
|
|
},
|
|
{
|
|
title: "expect-last-seq",
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSExpectedLastSeq: {"0"}}},
|
|
{subject: "bar"},
|
|
},
|
|
},
|
|
{
|
|
title: "expect-last-seq-not-first",
|
|
batch: []BatchItem{
|
|
{subject: "foo"},
|
|
{subject: "bar", header: nats.Header{JSExpectedLastSeq: {"0"}}, err: errors.New("last sequence mismatch")},
|
|
},
|
|
},
|
|
{
|
|
title: "expect-last-seq-invalid-first",
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSExpectedLastSeq: {"1"}}, err: errors.New("last sequence mismatch: 1 vs 0")},
|
|
},
|
|
},
|
|
{
|
|
title: "expect-last-seq-invalid",
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSExpectedLastSeq: {"0"}}},
|
|
{subject: "bar", header: nats.Header{JSExpectedLastSeq: {"0"}}, err: errors.New("last sequence mismatch: 0 vs 1")},
|
|
},
|
|
},
|
|
{
|
|
title: "expect-per-subj-simple",
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSExpectedLastSubjSeq: {"0"}}},
|
|
{subject: "bar", header: nats.Header{JSExpectedLastSubjSeq: {"0"}}},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
if !commit {
|
|
require_Len(t, len(mset.expectedPerSubjectSequence), 0)
|
|
require_Len(t, len(mset.expectedPerSubjectInProcess), 0)
|
|
} else {
|
|
require_Len(t, len(mset.expectedPerSubjectSequence), 2)
|
|
require_Len(t, len(mset.expectedPerSubjectInProcess), 2)
|
|
require_Equal(t, mset.expectedPerSubjectSequence[0], "foo")
|
|
require_Equal(t, mset.expectedPerSubjectSequence[1], "bar")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
title: "expect-per-subj-redundant-in-batch",
|
|
batch: []BatchItem{
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{JSExpectedLastSubjSeq: {"0"}},
|
|
},
|
|
// Would normally fail the batch, recognize in-process.
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{JSExpectedLastSubjSeq: {"1"}},
|
|
err: errors.New("last sequence by subject mismatch: 1 vs 0"),
|
|
},
|
|
// Redundant expected check results in an error. The subject 'foo' is also updated in the batch.
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{JSExpectedLastSubjSeq: {"0"}},
|
|
err: errors.New("last sequence by subject mismatch"),
|
|
},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
if !commit {
|
|
require_Len(t, len(mset.expectedPerSubjectSequence), 0)
|
|
require_Len(t, len(mset.expectedPerSubjectInProcess), 0)
|
|
} else {
|
|
require_Len(t, len(mset.expectedPerSubjectSequence), 1)
|
|
require_Len(t, len(mset.expectedPerSubjectInProcess), 1)
|
|
require_Equal(t, mset.expectedPerSubjectSequence[0], "foo")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
title: "expect-per-subj-dupe-in-change",
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSExpectedLastSubjSeq: {"0"}, JSExpectedLastSubjSeqSubj: {"baz"}}},
|
|
{subject: "bar", header: nats.Header{JSExpectedLastSubjSeq: {"0"}, JSExpectedLastSubjSeqSubj: {"baz"}}},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
if !commit {
|
|
require_Len(t, len(mset.expectedPerSubjectSequence), 0)
|
|
require_Len(t, len(mset.expectedPerSubjectInProcess), 0)
|
|
} else {
|
|
require_Len(t, len(mset.expectedPerSubjectSequence), 1)
|
|
require_Len(t, len(mset.expectedPerSubjectInProcess), 1)
|
|
require_Equal(t, mset.expectedPerSubjectSequence[1], "baz")
|
|
}
|
|
},
|
|
},
|
|
{
|
|
title: "expect-per-subj-not-first",
|
|
batch: []BatchItem{
|
|
{subject: "foo"},
|
|
// Mismatch because only the first 'foo' can have the expected check.
|
|
{
|
|
subject: "foo",
|
|
header: nats.Header{JSExpectedLastSubjSeq: {"0"}},
|
|
err: errors.New("last sequence by subject mismatch"),
|
|
},
|
|
// Mismatch because only the first 'foo' can have the expected check.
|
|
{
|
|
subject: "bar",
|
|
header: nats.Header{JSExpectedLastSubjSeq: {"0"}, JSExpectedLastSubjSeqSubj: {"foo"}},
|
|
err: errors.New("last sequence by subject mismatch"),
|
|
},
|
|
},
|
|
},
|
|
{
|
|
title: "expect-per-subj-in-process",
|
|
init: func(mset *stream) {
|
|
mset.expectedPerSubjectInProcess = map[string]struct{}{"foo": {}}
|
|
},
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSExpectedLastSubjSeq: {"10"}}, err: errors.New("last sequence by subject mismatch")},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
require_Len(t, len(mset.expectedPerSubjectSequence), 0)
|
|
require_Len(t, len(mset.expectedPerSubjectInProcess), 1)
|
|
},
|
|
},
|
|
{
|
|
title: "expect-per-subj-inflight",
|
|
init: func(mset *stream) {
|
|
mset.inflight = map[string]*inflightSubjectRunningTotal{"bar": {bytes: 33, ops: 1}}
|
|
},
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSExpectedLastSubjSeq: {"10"}, JSExpectedLastSubjSeqSubj: {"bar"}}, err: errors.New("last sequence by subject mismatch")},
|
|
},
|
|
validate: func(mset *stream, commit bool) {
|
|
require_Len(t, len(mset.expectedPerSubjectSequence), 0)
|
|
require_Len(t, len(mset.expectedPerSubjectInProcess), 0)
|
|
require_Len(t, len(mset.inflight), 1)
|
|
},
|
|
},
|
|
{
|
|
title: "rollup-deny-purge",
|
|
allowRollup: true,
|
|
denyPurge: true,
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSMsgRollup: {JSMsgRollupSubject}}, err: errors.New("rollup not permitted")},
|
|
},
|
|
},
|
|
{
|
|
title: "rollup-invalid",
|
|
allowRollup: true,
|
|
denyPurge: false,
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSMsgRollup: {"invalid"}}, err: fmt.Errorf("rollup value invalid: %q", "invalid")},
|
|
},
|
|
},
|
|
{
|
|
title: "rollup-all-first",
|
|
allowRollup: true,
|
|
denyPurge: false,
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSMsgRollup: {JSMsgRollupAll}}},
|
|
},
|
|
},
|
|
{
|
|
title: "rollup-all-not-first",
|
|
allowRollup: true,
|
|
denyPurge: false,
|
|
batch: []BatchItem{
|
|
{subject: "foo"},
|
|
{subject: "bar", header: nats.Header{JSMsgRollup: {JSMsgRollupAll}}, err: errors.New("batch rollup all invalid")},
|
|
},
|
|
},
|
|
{
|
|
title: "rollup-sub-unique",
|
|
allowRollup: true,
|
|
denyPurge: false,
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSMsgRollup: {JSMsgRollupSubject}}},
|
|
{subject: "bar", header: nats.Header{JSMsgRollup: {JSMsgRollupSubject}}},
|
|
},
|
|
},
|
|
{
|
|
title: "rollup-sub-overlap",
|
|
allowRollup: true,
|
|
denyPurge: false,
|
|
batch: []BatchItem{
|
|
{subject: "foo", header: nats.Header{JSMsgRollup: {JSMsgRollupSubject}}},
|
|
{subject: "foo", header: nats.Header{JSMsgRollup: {JSMsgRollupSubject}}, err: errors.New("batch rollup sub invalid")},
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.title, func(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
nc := clientConnectToServer(t, s)
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Storage: MemoryStorage,
|
|
AllowAtomicPublish: true,
|
|
Subjects: []string{"foo", "bar", "baz"},
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
mset, err := s.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
if test.init != nil {
|
|
test.init(mset)
|
|
}
|
|
|
|
var (
|
|
discard DiscardPolicy
|
|
discardNewPer bool
|
|
maxMsgs int64 = -1
|
|
maxMsgsPer int64 = -1
|
|
maxBytes int64 = -1
|
|
)
|
|
if test.discardNew {
|
|
discard, maxMsgs, maxBytes = DiscardNew, 2, 60
|
|
}
|
|
if test.discardNewPerSubj {
|
|
require_True(t, test.discardNew)
|
|
discardNewPer, maxMsgs, maxBytes, maxMsgsPer = true, -1, -1, 1
|
|
}
|
|
|
|
diff := &batchStagedDiff{}
|
|
mset.clMu.Lock()
|
|
defer mset.clMu.Unlock()
|
|
var hadError bool
|
|
for _, m := range test.batch {
|
|
var hdr []byte
|
|
for key, values := range m.header {
|
|
for _, value := range values {
|
|
hdr = genHeader(hdr, key, value)
|
|
}
|
|
}
|
|
_, _, _, _, err = checkMsgHeadersPreClusteredProposal(diff, mset, m.subject, hdr, m.msg, false, "TEST", nil, test.allowRollup, test.denyPurge, test.allowTTL, test.allowMsgCounter, test.allowMsgSchedules, discard, discardNewPer, -1, maxMsgs, maxMsgsPer, maxBytes)
|
|
if m.err != nil {
|
|
require_Error(t, err, m.err)
|
|
} else if err != nil {
|
|
require_NoError(t, err)
|
|
}
|
|
if err != nil {
|
|
hadError = true
|
|
}
|
|
if test.validate != nil {
|
|
test.validate(mset, false)
|
|
}
|
|
mset.clseq++
|
|
}
|
|
if test.validate != nil && !hadError {
|
|
diff.commit(mset)
|
|
test.validate(mset, true)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishHighLevelRollback(t *testing.T) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Replicas: 3,
|
|
Storage: FileStorage,
|
|
Retention: InterestPolicy,
|
|
MaxMsgs: 1,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
sl := c.streamLeader(globalAccountName, "TEST")
|
|
mset, err := sl.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
requireEmpty := func() {
|
|
mset.ddMu.Lock()
|
|
ddarr, ddmap := len(mset.ddarr), len(mset.ddmap)
|
|
mset.ddMu.Unlock()
|
|
require_Len(t, ddarr, 0)
|
|
require_Len(t, ddmap, 0)
|
|
|
|
mset.clMu.Lock()
|
|
inflight, subjSeq, subjInProcess := len(mset.inflight), len(mset.expectedPerSubjectSequence), len(mset.expectedPerSubjectInProcess)
|
|
mset.clMu.Unlock()
|
|
require_Len(t, inflight, 0)
|
|
require_Len(t, subjSeq, 0)
|
|
require_Len(t, subjInProcess, 0)
|
|
}
|
|
requireEmpty()
|
|
|
|
m := nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Msg-Id", "dedupe")
|
|
m.Header.Set("Nats-Expected-Last-Subject-Sequence", "1")
|
|
_, err = js.PublishMsg(m)
|
|
require_Error(t, err, NewJSStreamWrongLastSequenceError(0))
|
|
requireEmpty()
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishExpectedPerSubject(t *testing.T) {
|
|
type TestKind int
|
|
const (
|
|
OnlyFirst TestKind = iota
|
|
Redundant
|
|
NotFirst
|
|
)
|
|
|
|
test := func(t *testing.T, kind TestKind) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
Replicas: 3,
|
|
AllowAtomicPublish: true,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
_, err = js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
|
|
m := nats.NewMsg("foo")
|
|
if kind != NotFirst {
|
|
m.Header.Set("Nats-Expected-Last-Subject-Sequence", "1")
|
|
}
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
require_NoError(t, nc.PublishMsg(m))
|
|
|
|
var pubAck JSPubAckResponse
|
|
|
|
// Redundant expected headers are okay, as long as they reflect the state prior to the batch.
|
|
m = nats.NewMsg("foo")
|
|
if kind == Redundant || kind == NotFirst {
|
|
m.Header.Set("Nats-Expected-Last-Subject-Sequence", "1")
|
|
}
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "2")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
rmsg, err := nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
if kind == Redundant || kind == NotFirst {
|
|
require_NotNil(t, pubAck.Error)
|
|
require_Error(t, pubAck.Error, NewJSStreamWrongLastSequenceConstantError())
|
|
return
|
|
}
|
|
require_True(t, pubAck.Error == nil)
|
|
require_Equal(t, pubAck.Sequence, 3)
|
|
require_Equal(t, pubAck.BatchSize, 2)
|
|
|
|
// The first message still contains the expected headers.
|
|
rsm, err := js.GetMsg("TEST", 2)
|
|
require_NoError(t, err)
|
|
require_Equal(t, rsm.Header.Get("Nats-Batch-Id"), "uuid")
|
|
require_Equal(t, rsm.Header.Get("Nats-Batch-Sequence"), "1")
|
|
require_Equal(t, rsm.Header.Get("Nats-Expected-Last-Subject-Sequence"), "1")
|
|
|
|
// The second message doesn't have the expected headers, as the condition was already checked
|
|
// and seems inconsistent when getting the message afterward.
|
|
rsm, err = js.GetMsg("TEST", 3)
|
|
require_NoError(t, err)
|
|
require_Equal(t, rsm.Header.Get("Nats-Batch-Id"), "uuid")
|
|
require_Equal(t, rsm.Header.Get("Nats-Batch-Sequence"), "2")
|
|
require_Equal(t, rsm.Header.Get("Nats-Expected-Last-Subject-Sequence"), _EMPTY_)
|
|
}
|
|
|
|
t.Run("single", func(t *testing.T) { test(t, OnlyFirst) })
|
|
t.Run("redundant", func(t *testing.T) { test(t, Redundant) })
|
|
t.Run("not-first", func(t *testing.T) { test(t, NotFirst) })
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishSingleServerRecovery(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
nc := clientConnectToServer(t, s)
|
|
defer nc.Close()
|
|
|
|
cfg := &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
Retention: LimitsPolicy,
|
|
Replicas: 1,
|
|
AllowAtomicPublish: true,
|
|
}
|
|
_, err := jsStreamCreate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
|
|
// Manually construct and store a batch, so it doesn't immediately commit.
|
|
mset, err := s.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
mset.mu.Lock()
|
|
batches := &batching{}
|
|
mset.batches = batches
|
|
mset.mu.Unlock()
|
|
batches.mu.Lock()
|
|
b, err := batches.newBatchGroup(mset, "uuid")
|
|
if err != nil {
|
|
batches.mu.Unlock()
|
|
require_NoError(t, err)
|
|
}
|
|
hdr1 := genHeader(nil, "Nats-Batch-Id", "uuid")
|
|
hdr1 = genHeader(hdr1, "Nats-Batch-Sequence", "1")
|
|
_, _, err = b.store.StoreMsg("foo", hdr1, nil, 0)
|
|
if err != nil {
|
|
batches.mu.Unlock()
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
hdr2 := genHeader(nil, "Nats-Batch-Id", "uuid")
|
|
hdr2 = genHeader(hdr2, "Nats-Batch-Sequence", "2")
|
|
hdr2 = genHeader(hdr2, "Nats-Batch-Commit", "1")
|
|
_, _, err = b.store.StoreMsg("foo", hdr2, nil, 0)
|
|
if err != nil {
|
|
batches.mu.Unlock()
|
|
require_NoError(t, err)
|
|
}
|
|
commitReady := b.readyForCommit()
|
|
batches.mu.Unlock()
|
|
require_True(t, commitReady)
|
|
|
|
// Simulate the first message of the batch is committed.
|
|
err = mset.processJetStreamMsg("foo", _EMPTY_, hdr1, nil, 0, 0, nil, false, true)
|
|
require_NoError(t, err)
|
|
|
|
// Simulate a hard kill, upon recovery the rest of the batch should be applied.
|
|
port := s.opts.Port
|
|
sd := s.StoreDir()
|
|
nc.Close()
|
|
s.Shutdown()
|
|
s = RunJetStreamServerOnPort(port, sd)
|
|
defer s.Shutdown()
|
|
|
|
mset, err = s.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
state := mset.state()
|
|
require_Equal(t, state.Msgs, 2)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 2)
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishEncode(t *testing.T) {
|
|
test := func(t *testing.T, commit bool, compress bool) {
|
|
ts := time.Now().UnixNano()
|
|
hdr := genHeader(nil, "Nats-Batch-Id", "uuid")
|
|
hdr = genHeader(hdr, "Nats-Batch-Sequence", "1")
|
|
msg := []byte("A")
|
|
if compress {
|
|
msg = bytes.Repeat(msg, compressThreshold)
|
|
}
|
|
esm := encodeStreamMsgAllowCompressAndBatch("foo", "reply", hdr, msg, 1, ts, false, "uuid", 1, commit)
|
|
|
|
buf, op := esm[1:], entryOp(esm[0])
|
|
if commit {
|
|
require_Equal(t, op, batchCommitMsgOp)
|
|
} else {
|
|
require_Equal(t, op, batchMsgOp)
|
|
}
|
|
|
|
batchId, batchSeq, op, mbuf, err := decodeBatchMsg(buf)
|
|
require_NoError(t, err)
|
|
require_Equal(t, batchId, "uuid")
|
|
require_Equal(t, batchSeq, 1)
|
|
if compress {
|
|
require_Equal(t, op, compressedStreamMsgOp)
|
|
mbuf, err = s2.Decode(nil, mbuf)
|
|
require_NoError(t, err)
|
|
} else {
|
|
require_Equal(t, op, streamMsgOp)
|
|
}
|
|
|
|
subject, reply, dhdr, dmsg, lseq, dts, sourced, err := decodeStreamMsg(mbuf)
|
|
require_NoError(t, err)
|
|
require_Equal(t, subject, "foo")
|
|
require_Equal(t, reply, "reply")
|
|
require_True(t, bytes.Equal(dhdr, hdr))
|
|
require_True(t, bytes.Equal(dmsg, msg))
|
|
require_Equal(t, lseq, 1)
|
|
require_Equal(t, dts, ts)
|
|
require_False(t, sourced)
|
|
}
|
|
|
|
t.Run("normal", func(t *testing.T) { test(t, false, false) })
|
|
t.Run("normal-compress", func(t *testing.T) { test(t, false, true) })
|
|
t.Run("commit", func(t *testing.T) { test(t, true, false) })
|
|
t.Run("commit-compress", func(t *testing.T) { test(t, true, true) })
|
|
}
|
|
|
|
// Test a batch within a single proposal, optionally combined with messages unrelated
|
|
// to the batch but within the same proposal.
|
|
func TestJetStreamAtomicBatchPublishProposeOne(t *testing.T) {
|
|
test := func(t *testing.T, combined bool) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
Replicas: 3,
|
|
AllowAtomicPublish: true,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
sl := c.streamLeader(globalAccountName, "TEST")
|
|
mset, err := sl.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
pubAck, err := js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, pubAck.Sequence, 1)
|
|
|
|
var entries []*Entry
|
|
|
|
mset.clMu.Lock()
|
|
if combined {
|
|
esm := encodeStreamMsg("foo", _EMPTY_, nil, nil, mset.clseq, 0, false)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
}
|
|
|
|
msg := []byte("hello")
|
|
hdr := genHeader(nil, "Nats-Batch-Id", "uuid")
|
|
hdr = setHeader("Nats-Batch-Sequence", "1", hdr)
|
|
esm := encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, msg, mset.clseq, 0, false, "uuid", 1, false)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
|
|
hdr = setHeader("Nats-Batch-Sequence", "2", hdr)
|
|
hdr = setHeader("Nats-Batch-Commit", "1", hdr)
|
|
esm = encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, msg, mset.clseq, 0, false, "uuid", 2, true)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
|
|
if combined {
|
|
esm = encodeStreamMsg("foo", _EMPTY_, nil, nil, mset.clseq, 0, false)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
}
|
|
mset.clMu.Unlock()
|
|
n := mset.raftNode().(*raft)
|
|
n.sendAppendEntry(entries)
|
|
|
|
pubAck, err = js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
if combined {
|
|
require_Equal(t, pubAck.Sequence, 6)
|
|
} else {
|
|
require_Equal(t, pubAck.Sequence, 4)
|
|
}
|
|
}
|
|
|
|
t.Run("single", func(t *testing.T) { test(t, false) })
|
|
t.Run("combined", func(t *testing.T) { test(t, true) })
|
|
}
|
|
|
|
// Test a batch spanning multiple proposals, optionally combined with messages unrelated
|
|
// to the batch but within the same first/last proposal.
|
|
func TestJetStreamAtomicBatchPublishProposeMultiple(t *testing.T) {
|
|
test := func(t *testing.T, partial bool, combined bool) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
Replicas: 3,
|
|
AllowAtomicPublish: true,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
sl := c.streamLeader(globalAccountName, "TEST")
|
|
mset, err := sl.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
pubAck, err := js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, pubAck.Sequence, 1)
|
|
|
|
var entries []*Entry
|
|
mset.clMu.Lock()
|
|
hdr := genHeader(nil, "Nats-Batch-Id", "uuid")
|
|
hdr = genHeader(hdr, "Nats-Batch-Sequence", "1")
|
|
msg := []byte("hello")
|
|
if combined {
|
|
esm := encodeStreamMsg("foo", _EMPTY_, nil, nil, mset.clseq, 0, false)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
}
|
|
esm := encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, msg, mset.clseq, 0, false, "uuid", 1, false)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
mset.clMu.Unlock()
|
|
n := mset.raftNode().(*raft)
|
|
n.sendAppendEntry(entries)
|
|
|
|
mset.clMu.Lock()
|
|
hdr = setHeader("Nats-Batch-Sequence", "2", hdr)
|
|
esm = encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, msg, mset.clseq, 0, false, "uuid", 2, false)
|
|
mset.clseq++
|
|
mset.clMu.Unlock()
|
|
n.sendAppendEntry([]*Entry{newEntry(EntryNormal, esm)})
|
|
|
|
entries = nil
|
|
mset.clMu.Lock()
|
|
if !partial {
|
|
hdr = setHeader("Nats-Batch-Sequence", "3", hdr)
|
|
hdr = genHeader(hdr, "Nats-Batch-Commit", "1")
|
|
esm = encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, msg, mset.clseq, 0, false, "uuid", 3, true)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
}
|
|
if combined {
|
|
esm = encodeStreamMsg("foo", _EMPTY_, nil, nil, mset.clseq, 0, false)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
}
|
|
mset.clMu.Unlock()
|
|
n.sendAppendEntry(entries)
|
|
|
|
pubAck, err = js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
expectedSeq := uint64(2)
|
|
if !partial {
|
|
expectedSeq += 3
|
|
}
|
|
if combined {
|
|
expectedSeq += 2
|
|
}
|
|
require_Equal(t, pubAck.Sequence, expectedSeq)
|
|
}
|
|
|
|
t.Run("partial", func(t *testing.T) { test(t, true, false) })
|
|
t.Run("partial-combined", func(t *testing.T) { test(t, true, true) })
|
|
t.Run("full", func(t *testing.T) { test(t, false, false) })
|
|
t.Run("full-combined", func(t *testing.T) { test(t, false, true) })
|
|
}
|
|
|
|
// Test a batch that was only partially proposed.
|
|
func TestJetStreamAtomicBatchPublishProposeOnePartialBatch(t *testing.T) {
|
|
maxEntries := 3
|
|
for i := range maxEntries + 1 {
|
|
t.Run(fmt.Sprintf("I-%d", i), func(t *testing.T) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
Replicas: 3,
|
|
AllowAtomicPublish: true,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
sl := c.streamLeader(globalAccountName, "TEST")
|
|
mset, err := sl.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
pubAck, err := js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, pubAck.Sequence, 1)
|
|
|
|
var entries []*Entry
|
|
mset.clMu.Lock()
|
|
msg := []byte("hello")
|
|
hdr := genHeader(nil, "Nats-Batch-Id", "uuid")
|
|
for j := range 3 {
|
|
// Skip if indices equal.
|
|
if i == j {
|
|
continue
|
|
}
|
|
bseq := uint64(j + 1)
|
|
hdr = setHeader("Nats-Batch-Sequence", strconv.FormatUint(bseq, 10), hdr)
|
|
commit := bseq == uint64(maxEntries)
|
|
if commit {
|
|
hdr = setHeader("Nats-Batch-Commit", "1", hdr)
|
|
}
|
|
esm := encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, msg, mset.clseq, 0, false, "uuid", bseq, commit)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
}
|
|
mset.clMu.Unlock()
|
|
n := mset.raftNode().(*raft)
|
|
n.sendAppendEntry(entries)
|
|
|
|
pubAck, err = js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
expectedSeq := uint64(2)
|
|
if i >= maxEntries {
|
|
expectedSeq += uint64(maxEntries)
|
|
}
|
|
require_Equal(t, pubAck.Sequence, expectedSeq)
|
|
})
|
|
}
|
|
}
|
|
|
|
// Test multiple sequential batches, the first batch is partially proposed.
|
|
func TestJetStreamAtomicBatchPublishProposeMultiplePartialBatches(t *testing.T) {
|
|
test := func(t *testing.T, batchSize int, retry bool) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
Replicas: 3,
|
|
AllowAtomicPublish: true,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
sl := c.streamLeader(globalAccountName, "TEST")
|
|
mset, err := sl.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
pubAck, err := js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, pubAck.Sequence, 1)
|
|
|
|
batchId1, batchId2 := "ID_1", "ID_2"
|
|
if retry {
|
|
batchId1, batchId2 = "uuid", "uuid"
|
|
}
|
|
|
|
var entries []*Entry
|
|
mset.clMu.Lock()
|
|
msg := []byte("hello")
|
|
hdr := genHeader(nil, "Nats-Batch-Id", batchId1)
|
|
hdr = setHeader("Nats-Batch-Sequence", "1", hdr)
|
|
esm := encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, msg, mset.clseq, 0, false, batchId1, 1, false)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
|
|
for j := range batchSize {
|
|
bseq := uint64(j + 1)
|
|
hdr = genHeader(nil, "Nats-Batch-Id", batchId2)
|
|
hdr = setHeader("Nats-Batch-Sequence", strconv.FormatUint(bseq, 10), hdr)
|
|
commit := bseq == uint64(batchSize)
|
|
if commit {
|
|
hdr = setHeader("Nats-Batch-Commit", "1", hdr)
|
|
}
|
|
esm = encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, msg, mset.clseq, 0, false, batchId2, bseq, commit)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
}
|
|
mset.clMu.Unlock()
|
|
n := mset.raftNode().(*raft)
|
|
n.sendAppendEntry(entries)
|
|
|
|
pubAck, err = js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, pubAck.Sequence, uint64(2+batchSize))
|
|
|
|
// Validate the stream only committed the full batch.
|
|
rsm, err := js.GetMsg("TEST", 1)
|
|
require_NoError(t, err)
|
|
require_True(t, rsm.Header.Get("Nats-Batch-Id") == _EMPTY_)
|
|
for j := range batchSize {
|
|
rsm, err = js.GetMsg("TEST", uint64(2+j))
|
|
require_NoError(t, err)
|
|
require_Equal(t, rsm.Header.Get("Nats-Batch-Id"), batchId2)
|
|
require_Equal(t, rsm.Header.Get("Nats-Batch-Sequence"), strconv.Itoa(j+1))
|
|
}
|
|
rsm, err = js.GetMsg("TEST", uint64(2+batchSize))
|
|
require_NoError(t, err)
|
|
require_True(t, rsm.Header.Get("Nats-Batch-Id") == _EMPTY_)
|
|
|
|
mset.clMu.Lock()
|
|
clfs := mset.clfs
|
|
mset.clMu.Unlock()
|
|
require_Equal(t, clfs, 1)
|
|
}
|
|
for _, retry := range []bool{false, true} {
|
|
for i := range 2 {
|
|
batchSize := i + 1
|
|
title := fmt.Sprintf("B-%d", batchSize)
|
|
if retry {
|
|
title += "/Retry"
|
|
} else {
|
|
title += "/NoRetry"
|
|
}
|
|
t.Run(title, func(t *testing.T) {
|
|
test(t, batchSize, retry)
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test a continuous flow of batches spanning multiple append entries can still move applied up.
|
|
// Also, test a server can become leader if the previous leader left it with a partial batch.
|
|
func TestJetStreamAtomicBatchPublishContinuousBatchesStillMoveAppliedUp(t *testing.T) {
|
|
test := func(t *testing.T, largePayload bool) {
|
|
var payload []byte
|
|
if largePayload {
|
|
// Create a large payload that can be easily compressed.
|
|
payload = make([]byte, 2*compressThreshold)
|
|
}
|
|
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
Replicas: 3,
|
|
AllowAtomicPublish: true,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
sl := c.streamLeader(globalAccountName, "TEST")
|
|
mset, err := sl.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
pubAck, err := js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, pubAck.Sequence, 1)
|
|
|
|
n := mset.raftNode().(*raft)
|
|
index, commit, applied := n.Progress()
|
|
|
|
// The first batch spans two append entries, but commits.
|
|
mset.clMu.Lock()
|
|
hdr := genHeader(nil, "Nats-Batch-Id", "ID_1")
|
|
hdr = setHeader("Nats-Batch-Sequence", "1", hdr)
|
|
esm := encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, payload, mset.clseq, 0, false, "ID_1", 1, false)
|
|
mset.clseq++
|
|
mset.clMu.Unlock()
|
|
n.sendAppendEntry([]*Entry{newEntry(EntryNormal, esm)})
|
|
|
|
var entries []*Entry
|
|
mset.clMu.Lock()
|
|
hdr = genHeader(nil, "Nats-Batch-Id", "ID_1")
|
|
hdr = setHeader("Nats-Batch-Sequence", "2", hdr)
|
|
esm = encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, payload, mset.clseq, 0, false, "ID_1", 2, false)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
|
|
hdr = genHeader(nil, "Nats-Batch-Id", "ID_1")
|
|
hdr = setHeader("Nats-Batch-Sequence", "3", hdr)
|
|
hdr = setHeader("Nats-Batch-Commit", "1", hdr)
|
|
esm = encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, payload, mset.clseq, 0, false, "ID_1", 3, true)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
|
|
// The second batch doesn't commit.
|
|
hdr = genHeader(nil, "Nats-Batch-Id", "ID_2")
|
|
hdr = setHeader("Nats-Batch-Sequence", "1", hdr)
|
|
esm = encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, payload, mset.clseq, 0, false, "ID_2", 1, false)
|
|
mset.clseq++
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clMu.Unlock()
|
|
n.sendAppendEntry(entries)
|
|
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
n.RLock()
|
|
nindex, ncommit, processed, napplied := n.pindex, n.commit, n.processed, n.applied
|
|
n.RUnlock()
|
|
if nindex == index {
|
|
return errors.New("index not updated")
|
|
} else if ncommit == commit {
|
|
return errors.New("commit not updated")
|
|
} else if napplied == applied {
|
|
return errors.New("applied not updated")
|
|
} else if napplied == ncommit {
|
|
return errors.New("applied should not be able to equal commit yet")
|
|
} else if processed != ncommit {
|
|
return errors.New("must have processed all commits")
|
|
}
|
|
return checkState(t, c, globalAccountName, "TEST")
|
|
})
|
|
|
|
// Followers are now stranded with a partial batch, one needs to become leader
|
|
// and have the batch be rejected since it was partially proposed.
|
|
sl.Shutdown()
|
|
c.waitOnStreamLeader(globalAccountName, "TEST")
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
return checkState(t, c, globalAccountName, "TEST")
|
|
})
|
|
|
|
// Confirm the last batch gets rejected, and we are still able to publish with quorum.
|
|
pubAck, err = js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, pubAck.Sequence, 5)
|
|
|
|
c.restartServer(sl)
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
return checkState(t, c, globalAccountName, "TEST")
|
|
})
|
|
|
|
// Publish again, now with all servers online.
|
|
pubAck, err = js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, pubAck.Sequence, 6)
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
return checkState(t, c, globalAccountName, "TEST")
|
|
})
|
|
}
|
|
|
|
t.Run("Normal", func(t *testing.T) { test(t, false) })
|
|
t.Run("Compressed", func(t *testing.T) { test(t, true) })
|
|
}
|
|
|
|
// Test a batch that's committed but only partially applied, and the server gets hard killed.
|
|
// Upon recovery the server should skip the messages that were already applied but store the
|
|
// messages that weren't applied yet.
|
|
func TestJetStreamAtomicBatchPublishPartiallyAppliedBatchOnRecovery(t *testing.T) {
|
|
test := func(t *testing.T, largePayload bool) {
|
|
var payload []byte
|
|
expectedOp := streamMsgOp
|
|
if largePayload {
|
|
// Create a large payload that can be easily compressed.
|
|
payload = make([]byte, 2*compressThreshold)
|
|
expectedOp = compressedStreamMsgOp
|
|
}
|
|
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
Replicas: 3,
|
|
AllowAtomicPublish: true,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
sl := c.streamLeader(globalAccountName, "TEST")
|
|
mset, err := sl.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
pubAck, err := js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, pubAck.Sequence, 1)
|
|
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
return checkState(t, c, globalAccountName, "TEST")
|
|
})
|
|
|
|
// Pause applies on all followers.
|
|
for _, s := range c.servers {
|
|
if s == sl {
|
|
continue
|
|
}
|
|
fmset, err := s.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
require_NoError(t, fmset.raftNode().PauseApply())
|
|
}
|
|
|
|
// Publish a batch.
|
|
var entries []*Entry
|
|
mset.clMu.Lock()
|
|
for seq := uint64(1); seq <= 4; seq++ {
|
|
batchCommit := seq == 4
|
|
hdr := genHeader(nil, "Nats-Batch-Id", "ID")
|
|
hdr = genHeader(hdr, "Nats-Batch-Sequence", strconv.FormatUint(seq, 10))
|
|
esm := encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, hdr, payload, mset.clseq, 0, false, "ID", seq, batchCommit)
|
|
if _, _, bop, _, err := decodeBatchMsg(esm[1:]); err != nil || bop != expectedOp {
|
|
mset.clMu.Unlock()
|
|
t.Fatalf("Unexpected batch op %v: %v", bop, err)
|
|
}
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
mset.clseq++
|
|
}
|
|
mset.clMu.Unlock()
|
|
|
|
// Wait for the leader to have applied the batch, so the next
|
|
// publish informs the followers that it can be committed.
|
|
n := mset.raftNode().(*raft)
|
|
_, _, applied := n.Progress()
|
|
n.sendAppendEntry(entries)
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
_, _, napplied := n.Progress()
|
|
if applied == napplied {
|
|
return errors.New("batch hasn't been applied yet")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Publish so the commit moves up on all servers.
|
|
pubAck, err = js.Publish("foo", nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, pubAck.Sequence, 6)
|
|
|
|
// Simulate this follower only partially applied the batch and then got hard killed.
|
|
rs := c.randomNonStreamLeader(globalAccountName, "TEST")
|
|
mset, err = rs.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
ts := time.Now().UnixNano()
|
|
hdr := genHeader(nil, "Nats-Batch-Id", "ID")
|
|
hdr = genHeader(hdr, "Nats-Batch-Sequence", "1")
|
|
require_NoError(t, mset.store.StoreRawMsg("foo", hdr, nil, 2, ts, 0))
|
|
|
|
hdr = genHeader(nil, "Nats-Batch-Id", "ID")
|
|
hdr = genHeader(hdr, "Nats-Batch-Sequence", "2")
|
|
require_NoError(t, mset.store.StoreRawMsg("foo", hdr, nil, 3, ts, 0))
|
|
|
|
// Unpause applies on the remaining follower.
|
|
for _, s := range c.servers {
|
|
if s == sl || s == rs {
|
|
continue
|
|
}
|
|
fmset, err := s.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
fmset.raftNode().ResumeApply()
|
|
}
|
|
|
|
// Restart the "hard killed" follower, should apply the remainder of the batch upon recovery.
|
|
rs.Shutdown()
|
|
c.restartServer(rs)
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
return checkState(t, c, globalAccountName, "TEST")
|
|
})
|
|
}
|
|
|
|
t.Run("Normal", func(t *testing.T) { test(t, false) })
|
|
t.Run("Compressed", func(t *testing.T) { test(t, true) })
|
|
}
|
|
|
|
func TestJetStreamRollupIsolatedRead(t *testing.T) {
|
|
const (
|
|
DirectGet = iota
|
|
DirectBatchGet
|
|
DirectMultiGet
|
|
Consumer
|
|
)
|
|
|
|
test := func(t *testing.T, mode int) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc, js := jsClientConnect(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
_, err := js.AddStream(&nats.StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo.*"},
|
|
AllowRollup: true,
|
|
Replicas: 3,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
rs := c.randomNonStreamLeader(globalAccountName, "TEST")
|
|
mset, err := rs.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
// Reconnect to the selected server.
|
|
nc.Close()
|
|
nc, js = jsClientConnect(t, rs)
|
|
defer nc.Close()
|
|
|
|
m := nats.NewMsg("foo.0")
|
|
pubAck, err := js.PublishMsg(m)
|
|
require_NoError(t, err)
|
|
require_Equal(t, pubAck.Sequence, 1)
|
|
|
|
// mset.processJetStreamMsg will first store the new message, then update dedupe state, and then do the rollup.
|
|
// We block the replica such that it can store the new message but can't do the rollup yet.
|
|
// A read should wait for the rollup to complete.
|
|
mset.ddMu.Lock()
|
|
m.Subject = "foo.1"
|
|
m.Header.Set(JSMsgRollup, JSMsgRollupAll)
|
|
m.Header.Set(JSMsgId, "msgId")
|
|
_, _ = js.PublishMsg(m)
|
|
err = checkForErr(2*time.Second, 200*time.Millisecond, func() error {
|
|
var state StreamState
|
|
mset.store.FastState(&state)
|
|
if state.LastSeq != 2 {
|
|
return fmt.Errorf("expected last seq 2, got: %d", state.LastSeq)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
mset.ddMu.Unlock()
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// We're now subscribing and going to do a request, while the write is still halfway.
|
|
sub, err := nc.SubscribeSync("reply")
|
|
if err != nil {
|
|
mset.ddMu.Unlock()
|
|
require_NoError(t, err)
|
|
}
|
|
defer sub.Drain()
|
|
if err = nc.Flush(); err != nil {
|
|
mset.ddMu.Unlock()
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Run two goroutines, one will unblock the first write after a short sleep,
|
|
// the second will do the read/consumer request.
|
|
var (
|
|
ready sync.WaitGroup
|
|
run sync.WaitGroup
|
|
cleanup sync.WaitGroup
|
|
)
|
|
ready.Add(2)
|
|
run.Add(1)
|
|
cleanup.Add(1)
|
|
go func() {
|
|
ready.Done()
|
|
defer cleanup.Done()
|
|
run.Wait()
|
|
time.Sleep(250 * time.Millisecond)
|
|
mset.ddMu.Unlock()
|
|
}()
|
|
go func() {
|
|
ready.Done()
|
|
run.Wait()
|
|
switch mode {
|
|
case DirectGet:
|
|
mset.getDirectRequest(&JSApiMsgGetRequest{LastFor: "foo.0"}, "reply")
|
|
case DirectBatchGet:
|
|
mset.getDirectRequest(&JSApiMsgGetRequest{NextFor: "foo.*", Batch: 2}, "reply")
|
|
case DirectMultiGet:
|
|
mset.getDirectRequest(&JSApiMsgGetRequest{MultiLastFor: []string{"foo.*"}, Batch: 2}, "reply")
|
|
case Consumer:
|
|
mset.addConsumer(&ConsumerConfig{DeliverSubject: "reply", Direct: true})
|
|
}
|
|
}()
|
|
|
|
// Wait for both to be ready, then run both of them.
|
|
ready.Wait()
|
|
run.Done()
|
|
|
|
msg, err := sub.NextMsg(time.Second)
|
|
// Make sure cleanup has happened before validating.
|
|
cleanup.Wait()
|
|
require_NoError(t, err)
|
|
if mode == DirectGet {
|
|
require_Equal(t, msg.Header.Get("Status"), "404")
|
|
} else if mode != Consumer {
|
|
require_Equal(t, msg.Header.Get(JSNumPending), "0")
|
|
require_Equal(t, msg.Header.Get(JSSequence), "2")
|
|
} else {
|
|
// $JS.ACK.<stream>.<consumer>.<delivered>.<sseq>.<cseq>.<tm>.<pending>
|
|
require_True(t, strings.HasPrefix(msg.Reply, jsAckPre))
|
|
tokens := strings.Split(msg.Reply, ".")
|
|
require_Len(t, len(tokens), 9)
|
|
require_Equal(t, tokens[8], "0") // pending
|
|
require_Equal(t, tokens[5], "2") // sseq
|
|
}
|
|
}
|
|
|
|
t.Run("DirectGet", func(t *testing.T) { test(t, DirectGet) })
|
|
t.Run("DirectBatchGet", func(t *testing.T) { test(t, DirectBatchGet) })
|
|
t.Run("DirectMultiGet", func(t *testing.T) { test(t, DirectMultiGet) })
|
|
t.Run("Consumer", func(t *testing.T) { test(t, Consumer) })
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishAdvisories(t *testing.T) {
|
|
templ := `
|
|
listen: 127.0.0.1:-1
|
|
server_name: %s
|
|
jetstream: {
|
|
max_mem_store: 2GB
|
|
max_file_store: 2GB
|
|
store_dir: '%s'
|
|
limits {
|
|
batch {
|
|
timeout: 2s
|
|
}
|
|
}
|
|
}
|
|
|
|
leaf {
|
|
listen: 127.0.0.1:-1
|
|
}
|
|
|
|
cluster {
|
|
name: %s
|
|
listen: 127.0.0.1:%d
|
|
routes = [%s]
|
|
}
|
|
|
|
# For access to system account.
|
|
accounts { $SYS { users = [ { user: "admin", pass: "s3cr3t!" } ] } }
|
|
`
|
|
test := func(t *testing.T, replicas int) {
|
|
c := createJetStreamClusterWithTemplate(t, templ, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc := clientConnectToServer(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
cfg := &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Replicas: replicas,
|
|
Storage: FileStorage,
|
|
AllowAtomicPublish: true,
|
|
}
|
|
|
|
_, err := jsStreamCreate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
|
|
sub, err := nc.SubscribeSync(fmt.Sprintf("%s.*", JSAdvisoryStreamBatchAbandonedPre))
|
|
require_NoError(t, err)
|
|
defer sub.Drain()
|
|
require_NoError(t, nc.Flush())
|
|
|
|
// Should receive an advisory if gaps are detected.
|
|
m := nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
require_NoError(t, nc.PublishMsg(m))
|
|
m.Header.Set("Nats-Batch-Sequence", "3")
|
|
require_NoError(t, nc.PublishMsg(m))
|
|
|
|
msg, err := sub.NextMsg(time.Second)
|
|
require_NoError(t, err)
|
|
var advisory JSStreamBatchAbandonedAdvisory
|
|
require_NoError(t, json.Unmarshal(msg.Data, &advisory))
|
|
require_Equal(t, advisory.BatchId, "uuid")
|
|
require_Equal(t, advisory.Reason, BatchIncomplete)
|
|
|
|
// Should receive an advisory if the batch is too large.
|
|
count := 1002
|
|
for seq := 1; seq <= count; seq++ {
|
|
m = nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", strconv.Itoa(seq))
|
|
if seq != count {
|
|
require_NoError(t, nc.PublishMsg(m))
|
|
continue
|
|
}
|
|
var pubAck JSPubAckResponse
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
rmsg, err := nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
require_NoError(t, json.Unmarshal(rmsg.Data, &pubAck))
|
|
require_NotNil(t, pubAck.Error)
|
|
require_Error(t, pubAck.Error, NewJSAtomicPublishTooLargeBatchError(streamDefaultMaxBatchSize))
|
|
}
|
|
|
|
msg, err = sub.NextMsg(time.Second)
|
|
require_NoError(t, err)
|
|
advisory = JSStreamBatchAbandonedAdvisory{}
|
|
require_NoError(t, json.Unmarshal(msg.Data, &advisory))
|
|
require_Equal(t, advisory.BatchId, "uuid")
|
|
require_Equal(t, advisory.Reason, BatchLarge)
|
|
|
|
// Should receive an advisory if the batch times out and is abandoned.
|
|
m = nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
require_NoError(t, nc.PublishMsg(m))
|
|
|
|
msg, err = sub.NextMsg(3 * time.Second)
|
|
require_NoError(t, err)
|
|
advisory = JSStreamBatchAbandonedAdvisory{}
|
|
require_NoError(t, json.Unmarshal(msg.Data, &advisory))
|
|
require_Equal(t, advisory.BatchId, "uuid")
|
|
require_Equal(t, advisory.Reason, BatchTimeout)
|
|
}
|
|
|
|
for _, replicas := range []int{1, 3} {
|
|
t.Run(fmt.Sprintf("R%d", replicas), func(t *testing.T) {
|
|
test(t, replicas)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishExpectedSeq(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
AllowAtomicPublish: true,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
mset, err := s.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
clseqAndClfs := func() (uint64, uint64) {
|
|
mset.clMu.Lock()
|
|
defer mset.clMu.Unlock()
|
|
return mset.clseq, mset.clfs
|
|
}
|
|
|
|
clseq, clfs := clseqAndClfs()
|
|
require_Equal(t, clseq, 0)
|
|
require_Equal(t, clfs, 0)
|
|
|
|
_, err = js.Publish("foo", []byte("hello"))
|
|
require_NoError(t, err)
|
|
|
|
clseq, clfs = clseqAndClfs()
|
|
require_Equal(t, clseq, 0)
|
|
require_Equal(t, clfs, 0)
|
|
|
|
// test expect last seq for standalone publish
|
|
_, err = js.Publish("foo", []byte("hello"), nats.ExpectLastSequence(1))
|
|
require_NoError(t, err)
|
|
|
|
clseq, clfs = clseqAndClfs()
|
|
require_Equal(t, clseq, 0)
|
|
require_Equal(t, clfs, 0)
|
|
|
|
// now do a single msg batch with expect last seq
|
|
m := nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Expected-Last-Sequence", "2")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
resp, err := nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
|
|
var pubAck JSPubAckResponse
|
|
require_NoError(t, json.Unmarshal(resp.Data, &pubAck))
|
|
if pubAck.Error != nil {
|
|
t.Fatalf("Commit error: %v", pubAck.Error)
|
|
}
|
|
require_Equal(t, pubAck.Sequence, 3)
|
|
require_Equal(t, pubAck.BatchSize, 1)
|
|
|
|
clseq, clfs = clseqAndClfs()
|
|
require_Equal(t, clseq, 0)
|
|
require_Equal(t, clfs, 0)
|
|
}
|
|
|
|
// Test two different batches within the same append entry.
|
|
func TestJetStreamAtomicBatchPublishPartialBatchInSharedAppendEntry(t *testing.T) {
|
|
test := func(t *testing.T, commit bool) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc := clientConnectToServer(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
Replicas: 3,
|
|
AllowAtomicPublish: true,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
sl := c.streamLeader(globalAccountName, "TEST")
|
|
mset, err := sl.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
js := sl.getJetStream()
|
|
esm1 := encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, nil, []byte("b1"), 0, 0, false, "b1", 1, false)
|
|
esm2 := encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, nil, []byte("b2"), 1, 0, false, "b2", 1, commit)
|
|
ce := newCommittedEntry(2, []*Entry{
|
|
newEntry(EntryNormal, esm1),
|
|
newEntry(EntryNormal, esm2),
|
|
})
|
|
_, err = js.applyStreamEntries(mset, ce, false)
|
|
require_NoError(t, err)
|
|
|
|
mset.mu.RLock()
|
|
batch := mset.batchApply
|
|
mset.mu.RUnlock()
|
|
batch.mu.Lock()
|
|
entryStart, maxApplied := batch.entryStart, batch.maxApplied
|
|
batch.mu.Unlock()
|
|
|
|
if !commit {
|
|
require_Equal(t, entryStart, 1)
|
|
require_Equal(t, maxApplied, 1)
|
|
return
|
|
}
|
|
require_Equal(t, entryStart, 0)
|
|
require_Equal(t, maxApplied, 0)
|
|
require_Equal(t, mset.lastSeq(), 1)
|
|
|
|
var smv StoreMsg
|
|
sm, err := mset.store.LoadMsg(1, &smv)
|
|
require_NoError(t, err)
|
|
require_Equal(t, string(sm.buf), "b2")
|
|
}
|
|
|
|
t.Run("NoCommit", func(t *testing.T) { test(t, false) })
|
|
t.Run("Commit", func(t *testing.T) { test(t, true) })
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishRejectPartialBatchOnLeaderChange(t *testing.T) {
|
|
c := createJetStreamClusterExplicit(t, "R3S", 3)
|
|
defer c.shutdown()
|
|
|
|
nc := clientConnectToServer(t, c.randomServer())
|
|
defer nc.Close()
|
|
|
|
_, err := jsStreamCreate(t, nc, &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
Replicas: 3,
|
|
AllowAtomicPublish: true,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
sl := c.streamLeader(globalAccountName, "TEST")
|
|
mset, err := sl.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
// Propose a partial batch that doesn't commit.
|
|
var entries []*Entry
|
|
for i := range uint64(10) {
|
|
esm := encodeStreamMsgAllowCompressAndBatch("foo", _EMPTY_, nil, nil, i, 0, false, "b", i+1, false)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
}
|
|
n := mset.raftNode()
|
|
require_NoError(t, n.ProposeMulti(entries))
|
|
|
|
// Wait for all servers to have received and populated the partial batch.
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
for _, s := range c.servers {
|
|
mset2, err := s.globalAccount().lookupStream("TEST")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
mset2.mu.RLock()
|
|
batch := mset2.batchApply
|
|
mset2.mu.RUnlock()
|
|
if batch == nil {
|
|
return fmt.Errorf("expected batch to be set")
|
|
}
|
|
batch.mu.Lock()
|
|
count := batch.count
|
|
batch.mu.Unlock()
|
|
if count != 10 {
|
|
return fmt.Errorf("expected batch count to be 10, got: %d", count)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Trigger a leader change and ensure the CLFS gets accounted for.
|
|
require_NoError(t, n.StepDown())
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
for _, s := range c.servers {
|
|
mset, err = s.globalAccount().lookupStream("TEST")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if clfs := mset.getCLFS(); clfs != 10 {
|
|
return fmt.Errorf("expected clfs to be 10, got: %d", clfs)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishPersistModeAsync(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
nc := clientConnectToServer(t, s)
|
|
defer nc.Close()
|
|
|
|
cfg := &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: FileStorage,
|
|
Replicas: 1,
|
|
AllowAtomicPublish: true,
|
|
PersistMode: AsyncPersistMode,
|
|
}
|
|
_, err := jsStreamCreate(t, nc, cfg)
|
|
require_Error(t, err, NewJSStreamInvalidConfigError(fmt.Errorf("async persist mode is not supported with atomic batch publish")))
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishExpectedLastSubjectSequence(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
|
|
cfg := &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo.*"},
|
|
Storage: FileStorage,
|
|
Replicas: 1,
|
|
AllowAtomicPublish: true,
|
|
}
|
|
_, err := jsStreamCreate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
|
|
_, err = js.Publish("foo.A", nil)
|
|
require_NoError(t, err)
|
|
_, err = js.Publish("foo.B", nil)
|
|
require_NoError(t, err)
|
|
|
|
m := nats.NewMsg("foo.A")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
m.Header.Set("Nats-Expected-Last-Sequence", "2")
|
|
m.Header.Set("Nats-Expected-Last-Subject-Sequence", "1")
|
|
m.Header.Set("Nats-Expected-Last-Subject-Sequence-Subject", "foo.A")
|
|
msg, err := nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
require_Len(t, len(msg.Data), 0) // Empty ack.
|
|
|
|
m = nats.NewMsg("foo.B")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "2")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
m.Header.Set("Nats-Expected-Last-Subject-Sequence", "2")
|
|
m.Header.Set("Nats-Expected-Last-Subject-Sequence-Subject", "foo.B")
|
|
msg, err = nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
var resp JSPubAckResponse
|
|
require_NoError(t, json.Unmarshal(msg.Data, &resp))
|
|
require_True(t, resp.Error == nil)
|
|
require_Equal(t, resp.PubAck.Sequence, 4)
|
|
require_Equal(t, resp.PubAck.BatchId, "uuid")
|
|
require_Equal(t, resp.PubAck.BatchSize, 2)
|
|
}
|
|
|
|
func TestJetStreamAtomicBatchPublishCommitUnsupported(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
nc := clientConnectToServer(t, s)
|
|
defer nc.Close()
|
|
|
|
cfg := &StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: MemoryStorage,
|
|
Replicas: 1,
|
|
AllowAtomicPublish: true,
|
|
}
|
|
_, err := jsStreamCreate(t, nc, cfg)
|
|
require_NoError(t, err)
|
|
|
|
mset, err := s.globalAccount().lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
|
|
var resp JSPubAckResponse
|
|
for _, unsupportedCommit := range []string{"", "unsupported", "0"} {
|
|
m := nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
_, err = nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
|
|
m.Header.Set("Nats-Batch-Sequence", "2")
|
|
m.Header.Set("Nats-Batch-Commit", unsupportedCommit)
|
|
msg, err := nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
resp = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(msg.Data, &resp))
|
|
require_True(t, resp.Error != nil)
|
|
require_Error(t, resp.Error, NewJSAtomicPublishInvalidBatchCommitError())
|
|
|
|
// Confirm no batches are left.
|
|
mset.mu.RLock()
|
|
batches := mset.batches
|
|
mset.mu.RUnlock()
|
|
batches.mu.Lock()
|
|
groups := len(batches.group)
|
|
batches.mu.Unlock()
|
|
require_Len(t, groups, 0)
|
|
}
|
|
|
|
// The required API level should allow the batch to be rejected.
|
|
m := nats.NewMsg("foo")
|
|
m.Header.Set("Nats-Batch-Id", "uuid")
|
|
m.Header.Set("Nats-Batch-Sequence", "1")
|
|
m.Header.Set("Nats-Batch-Commit", "1")
|
|
m.Header.Set("Nats-Required-Api-Level", strconv.Itoa(math.MaxInt))
|
|
msg, err := nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
resp = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(msg.Data, &resp))
|
|
require_True(t, resp.Error != nil)
|
|
require_Error(t, resp.Error, NewJSRequiredApiLevelError())
|
|
|
|
// If required API level check passes, the header should be stripped.
|
|
m.Header.Set("Nats-Required-Api-Level", "0")
|
|
msg, err = nc.RequestMsg(m, time.Second)
|
|
require_NoError(t, err)
|
|
resp = JSPubAckResponse{}
|
|
require_NoError(t, json.Unmarshal(msg.Data, &resp))
|
|
require_True(t, resp.Error == nil)
|
|
require_Equal(t, resp.PubAck.Sequence, 1)
|
|
|
|
sm, err := mset.getMsg(1)
|
|
require_NoError(t, err)
|
|
require_Len(t, len(sliceHeader(JSRequiredApiLevel, sm.Header)), 0)
|
|
}
|