mirror of
https://github.com/hanzoai/pubsub.git
synced 2026-10-30 15:06:26 +00:00
`go vet ./...` failed, so the gate never reached the tests. Six defects, none
of them subtle once vet names them:
mqtt_test.go — three tables declare wantErr and their bodies read it, but 18
rows across them stopped one value short, so the literals did not compile.
Every one of those cases asserts a specific message, so the value is false.
stree_test.go — IntersectGSL takes `func([]byte, *T) bool`. One callback in
LiteralMatchWithOverlappingPWCs returned nothing, unlike the three
identical callbacks beside it.
log_test.go, monitor_test.go — %q formats a string; both arguments are ints.
filestore_test.go, jetstream_consumer_test.go, raft_test.go — unsafe,
path/filepath and net/url imported and unused.
go vet ./... and go build ./... are both clean now.
13593 lines
388 KiB
Go
13593 lines
388 KiB
Go
// Copyright 2019-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_store_tests
|
|
|
|
package server
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"crypto/hmac"
|
|
crand "crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/binary"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/fs"
|
|
"math"
|
|
"math/bits"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
"unicode/utf8"
|
|
|
|
"github.com/klauspost/compress/s2"
|
|
"github.com/hanzoai/pubsub/server/ats"
|
|
"github.com/hanzoai/pubsub/server/avl"
|
|
"github.com/hanzoai/pubsub/server/gsl"
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nuid"
|
|
)
|
|
|
|
func testFileStoreAllPermutations(t *testing.T, fn func(t *testing.T, fcfg FileStoreConfig)) {
|
|
for _, fcfg := range []FileStoreConfig{
|
|
{Cipher: NoCipher, Compression: NoCompression},
|
|
{Cipher: NoCipher, Compression: S2Compression},
|
|
{Cipher: AES, Compression: NoCompression},
|
|
{Cipher: AES, Compression: S2Compression},
|
|
{Cipher: ChaCha, Compression: NoCompression},
|
|
{Cipher: ChaCha, Compression: S2Compression},
|
|
} {
|
|
subtestName := fmt.Sprintf("%s-%s", fcfg.Cipher, fcfg.Compression)
|
|
t.Run(subtestName, func(t *testing.T) {
|
|
fcfg.StoreDir = t.TempDir()
|
|
fn(t, fcfg)
|
|
time.Sleep(100 * time.Millisecond)
|
|
})
|
|
}
|
|
}
|
|
|
|
func prf(fcfg *FileStoreConfig) func(context []byte) ([]byte, error) {
|
|
if fcfg.Cipher == NoCipher {
|
|
return nil
|
|
}
|
|
return func(context []byte) ([]byte, error) {
|
|
h := hmac.New(sha256.New, []byte("dlc22"))
|
|
if _, err := h.Write(context); err != nil {
|
|
return nil, err
|
|
}
|
|
return h.Sum(nil), nil
|
|
}
|
|
}
|
|
|
|
func TestFileStoreBasics(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
for i := 1; i <= 5; i++ {
|
|
now := time.Now().UnixNano()
|
|
if seq, ts, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
t.Fatalf("Error storing msg: %v", err)
|
|
} else if seq != uint64(i) {
|
|
t.Fatalf("Expected sequence to be %d, got %d", i, seq)
|
|
} else if ts < now || ts > now+int64(time.Millisecond) {
|
|
t.Fatalf("Expected timestamp to be current, got %v", ts-now)
|
|
}
|
|
}
|
|
|
|
state := fs.State()
|
|
if state.Msgs != 5 {
|
|
t.Fatalf("Expected 5 msgs, got %d", state.Msgs)
|
|
}
|
|
expectedSize := 5 * fileStoreMsgSize(subj, nil, msg)
|
|
if state.Bytes != expectedSize {
|
|
t.Fatalf("Expected %d bytes, got %d", expectedSize, state.Bytes)
|
|
}
|
|
|
|
var smv StoreMsg
|
|
sm, err := fs.LoadMsg(2, &smv)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error looking up msg: %v", err)
|
|
}
|
|
if sm.subj != subj {
|
|
t.Fatalf("Subjects don't match, original %q vs %q", subj, sm.subj)
|
|
}
|
|
if !bytes.Equal(sm.msg, msg) {
|
|
t.Fatalf("Msgs don't match, original %q vs %q", msg, sm.msg)
|
|
}
|
|
_, err = fs.LoadMsg(3, nil)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error looking up msg: %v", err)
|
|
}
|
|
|
|
remove := func(seq, expectedMsgs uint64) {
|
|
t.Helper()
|
|
removed, err := fs.RemoveMsg(seq)
|
|
if err != nil {
|
|
t.Fatalf("Got an error on remove of %d: %v", seq, err)
|
|
}
|
|
if !removed {
|
|
t.Fatalf("Expected remove to return true for %d", seq)
|
|
}
|
|
if state := fs.State(); state.Msgs != expectedMsgs {
|
|
t.Fatalf("Expected %d msgs, got %d", expectedMsgs, state.Msgs)
|
|
}
|
|
}
|
|
|
|
// Remove first
|
|
remove(1, 4)
|
|
// Remove last
|
|
remove(5, 3)
|
|
// Remove a middle
|
|
remove(3, 2)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMsgHeaders(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
subj, hdr, msg := "foo", []byte("name:derek"), []byte("Hello World")
|
|
elen := 22 + len(subj) + 4 + len(hdr) + len(msg) + 8
|
|
if sz := int(fileStoreMsgSize(subj, hdr, msg)); sz != elen {
|
|
t.Fatalf("Wrong size for stored msg with header")
|
|
}
|
|
fs.StoreMsg(subj, hdr, msg, 0)
|
|
var smv StoreMsg
|
|
sm, err := fs.LoadMsg(1, &smv)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error looking up msg: %v", err)
|
|
}
|
|
if !bytes.Equal(msg, sm.msg) {
|
|
t.Fatalf("Expected same msg, got %q vs %q", sm.msg, msg)
|
|
}
|
|
if !bytes.Equal(hdr, sm.hdr) {
|
|
t.Fatalf("Expected same hdr, got %q vs %q", sm.hdr, hdr)
|
|
}
|
|
if removed, _ := fs.EraseMsg(1); !removed {
|
|
t.Fatalf("Expected erase msg to return success")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreBasicWriteMsgsAndRestore(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
if _, err := newFileStore(fcfg, StreamConfig{Storage: MemoryStorage}); err == nil {
|
|
t.Fatalf("Expected an error with wrong type")
|
|
}
|
|
if _, err := newFileStore(fcfg, StreamConfig{Storage: FileStorage}); err == nil {
|
|
t.Fatalf("Expected an error with no name")
|
|
}
|
|
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj := "foo"
|
|
|
|
// Write 100 msgs
|
|
toStore := uint64(100)
|
|
for i := uint64(1); i <= toStore; i++ {
|
|
msg := []byte(fmt.Sprintf("[%08d] Hello World!", i))
|
|
if seq, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
t.Fatalf("Error storing msg: %v", err)
|
|
} else if seq != uint64(i) {
|
|
t.Fatalf("Expected sequence to be %d, got %d", i, seq)
|
|
}
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != toStore {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
msg22 := []byte(fmt.Sprintf("[%08d] Hello World!", 22))
|
|
expectedSize := toStore * fileStoreMsgSize(subj, nil, msg22)
|
|
|
|
if state.Bytes != expectedSize {
|
|
t.Fatalf("Expected %d bytes, got %d", expectedSize, state.Bytes)
|
|
}
|
|
// Stop will flush to disk.
|
|
fs.Stop()
|
|
|
|
// Make sure Store call after does not work.
|
|
if _, _, err := fs.StoreMsg(subj, nil, []byte("no work"), 0); err == nil {
|
|
t.Fatalf("Expected an error for StoreMsg call after Stop, got none")
|
|
}
|
|
|
|
// Restart
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state = fs.State()
|
|
if state.Msgs != toStore {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
if state.Bytes != expectedSize {
|
|
t.Fatalf("Expected %d bytes, got %d", expectedSize, state.Bytes)
|
|
}
|
|
|
|
// Now write 100 more msgs
|
|
for i := uint64(101); i <= toStore*2; i++ {
|
|
msg := []byte(fmt.Sprintf("[%08d] Hello World!", i))
|
|
if seq, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
t.Fatalf("Error storing msg: %v", err)
|
|
} else if seq != uint64(i) {
|
|
t.Fatalf("Expected sequence to be %d, got %d", i, seq)
|
|
}
|
|
}
|
|
state = fs.State()
|
|
if state.Msgs != toStore*2 {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore*2, state.Msgs)
|
|
}
|
|
|
|
// Now cycle again and make sure that last batch was stored.
|
|
// Stop will flush to disk.
|
|
fs.Stop()
|
|
|
|
// Restart
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state = fs.State()
|
|
if state.Msgs != toStore*2 {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore*2, state.Msgs)
|
|
}
|
|
if state.Bytes != expectedSize*2 {
|
|
t.Fatalf("Expected %d bytes, got %d", expectedSize*2, state.Bytes)
|
|
}
|
|
|
|
fs.Purge()
|
|
fs.Stop()
|
|
|
|
// Restart
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state = fs.State()
|
|
if state.Msgs != 0 {
|
|
t.Fatalf("Expected %d msgs, got %d", 0, state.Msgs)
|
|
}
|
|
if state.Bytes != 0 {
|
|
t.Fatalf("Expected %d bytes, got %d", 0, state.Bytes)
|
|
}
|
|
|
|
seq, _, err := fs.StoreMsg(subj, nil, []byte("Hello"), 0)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
fs.RemoveMsg(seq)
|
|
|
|
fs.Stop()
|
|
|
|
// Restart
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state = fs.State()
|
|
require_Equal(t, state.FirstSeq, seq+1)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreSelectNextFirst(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 256
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
numMsgs := 10
|
|
subj, msg := "zzz", []byte("Hello World")
|
|
for i := 0; i < numMsgs; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
if state := fs.State(); state.Msgs != uint64(numMsgs) {
|
|
t.Fatalf("Expected %d msgs, got %d", numMsgs, state.Msgs)
|
|
}
|
|
|
|
// Note the 256 block size is tied to the msg size below to give us 5 messages per block.
|
|
if fmb := fs.selectMsgBlock(1); fmb.msgs != 5 {
|
|
t.Fatalf("Expected 5 messages per block, but got %d", fmb.msgs)
|
|
}
|
|
|
|
// Delete 2-7, this will cross message blocks.
|
|
for i := 2; i <= 7; i++ {
|
|
fs.RemoveMsg(uint64(i))
|
|
}
|
|
|
|
if state := fs.State(); state.Msgs != 4 || state.FirstSeq != 1 {
|
|
t.Fatalf("Expected 4 msgs, first seq of 11, got msgs of %d and first seq of %d", state.Msgs, state.FirstSeq)
|
|
}
|
|
// Now close the gap which will force the system to jump underlying message blocks to find the right sequence.
|
|
fs.RemoveMsg(1)
|
|
if state := fs.State(); state.Msgs != 3 || state.FirstSeq != 8 {
|
|
t.Fatalf("Expected 3 msgs, first seq of 8, got msgs of %d and first seq of %d", state.Msgs, state.FirstSeq)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreSkipMsg(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 256
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
numSkips := 10
|
|
for i := 0; i < numSkips; i++ {
|
|
fs.SkipMsg(0)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != 0 {
|
|
t.Fatalf("Expected %d msgs, got %d", 0, state.Msgs)
|
|
}
|
|
if state.FirstSeq != uint64(numSkips+1) || state.LastSeq != uint64(numSkips) {
|
|
t.Fatalf("Expected first to be %d and last to be %d. got first %d and last %d", numSkips+1, numSkips, state.FirstSeq, state.LastSeq)
|
|
}
|
|
|
|
fs.StoreMsg("zzz", nil, []byte("Hello World!"), 0)
|
|
fs.SkipMsg(0)
|
|
fs.SkipMsg(0)
|
|
fs.StoreMsg("zzz", nil, []byte("Hello World!"), 0)
|
|
fs.SkipMsg(0)
|
|
|
|
state = fs.State()
|
|
if state.Msgs != 2 {
|
|
t.Fatalf("Expected %d msgs, got %d", 2, state.Msgs)
|
|
}
|
|
if state.FirstSeq != uint64(numSkips+1) || state.LastSeq != uint64(numSkips+5) {
|
|
t.Fatalf("Expected first to be %d and last to be %d. got first %d and last %d", numSkips+1, numSkips+5, state.FirstSeq, state.LastSeq)
|
|
}
|
|
|
|
// Make sure we recover same state.
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state = fs.State()
|
|
if state.Msgs != 2 {
|
|
t.Fatalf("Expected %d msgs, got %d", 2, state.Msgs)
|
|
}
|
|
if state.FirstSeq != uint64(numSkips+1) || state.LastSeq != uint64(numSkips+5) {
|
|
t.Fatalf("Expected first to be %d and last to be %d. got first %d and last %d", numSkips+1, numSkips+5, state.FirstSeq, state.LastSeq)
|
|
}
|
|
|
|
var smv StoreMsg
|
|
sm, err := fs.LoadMsg(11, &smv)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error looking up seq 11: %v", err)
|
|
}
|
|
if sm.subj != "zzz" || string(sm.msg) != "Hello World!" {
|
|
t.Fatalf("Message did not match")
|
|
}
|
|
|
|
fs.SkipMsg(0)
|
|
nseq, _, err := fs.StoreMsg("AAA", nil, []byte("Skip?"), 0)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error looking up seq 11: %v", err)
|
|
}
|
|
if nseq != 17 {
|
|
t.Fatalf("Expected seq of %d but got %d", 17, nseq)
|
|
}
|
|
|
|
// Make sure we recover same state.
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
sm, err = fs.LoadMsg(nseq, &smv)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error looking up seq %d: %v", nseq, err)
|
|
}
|
|
if sm.subj != "AAA" || string(sm.msg) != "Skip?" {
|
|
t.Fatalf("Message did not match")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreWriteExpireWrite(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cexp := 10 * time.Millisecond
|
|
fcfg.CacheExpire = cexp
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
toSend := 10
|
|
for i := 0; i < toSend; i++ {
|
|
fs.StoreMsg("zzz", nil, []byte("Hello World!"), 0)
|
|
}
|
|
|
|
// Wait for write cache portion to go to zero.
|
|
checkFor(t, time.Second, 20*time.Millisecond, func() error {
|
|
if csz := fs.cacheSize(); csz != 0 {
|
|
return fmt.Errorf("cache size not 0, got %s", friendlyBytes(int64(csz)))
|
|
}
|
|
return nil
|
|
})
|
|
|
|
for i := 0; i < toSend; i++ {
|
|
fs.StoreMsg("zzz", nil, []byte("Hello World! - 22"), 0)
|
|
}
|
|
|
|
if state := fs.State(); state.Msgs != uint64(toSend*2) {
|
|
t.Fatalf("Expected %d msgs, got %d", toSend*2, state.Msgs)
|
|
}
|
|
|
|
// Make sure we recover same state.
|
|
fs.Stop()
|
|
|
|
fcfg.CacheExpire = 0
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); state.Msgs != uint64(toSend*2) {
|
|
t.Fatalf("Expected %d msgs, got %d", toSend*2, state.Msgs)
|
|
}
|
|
|
|
// Now load them in and check.
|
|
var smv StoreMsg
|
|
for i := 1; i <= toSend*2; i++ {
|
|
sm, err := fs.LoadMsg(uint64(i), &smv)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error looking up seq %d: %v", i, err)
|
|
}
|
|
str := "Hello World!"
|
|
if i > toSend {
|
|
str = "Hello World! - 22"
|
|
}
|
|
if sm.subj != "zzz" || string(sm.msg) != str {
|
|
t.Fatalf("Message did not match")
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMsgLimit(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage, MaxMsgs: 10}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
for i := 0; i < 10; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != 10 {
|
|
t.Fatalf("Expected %d msgs, got %d", 10, state.Msgs)
|
|
}
|
|
if _, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
t.Fatalf("Error storing msg: %v", err)
|
|
}
|
|
state = fs.State()
|
|
if state.Msgs != 10 {
|
|
t.Fatalf("Expected %d msgs, got %d", 10, state.Msgs)
|
|
}
|
|
if state.LastSeq != 11 {
|
|
t.Fatalf("Expected the last sequence to be 11 now, but got %d", state.LastSeq)
|
|
}
|
|
if state.FirstSeq != 2 {
|
|
t.Fatalf("Expected the first sequence to be 2 now, but got %d", state.FirstSeq)
|
|
}
|
|
// Make sure we can not lookup seq 1.
|
|
if _, err := fs.LoadMsg(1, nil); err == nil {
|
|
t.Fatalf("Expected error looking up seq 1 but got none")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMsgLimitBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage, MaxMsgs: 1}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage, MaxMsgs: 1}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreBytesLimit(t *testing.T) {
|
|
subj, msg := "foo", make([]byte, 512)
|
|
storedMsgSize := fileStoreMsgSize(subj, nil, msg)
|
|
|
|
toStore := uint64(1024)
|
|
maxBytes := storedMsgSize * toStore
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage, MaxBytes: int64(maxBytes)}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := uint64(0); i < toStore; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != toStore {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
if state.Bytes != storedMsgSize*toStore {
|
|
t.Fatalf("Expected bytes to be %d, got %d", storedMsgSize*toStore, state.Bytes)
|
|
}
|
|
|
|
// Now send 10 more and check that bytes limit enforced.
|
|
for i := 0; i < 10; i++ {
|
|
if _, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
t.Fatalf("Error storing msg: %v", err)
|
|
}
|
|
}
|
|
state = fs.State()
|
|
if state.Msgs != toStore {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
if state.Bytes != storedMsgSize*toStore {
|
|
t.Fatalf("Expected bytes to be %d, got %d", storedMsgSize*toStore, state.Bytes)
|
|
}
|
|
if state.FirstSeq != 11 {
|
|
t.Fatalf("Expected first sequence to be 11, got %d", state.FirstSeq)
|
|
}
|
|
if state.LastSeq != toStore+10 {
|
|
t.Fatalf("Expected last sequence to be %d, got %d", toStore+10, state.LastSeq)
|
|
}
|
|
})
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/4771
|
|
func TestFileStoreBytesLimitWithDiscardNew(t *testing.T) {
|
|
subj, msg := "tiny", make([]byte, 7)
|
|
storedMsgSize := fileStoreMsgSize(subj, nil, msg)
|
|
|
|
toStore := uint64(2)
|
|
maxBytes := 100
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Storage: FileStorage, MaxBytes: int64(maxBytes), Discard: DiscardNew}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
_, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
if i < int(toStore) {
|
|
if err != nil {
|
|
t.Fatalf("Error storing msg: %v", err)
|
|
}
|
|
} else if !errors.Is(err, ErrMaxBytes) {
|
|
t.Fatalf("Storing msg should result in: %v", ErrMaxBytes)
|
|
}
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != toStore {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
if state.Bytes != storedMsgSize*toStore {
|
|
t.Fatalf("Expected bytes to be %d, got %d", storedMsgSize*toStore, state.Bytes)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreAgeLimit(t *testing.T) {
|
|
maxAge := 1 * time.Second
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
if fcfg.Compression != NoCompression || fcfg.Cipher != NoCipher {
|
|
// TODO(nat): This test flakes at the moment with compression or encryption
|
|
// because it takes too long to compress/encrypt the blocks in CI, by which
|
|
// time the messages have expired. Need to think about a balanced age so that
|
|
// the test doesn't take too long in these cases.
|
|
t.SkipNow()
|
|
}
|
|
|
|
fcfg.BlockSize = 256
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage, MaxAge: maxAge}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Store some messages. Does not really matter how many.
|
|
subj, msg := "foo", []byte("Hello World")
|
|
toStore := 500
|
|
for i := 0; i < toStore; i++ {
|
|
if _, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != uint64(toStore) {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
checkExpired := func(t *testing.T) {
|
|
t.Helper()
|
|
checkFor(t, 5*time.Second, maxAge, func() error {
|
|
state = fs.State()
|
|
if state.Msgs != 0 {
|
|
return fmt.Errorf("Expected no msgs, got %d", state.Msgs)
|
|
}
|
|
if state.Bytes != 0 {
|
|
return fmt.Errorf("Expected no bytes, got %d", state.Bytes)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
// Let them expire
|
|
checkExpired(t)
|
|
|
|
// Now add some more and make sure that timer will fire again.
|
|
for i := 0; i < toStore; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
state = fs.State()
|
|
if state.Msgs != uint64(toStore) {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
fs.RemoveMsg(502)
|
|
fs.RemoveMsg(602)
|
|
fs.RemoveMsg(702)
|
|
fs.RemoveMsg(802)
|
|
// We will measure the time to make sure expires works with interior deletes.
|
|
start := time.Now()
|
|
checkExpired(t)
|
|
if elapsed := time.Since(start); elapsed > 5*time.Second {
|
|
t.Fatalf("Took too long to expire: %v", elapsed)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreTimeStamps(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
last := time.Now().UnixNano()
|
|
subj, msg := "foo", []byte("Hello World")
|
|
for i := 0; i < 10; i++ {
|
|
time.Sleep(5 * time.Millisecond)
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
var smv StoreMsg
|
|
for seq := uint64(1); seq <= 10; seq++ {
|
|
sm, err := fs.LoadMsg(seq, &smv)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error looking up msg [%d]: %v", seq, err)
|
|
}
|
|
// These should be different
|
|
if sm.ts <= last {
|
|
t.Fatalf("Expected different timestamps, got last %v vs %v", last, sm.ts)
|
|
}
|
|
last = sm.ts
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStorePurge(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
blkSize := uint64(64 * 1024)
|
|
fcfg.BlockSize = blkSize
|
|
created := time.Now()
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", make([]byte, 8*1024)
|
|
storedMsgSize := fileStoreMsgSize(subj, nil, msg)
|
|
|
|
toStore := uint64(1024)
|
|
for i := uint64(0); i < toStore; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != toStore {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
if state.Bytes != storedMsgSize*toStore {
|
|
t.Fatalf("Expected bytes to be %d, got %d", storedMsgSize*toStore, state.Bytes)
|
|
}
|
|
|
|
expectedBlocks := int(storedMsgSize * toStore / blkSize)
|
|
if numBlocks := fs.numMsgBlocks(); numBlocks <= expectedBlocks {
|
|
t.Fatalf("Expected to have more then %d msg blocks, got %d", blkSize, numBlocks)
|
|
}
|
|
|
|
fs.Purge()
|
|
|
|
if numBlocks := fs.numMsgBlocks(); numBlocks != 1 {
|
|
t.Fatalf("Expected to have exactly 1 empty msg block, got %d", numBlocks)
|
|
}
|
|
|
|
checkPurgeState := func(stored uint64) {
|
|
t.Helper()
|
|
state = fs.State()
|
|
if state.Msgs != 0 {
|
|
t.Fatalf("Expected 0 msgs after purge, got %d", state.Msgs)
|
|
}
|
|
if state.Bytes != 0 {
|
|
t.Fatalf("Expected 0 bytes after purge, got %d", state.Bytes)
|
|
}
|
|
if state.LastSeq != stored {
|
|
t.Fatalf("Expected LastSeq to be %d., got %d", toStore, state.LastSeq)
|
|
}
|
|
if state.FirstSeq != stored+1 {
|
|
t.Fatalf("Expected FirstSeq to be %d., got %d", toStore+1, state.FirstSeq)
|
|
}
|
|
}
|
|
checkPurgeState(toStore)
|
|
|
|
// Make sure we recover same state.
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if numBlocks := fs.numMsgBlocks(); numBlocks != 1 {
|
|
t.Fatalf("Expected to have exactly 1 empty msg block, got %d", numBlocks)
|
|
}
|
|
|
|
checkPurgeState(toStore)
|
|
|
|
// Now make sure we clean up any dangling purged messages.
|
|
for i := uint64(0); i < toStore; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
state = fs.State()
|
|
if state.Msgs != toStore {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
if state.Bytes != storedMsgSize*toStore {
|
|
t.Fatalf("Expected bytes to be %d, got %d", storedMsgSize*toStore, state.Bytes)
|
|
}
|
|
|
|
// We will simulate crashing before the purge directory is cleared.
|
|
mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
|
|
pdir := filepath.Join(fs.fcfg.StoreDir, "ptest")
|
|
os.Rename(mdir, pdir)
|
|
os.MkdirAll(mdir, 0755)
|
|
|
|
fs.Purge()
|
|
checkPurgeState(toStore * 2)
|
|
|
|
// Make sure we recover same state.
|
|
fs.Stop()
|
|
|
|
purgeDir := filepath.Join(fs.fcfg.StoreDir, purgeDir)
|
|
os.Rename(pdir, purgeDir)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if numBlocks := fs.numMsgBlocks(); numBlocks != 1 {
|
|
t.Fatalf("Expected to have exactly 1 empty msg block, got %d", numBlocks)
|
|
}
|
|
|
|
checkPurgeState(toStore * 2)
|
|
|
|
checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
|
|
if _, err := os.Stat(purgeDir); err == nil {
|
|
return fmt.Errorf("purge directory still present")
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestFileStoreEncryptedPurgeRecoveryAfterKeyRename(t *testing.T) {
|
|
fcfg := FileStoreConfig{
|
|
StoreDir: t.TempDir(),
|
|
Cipher: AES,
|
|
Compression: NoCompression,
|
|
BlockSize: 64 * 1024,
|
|
}
|
|
created := time.Now()
|
|
cfg := StreamConfig{Name: "zzz", Storage: FileStorage}
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
|
|
subj, msg := "foo", make([]byte, 8*1024)
|
|
toStore := uint64(1024)
|
|
for i := uint64(0); i < toStore; i++ {
|
|
_, _, err = fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
baseline := fs.State()
|
|
|
|
storeDir := fcfg.StoreDir
|
|
mdir := filepath.Join(storeDir, msgDir)
|
|
preMsgs := filepath.Join(storeDir, "msgs.pre")
|
|
// Snapshot the pre-purge msg directory so we can recreate the crash window.
|
|
require_NoError(t, copyDir(t, preMsgs, mdir))
|
|
|
|
// Run a real purge once to create a valid encrypted tombstone block + key.
|
|
_, err = fs.Purge()
|
|
require_NoError(t, err)
|
|
|
|
fs.mu.RLock()
|
|
tombIdx := fs.lmb.index
|
|
fs.mu.RUnlock()
|
|
|
|
require_NoError(t, fs.stop(false, false))
|
|
|
|
postMsgs := filepath.Join(storeDir, "msgs.post")
|
|
require_NoError(t, os.Rename(mdir, postMsgs))
|
|
require_NoError(t, copyDir(t, mdir, preMsgs))
|
|
// Force recovery from the block files instead of a fresh full-state snapshot.
|
|
require_NoError(t, os.RemoveAll(filepath.Join(mdir, streamStreamStateFile)))
|
|
|
|
tombBlk := fmt.Sprintf(blkScan, tombIdx)
|
|
tombKey := fmt.Sprintf(keyScan, tombIdx)
|
|
require_NoError(t, os.Rename(filepath.Join(postMsgs, tombBlk), filepath.Join(mdir, tombBlk)))
|
|
|
|
ndir := filepath.Join(storeDir, newMsgDir)
|
|
require_NoError(t, os.MkdirAll(ndir, defaultDirPerms))
|
|
// Simulate a crash after moving N.key into __new_msgs__ but before moving N.blk.
|
|
require_NoError(t, os.Rename(filepath.Join(postMsgs, tombKey), filepath.Join(ndir, tombKey)))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, baseline.Msgs)
|
|
require_Equal(t, state.Bytes, baseline.Bytes)
|
|
require_Equal(t, state.FirstSeq, baseline.FirstSeq)
|
|
require_Equal(t, state.LastSeq, baseline.LastSeq)
|
|
|
|
if _, err := os.Stat(filepath.Join(mdir, tombBlk)); !os.IsNotExist(err) {
|
|
t.Fatalf("Expected rollback to remove %q, got err=%v", tombBlk, err)
|
|
}
|
|
if _, err := os.Stat(ndir); !os.IsNotExist(err) {
|
|
t.Fatalf("Expected rollback to remove %q, got err=%v", ndir, err)
|
|
}
|
|
}
|
|
|
|
func TestFileStoreCompact(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 350
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
for i := 0; i < 10; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
if state := fs.State(); state.Msgs != 10 {
|
|
t.Fatalf("Expected 10 msgs, got %d", state.Msgs)
|
|
}
|
|
n, err := fs.Compact(6)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if n != 5 {
|
|
t.Fatalf("Expected to have purged 5 msgs, got %d", n)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != 5 {
|
|
t.Fatalf("Expected 5 msgs, got %d", state.Msgs)
|
|
}
|
|
if state.FirstSeq != 6 {
|
|
t.Fatalf("Expected first seq of 6, got %d", state.FirstSeq)
|
|
}
|
|
// Now test that compact will also reset first if seq > last
|
|
n, err = fs.Compact(100)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if n != 5 {
|
|
t.Fatalf("Expected to have purged 5 msgs, got %d", n)
|
|
}
|
|
if state = fs.State(); state.FirstSeq != 100 {
|
|
t.Fatalf("Expected first seq of 100, got %d", state.FirstSeq)
|
|
}
|
|
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state = fs.State(); state.FirstSeq != 100 {
|
|
t.Fatalf("Expected first seq of 100, got %d", state.FirstSeq)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreCompactLastPlusOne(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 8192
|
|
fcfg.AsyncFlush = true
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", make([]byte, 10_000)
|
|
for i := 0; i < 10_000; i++ {
|
|
if _, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// The performance of this test is quite terrible with compression
|
|
// if we have AsyncFlush = false, so we'll batch flushes instead.
|
|
fs.mu.Lock()
|
|
fs.checkAndFlushLastBlock()
|
|
fs.mu.Unlock()
|
|
|
|
if state := fs.State(); state.Msgs != 10_000 {
|
|
t.Fatalf("Expected 1000000 msgs, got %d", state.Msgs)
|
|
}
|
|
if _, err := fs.Compact(10_001); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != 0 {
|
|
t.Fatalf("Expected no message but got %d", state.Msgs)
|
|
}
|
|
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
state = fs.State()
|
|
if state.Msgs != 1 {
|
|
t.Fatalf("Expected one message but got %d", state.Msgs)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreCompactMsgCountBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
for i := 0; i < 10; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
if state := fs.State(); state.Msgs != 10 {
|
|
t.Fatalf("Expected 10 msgs, got %d", state.Msgs)
|
|
}
|
|
// Now delete 2,3,4.
|
|
fs.EraseMsg(2)
|
|
fs.EraseMsg(3)
|
|
fs.EraseMsg(4)
|
|
|
|
// Also delete 7,8, and 9.
|
|
fs.RemoveMsg(7)
|
|
fs.RemoveMsg(8)
|
|
fs.RemoveMsg(9)
|
|
|
|
n, err := fs.Compact(6)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
// 1 & 5
|
|
if n != 2 {
|
|
t.Fatalf("Expected to have deleted 2 msgs, got %d", n)
|
|
}
|
|
if state := fs.State(); state.Msgs != 2 {
|
|
t.Fatalf("Expected to have 2 remaining, got %d", state.Msgs)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreCompactPerf(t *testing.T) {
|
|
t.SkipNow()
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 8192
|
|
fcfg.AsyncFlush = true
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
for i := 0; i < 100_000; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
if state := fs.State(); state.Msgs != 100_000 {
|
|
t.Fatalf("Expected 1000000 msgs, got %d", state.Msgs)
|
|
}
|
|
start := time.Now()
|
|
n, err := fs.Compact(90_001)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
t.Logf("Took %v to compact\n", time.Since(start))
|
|
|
|
if n != 90_000 {
|
|
t.Fatalf("Expected to have purged 90_000 msgs, got %d", n)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != 10_000 {
|
|
t.Fatalf("Expected 10_000 msgs, got %d", state.Msgs)
|
|
}
|
|
if state.FirstSeq != 90_001 {
|
|
t.Fatalf("Expected first seq of 90_001, got %d", state.FirstSeq)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreStreamTruncate(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 350
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
tseq := uint64(50)
|
|
|
|
subj, toStore := "foo", uint64(100)
|
|
for i := uint64(1); i < tseq; i++ {
|
|
_, _, err := fs.StoreMsg(subj, nil, []byte("ok"), 0)
|
|
require_NoError(t, err)
|
|
}
|
|
subj = "bar"
|
|
for i := tseq; i <= toStore; i++ {
|
|
_, _, err := fs.StoreMsg(subj, nil, []byte("ok"), 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
if state := fs.State(); state.Msgs != toStore {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
|
|
if err := fs.Truncate(tseq); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if state := fs.State(); state.Msgs != tseq {
|
|
t.Fatalf("Expected %d msgs, got %d", tseq, state.Msgs)
|
|
}
|
|
|
|
// Now make sure we report properly if we have some deleted interior messages.
|
|
fs.RemoveMsg(10)
|
|
fs.RemoveMsg(20)
|
|
fs.RemoveMsg(30)
|
|
fs.RemoveMsg(40)
|
|
|
|
tseq = uint64(25)
|
|
if err := fs.Truncate(tseq); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != tseq-2 {
|
|
t.Fatalf("Expected %d msgs, got %d", tseq-2, state.Msgs)
|
|
}
|
|
expected := []uint64{10, 20}
|
|
if !reflect.DeepEqual(state.Deleted, expected) {
|
|
t.Fatalf("Expected deleted to be %+v, got %+v\n", expected, state.Deleted)
|
|
}
|
|
|
|
before := state
|
|
|
|
// Make sure we can recover same state.
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of %+v, got %+v", before, state)
|
|
}
|
|
|
|
mb := fs.getFirstBlock()
|
|
require_True(t, mb != nil)
|
|
require_NoError(t, mb.loadMsgs())
|
|
|
|
// Also make sure we can recover properly with no index.db present.
|
|
// We want to make sure we preserve tombstones from any blocks being deleted.
|
|
fs.Stop()
|
|
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of %+v, got %+v without index.db state", before, state)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreRemovePartialRecovery(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
toStore := 100
|
|
for i := 0; i < toStore; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != uint64(toStore) {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
|
|
// Remove half
|
|
for i := 1; i <= toStore/2; i++ {
|
|
fs.RemoveMsg(uint64(i))
|
|
}
|
|
|
|
state = fs.State()
|
|
if state.Msgs != uint64(toStore/2) {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore/2, state.Msgs)
|
|
}
|
|
|
|
// Make sure we recover same state.
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state2 := fs.State()
|
|
if !reflect.DeepEqual(state2, state) {
|
|
t.Fatalf("Expected recovered state to be the same, got %+v vs %+v\n", state2, state)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreRemoveOutOfOrderRecovery(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
toStore := 100
|
|
for i := 0; i < toStore; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != uint64(toStore) {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
|
|
// Remove evens
|
|
for i := 2; i <= toStore; i += 2 {
|
|
if removed, _ := fs.RemoveMsg(uint64(i)); !removed {
|
|
t.Fatalf("Expected remove to return true")
|
|
}
|
|
}
|
|
|
|
state = fs.State()
|
|
if state.Msgs != uint64(toStore/2) {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore/2, state.Msgs)
|
|
}
|
|
|
|
var smv StoreMsg
|
|
if _, err := fs.LoadMsg(1, &smv); err != nil {
|
|
t.Fatalf("Expected to retrieve seq 1")
|
|
}
|
|
for i := 2; i <= toStore; i += 2 {
|
|
if _, err := fs.LoadMsg(uint64(i), &smv); err == nil {
|
|
t.Fatalf("Expected error looking up seq %d that should be deleted", i)
|
|
}
|
|
}
|
|
|
|
// Make sure we recover same state.
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state2 := fs.State()
|
|
if !reflect.DeepEqual(state2, state) {
|
|
t.Fatalf("Expected recovered states to be the same, got %+v vs %+v\n", state, state2)
|
|
}
|
|
|
|
if _, err := fs.LoadMsg(1, &smv); err != nil {
|
|
t.Fatalf("Expected to retrieve seq 1")
|
|
}
|
|
for i := 2; i <= toStore; i += 2 {
|
|
if _, err := fs.LoadMsg(uint64(i), nil); err == nil {
|
|
t.Fatalf("Expected error looking up seq %d that should be deleted", i)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreAgeLimitRecovery(t *testing.T) {
|
|
maxAge := 1 * time.Second
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.CacheExpire = 1 * time.Millisecond
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage, MaxAge: maxAge}
|
|
created := time.Now()
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Store some messages. Does not really matter how many.
|
|
subj, msg := "foo", []byte("Hello World")
|
|
toStore := 100
|
|
for i := 0; i < toStore; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != uint64(toStore) {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
fs.Stop()
|
|
|
|
time.Sleep(maxAge)
|
|
|
|
fcfg.CacheExpire = 0
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Make sure they expire.
|
|
checkFor(t, time.Second, 2*maxAge, func() error {
|
|
t.Helper()
|
|
state = fs.State()
|
|
if state.Msgs != 0 {
|
|
return fmt.Errorf("Expected no msgs, got %d", state.Msgs)
|
|
}
|
|
if state.Bytes != 0 {
|
|
return fmt.Errorf("Expected no bytes, got %d", state.Bytes)
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestFileStoreBitRot(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Store some messages. Does not really matter how many.
|
|
subj, msg := "foo", []byte("Hello World")
|
|
toStore := 100
|
|
for i := 0; i < toStore; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != uint64(toStore) {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
|
|
if ld, err := fs.checkMsgs(); err != nil {
|
|
t.Fatalf("Unexpected error from checkMsgs: %v", err)
|
|
} else if ld != nil && len(ld.Msgs) > 0 {
|
|
t.Fatalf("Expected to have no corrupt msgs, got %d", len(ld.Msgs))
|
|
}
|
|
|
|
for i := 0; i < 10; i++ {
|
|
// Now twiddle some bits.
|
|
fs.mu.Lock()
|
|
lmb := fs.lmb
|
|
contents, err := os.ReadFile(lmb.mfn)
|
|
require_NoError(t, err)
|
|
require_True(t, len(contents) > 0)
|
|
|
|
var index int
|
|
for {
|
|
index = rand.Intn(len(contents))
|
|
// Reverse one byte anywhere.
|
|
b := contents[index]
|
|
contents[index] = bits.Reverse8(b)
|
|
if b != contents[index] {
|
|
break
|
|
}
|
|
}
|
|
os.WriteFile(lmb.mfn, contents, 0644)
|
|
fs.mu.Unlock()
|
|
|
|
ld, err := fs.checkMsgs()
|
|
require_NoError(t, err)
|
|
if len(ld.Msgs) > 0 {
|
|
break
|
|
}
|
|
// If our bitrot caused us to not be able to recover any messages we can break as well.
|
|
if state := fs.State(); state.Msgs == 0 {
|
|
break
|
|
}
|
|
// Fail the test if we have tried the 10 times and still did not
|
|
// get any corruption report.
|
|
if i == 9 {
|
|
t.Fatalf("Expected to have corrupt msgs got none: changed [%d]", index)
|
|
}
|
|
}
|
|
|
|
// Make sure we can restore.
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// checkMsgs will repair the underlying store, so checkMsgs should be clean now.
|
|
if ld, err := fs.checkMsgs(); err != nil {
|
|
t.Fatalf("Unexpected error from checkMsgs: %v", err)
|
|
} else if ld != nil {
|
|
// If we have no msgs left this will report the head msgs as lost again.
|
|
if state := fs.State(); state.Msgs > 0 {
|
|
t.Fatalf("Expected no errors restoring checked and fixed filestore, got %+v", ld)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreEraseMsg(t *testing.T) {
|
|
// Just do no encryption, etc.
|
|
fcfg := FileStoreConfig{StoreDir: t.TempDir()}
|
|
fs, err := newFileStore(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
fs.StoreMsg(subj, nil, msg, 0) // To keep block from being deleted.
|
|
var smv StoreMsg
|
|
sm, err := fs.LoadMsg(1, &smv)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error looking up msg: %v", err)
|
|
}
|
|
if !bytes.Equal(msg, sm.msg) {
|
|
t.Fatalf("Expected same msg, got %q vs %q", sm.msg, msg)
|
|
}
|
|
if removed, _ := fs.EraseMsg(1); !removed {
|
|
t.Fatalf("Expected erase msg to return success")
|
|
}
|
|
if sm2, _ := fs.msgForSeq(1, nil); sm2 != nil {
|
|
t.Fatalf("Expected msg to be erased")
|
|
}
|
|
fs.checkAndFlushLastBlock()
|
|
|
|
// Now look on disk as well.
|
|
rl := fileStoreMsgSize(subj, nil, msg)
|
|
buf := make([]byte, rl)
|
|
fp, err := os.Open(filepath.Join(fcfg.StoreDir, msgDir, fmt.Sprintf(blkScan, 1)))
|
|
if err != nil {
|
|
t.Fatalf("Error opening msg block file: %v", err)
|
|
}
|
|
defer fp.Close()
|
|
|
|
fp.ReadAt(buf, 0)
|
|
fs.mu.RLock()
|
|
mb := fs.blks[0]
|
|
fs.mu.RUnlock()
|
|
mb.mu.Lock()
|
|
sm, err = mb.msgFromBuf(buf, nil, nil)
|
|
mb.mu.Unlock()
|
|
if err != nil {
|
|
t.Fatalf("error reading message from block: %v", err)
|
|
}
|
|
if sm.subj == subj {
|
|
t.Fatalf("Expected the subjects to be different")
|
|
}
|
|
if sm.seq != 0 && sm.seq&ebit == 0 {
|
|
t.Fatalf("Expected seq to be 0, marking as deleted, got %d", sm.seq)
|
|
}
|
|
if sm.ts != 0 {
|
|
t.Fatalf("Expected timestamp to be 0, got %d", sm.ts)
|
|
}
|
|
if bytes.Equal(sm.msg, msg) {
|
|
t.Fatalf("Expected message body to be randomized")
|
|
}
|
|
}
|
|
|
|
func TestFileStoreEraseAndNoIndexRecovery(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
toStore := 100
|
|
for i := 0; i < toStore; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != uint64(toStore) {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore, state.Msgs)
|
|
}
|
|
|
|
// Erase the even messages.
|
|
for i := 2; i <= toStore; i += 2 {
|
|
if removed, _ := fs.EraseMsg(uint64(i)); !removed {
|
|
t.Fatalf("Expected erase msg to return true")
|
|
}
|
|
}
|
|
state = fs.State()
|
|
if state.Msgs != uint64(toStore/2) {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore/2, state.Msgs)
|
|
}
|
|
|
|
// Stop and remove the optional index file.
|
|
fs.Stop()
|
|
ifn := filepath.Join(fcfg.StoreDir, msgDir, fmt.Sprintf(indexScan, 1))
|
|
os.Remove(ifn)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state = fs.State()
|
|
if state.Msgs != uint64(toStore/2) {
|
|
t.Fatalf("Expected %d msgs, got %d", toStore/2, state.Msgs)
|
|
}
|
|
|
|
for i := 2; i <= toStore; i += 2 {
|
|
if _, err := fs.LoadMsg(uint64(i), nil); err == nil {
|
|
t.Fatalf("Expected error looking up seq %d that should be erased", i)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMeta(t *testing.T) {
|
|
// Just do no encryption, etc.
|
|
fcfg := FileStoreConfig{StoreDir: t.TempDir()}
|
|
mconfig := StreamConfig{Name: "ZZ-22-33", Storage: FileStorage, Subjects: []string{"foo.*"}, Replicas: 22}
|
|
fs, err := newFileStore(fcfg, mconfig)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
metafile := filepath.Join(fcfg.StoreDir, JetStreamMetaFile)
|
|
metasum := filepath.Join(fcfg.StoreDir, JetStreamMetaFileSum)
|
|
|
|
// Test to make sure meta file and checksum are present.
|
|
if _, err := os.Stat(metafile); os.IsNotExist(err) {
|
|
t.Fatalf("Expected metafile %q to exist", metafile)
|
|
}
|
|
if _, err := os.Stat(metasum); os.IsNotExist(err) {
|
|
t.Fatalf("Expected metafile's checksum %q to exist", metasum)
|
|
}
|
|
|
|
buf, err := os.ReadFile(metafile)
|
|
if err != nil {
|
|
t.Fatalf("Error reading metafile: %v", err)
|
|
}
|
|
var mconfig2 StreamConfig
|
|
if err := json.Unmarshal(buf, &mconfig2); err != nil {
|
|
t.Fatalf("Error unmarshalling: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(mconfig, mconfig2) {
|
|
t.Fatalf("Stream configs not equal, got %+v vs %+v", mconfig2, mconfig)
|
|
}
|
|
checksum, err := os.ReadFile(metasum)
|
|
if err != nil {
|
|
t.Fatalf("Error reading metafile checksum: %v", err)
|
|
}
|
|
|
|
fs.mu.Lock()
|
|
fs.hh.Reset()
|
|
fs.hh.Write(buf)
|
|
mychecksum := hex.EncodeToString(fs.hh.Sum(nil))
|
|
fs.mu.Unlock()
|
|
|
|
if mychecksum != string(checksum) {
|
|
t.Fatalf("Checksums do not match, got %q vs %q", mychecksum, checksum)
|
|
}
|
|
|
|
// Now create a consumer. Same deal for them.
|
|
oconfig := ConsumerConfig{
|
|
DeliverSubject: "d",
|
|
FilterSubject: "foo",
|
|
AckPolicy: AckAll,
|
|
}
|
|
oname := "obs22"
|
|
obs, err := fs.ConsumerStore(oname, time.Time{}, &oconfig)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
ometafile := filepath.Join(fcfg.StoreDir, consumerDir, oname, JetStreamMetaFile)
|
|
ometasum := filepath.Join(fcfg.StoreDir, consumerDir, oname, JetStreamMetaFileSum)
|
|
|
|
// Test to make sure meta file and checksum are present.
|
|
if _, err := os.Stat(ometafile); os.IsNotExist(err) {
|
|
t.Fatalf("Expected consumer metafile %q to exist", ometafile)
|
|
}
|
|
if _, err := os.Stat(ometasum); os.IsNotExist(err) {
|
|
t.Fatalf("Expected consumer metafile's checksum %q to exist", ometasum)
|
|
}
|
|
|
|
buf, err = os.ReadFile(ometafile)
|
|
if err != nil {
|
|
t.Fatalf("Error reading consumer metafile: %v", err)
|
|
}
|
|
|
|
var oconfig2 ConsumerConfig
|
|
if err := json.Unmarshal(buf, &oconfig2); err != nil {
|
|
t.Fatalf("Error unmarshalling: %v", err)
|
|
}
|
|
// Since we set name we will get that back now.
|
|
oconfig.Name = oname
|
|
if !reflect.DeepEqual(oconfig2, oconfig) {
|
|
t.Fatalf("Consumer configs not equal, got %+v vs %+v", oconfig2, oconfig)
|
|
}
|
|
checksum, err = os.ReadFile(ometasum)
|
|
|
|
if err != nil {
|
|
t.Fatalf("Error reading consumer metafile checksum: %v", err)
|
|
}
|
|
|
|
hh := obs.(*consumerFileStore).hh
|
|
hh.Reset()
|
|
hh.Write(buf)
|
|
mychecksum = hex.EncodeToString(hh.Sum(nil))
|
|
if mychecksum != string(checksum) {
|
|
t.Fatalf("Checksums do not match, got %q vs %q", mychecksum, checksum)
|
|
}
|
|
}
|
|
|
|
func TestFileStoreWriteAndReadSameBlock(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World!")
|
|
|
|
for i := uint64(1); i <= 10; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
if _, err := fs.LoadMsg(i, nil); err != nil {
|
|
t.Fatalf("Error loading %d: %v", i, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreAndRetrieveMultiBlock(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
subj, msg := "foo", []byte("Hello World!")
|
|
storedMsgSize := fileStoreMsgSize(subj, nil, msg)
|
|
|
|
fcfg.BlockSize = 4 * storedMsgSize
|
|
created := time.Now()
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 0; i < 20; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != 20 {
|
|
t.Fatalf("Expected 20 msgs, got %d", state.Msgs)
|
|
}
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
var smv StoreMsg
|
|
for i := uint64(1); i <= 20; i++ {
|
|
if _, err := fs.LoadMsg(i, &smv); err != nil {
|
|
t.Fatalf("Error loading %d: %v", i, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreCollapseDmap(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
subj, msg := "foo", []byte("Hello World!")
|
|
storedMsgSize := fileStoreMsgSize(subj, nil, msg)
|
|
|
|
fcfg.BlockSize = 4 * storedMsgSize
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 0; i < 10; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != 10 {
|
|
t.Fatalf("Expected 10 msgs, got %d", state.Msgs)
|
|
}
|
|
|
|
checkDmapTotal := func(total int) {
|
|
t.Helper()
|
|
if nde := fs.dmapEntries(); nde != total {
|
|
t.Fatalf("Expecting only %d entries, got %d", total, nde)
|
|
}
|
|
}
|
|
|
|
checkFirstSeq := func(seq uint64) {
|
|
t.Helper()
|
|
state := fs.State()
|
|
if state.FirstSeq != seq {
|
|
t.Fatalf("Expected first seq to be %d, got %d", seq, state.FirstSeq)
|
|
}
|
|
}
|
|
|
|
// Now remove some out of order, forming gaps and entries in dmaps.
|
|
fs.RemoveMsg(2)
|
|
checkFirstSeq(1)
|
|
fs.RemoveMsg(4)
|
|
checkFirstSeq(1)
|
|
fs.RemoveMsg(8)
|
|
checkFirstSeq(1)
|
|
|
|
state = fs.State()
|
|
if state.Msgs != 7 {
|
|
t.Fatalf("Expected 7 msgs, got %d", state.Msgs)
|
|
}
|
|
|
|
checkDmapTotal(3)
|
|
|
|
// Close gaps..
|
|
fs.RemoveMsg(1)
|
|
checkDmapTotal(2)
|
|
checkFirstSeq(3)
|
|
|
|
fs.RemoveMsg(3)
|
|
checkDmapTotal(1)
|
|
checkFirstSeq(5)
|
|
|
|
fs.RemoveMsg(5)
|
|
checkDmapTotal(1)
|
|
checkFirstSeq(6)
|
|
|
|
fs.RemoveMsg(7)
|
|
checkDmapTotal(2)
|
|
|
|
fs.RemoveMsg(6)
|
|
checkDmapTotal(0)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreReadCache(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.CacheExpire = ats.TickInterval
|
|
|
|
subj, msg := "foo.bar", make([]byte, 1024)
|
|
storedMsgSize := fileStoreMsgSize(subj, nil, msg)
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
toStore := 500
|
|
totalBytes := uint64(toStore) * storedMsgSize
|
|
|
|
for i := 0; i < toStore; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
// Wait for cache to go to zero.
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
if csz := fs.cacheSize(); csz != 0 {
|
|
return fmt.Errorf("cache size not 0, got %s", friendlyBytes(int64(csz)))
|
|
}
|
|
return nil
|
|
})
|
|
|
|
fs.LoadMsg(1, nil)
|
|
if csz := fs.cacheSize(); csz != totalBytes {
|
|
t.Fatalf("Expected all messages to be cached, got %d vs %d", csz, totalBytes)
|
|
}
|
|
// Should expire and be removed.
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
if csz := fs.cacheSize(); csz != 0 {
|
|
return fmt.Errorf("cache size not 0, got %s", friendlyBytes(int64(csz)))
|
|
}
|
|
return nil
|
|
})
|
|
if cls := fs.cacheLoads(); cls != 1 {
|
|
t.Fatalf("Expected only 1 cache load, got %d", cls)
|
|
}
|
|
// Now make sure we do not reload cache if there is activity.
|
|
fs.LoadMsg(1, nil)
|
|
timeout := time.Now().Add(250 * time.Millisecond)
|
|
for time.Now().Before(timeout) {
|
|
if cls := fs.cacheLoads(); cls != 2 {
|
|
t.Fatalf("cache loads not 2, got %d", cls)
|
|
}
|
|
time.Sleep(5 * time.Millisecond)
|
|
fs.LoadMsg(1, nil) // register activity.
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStorePartialCacheExpiration(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cexp := 10 * time.Millisecond
|
|
fcfg.CacheExpire = cexp
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.StoreMsg("foo", nil, []byte("msg1"), 0)
|
|
|
|
// Should expire and be removed.
|
|
time.Sleep(2 * cexp)
|
|
fs.StoreMsg("bar", nil, []byte("msg2"), 0)
|
|
|
|
// Again wait for cache to expire.
|
|
time.Sleep(2 * cexp)
|
|
if _, err := fs.LoadMsg(1, nil); err != nil {
|
|
t.Fatalf("Error loading message 1: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStorePartialIndexes(t *testing.T) {
|
|
// TODO(nat): This test is no longer applicable as we no longer have positional
|
|
// write caches but check before removing whether it proves anything else of value.
|
|
t.SkipNow()
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cexp := 10 * time.Millisecond
|
|
fcfg.CacheExpire = cexp
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
toSend := 5
|
|
for i := 0; i < toSend; i++ {
|
|
fs.StoreMsg("foo", nil, []byte("ok-1"), 0)
|
|
}
|
|
|
|
// Now wait til the cache expires, including the index.
|
|
fs.mu.Lock()
|
|
mb := fs.blks[0]
|
|
fs.mu.Unlock()
|
|
|
|
// Force idx to expire by resetting last remove ts.
|
|
mb.mu.Lock()
|
|
mb.llts = mb.llts - int64(defaultCacheBufferExpiration*2)
|
|
mb.mu.Unlock()
|
|
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
if mb.cache == nil || len(mb.cache.idx) == 0 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Index not empty")
|
|
})
|
|
|
|
// Create a partial cache by adding more msgs.
|
|
for i := 0; i < toSend; i++ {
|
|
fs.StoreMsg("foo", nil, []byte("ok-2"), 0)
|
|
}
|
|
// If we now load in a message in second half if we do not
|
|
// detect idx is a partial correctly this will panic.
|
|
if _, err := fs.LoadMsg(8, nil); err != nil {
|
|
t.Fatalf("Error loading %d: %v", 1, err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreInvalidIndexesRebuilt(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
toSend := 5
|
|
for i := 0; i < toSend; i++ {
|
|
_, _, err = fs.StoreMsg("foo", nil, []byte("ok-1"), 0)
|
|
require_NoError(t, err)
|
|
}
|
|
require_NoError(t, fs.FlushAllPending())
|
|
|
|
// Now we're going to mangle the in-memory cache by changing
|
|
// the sequence number of the first message. We also need to
|
|
// trick the cache into believing the hash is already validated
|
|
// so we don't fail on that. This is specifically testing the
|
|
// seq != fsm.seq condition.
|
|
mb := fs.selectMsgBlock(1)
|
|
require_NotNil(t, mb)
|
|
require_NoError(t, mb.loadMsgs())
|
|
require_True(t, mb.cacheAlreadyLoaded())
|
|
ri, rl, _, err := mb.slotInfo(0)
|
|
require_NoError(t, err)
|
|
require_NotNil(t, mb.cache)
|
|
require_NotNil(t, mb.cache.buf)
|
|
slot := mb.cache.buf[ri : ri+rl]
|
|
require_Equal(t, binary.LittleEndian.Uint64(slot[4:]), 1)
|
|
binary.LittleEndian.PutUint64(slot[4:], 12345)
|
|
mb.cache.idx[0] = (mb.cache.idx[0] | cbit)
|
|
|
|
// Expect an error on the first instance and for cacheLookupEx
|
|
// to discard the cache.
|
|
_, err = mb.cacheLookupEx(1, nil, false)
|
|
require_Error(t, err)
|
|
require_True(t, mb.ecache.Value() == nil)
|
|
|
|
// Now fetchMsg should notice and rebuild the index with the
|
|
// correct sequence from disk.
|
|
sm, _, err := mb.fetchMsg(1, nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, sm.seq, 1)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreSnapshot(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
subj, msg := "foo", []byte("Hello Snappy!")
|
|
scfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, scfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
toSend := 2233
|
|
for i := 0; i < toSend; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
// Create a few consumers.
|
|
o1, err := fs.ConsumerStore("o22", time.Time{}, &ConsumerConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
o2, err := fs.ConsumerStore("o33", time.Time{}, &ConsumerConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
state := &ConsumerState{}
|
|
state.Delivered.Consumer = 100
|
|
state.Delivered.Stream = 100
|
|
state.AckFloor.Consumer = 22
|
|
state.AckFloor.Stream = 22
|
|
|
|
if err := o1.Update(state); err != nil {
|
|
t.Fatalf("Unexpected error updating state: %v", err)
|
|
}
|
|
state.AckFloor.Consumer = 33
|
|
state.AckFloor.Stream = 33
|
|
|
|
if err := o2.Update(state); err != nil {
|
|
t.Fatalf("Unexpected error updating state: %v", err)
|
|
}
|
|
|
|
snapshot := func() []byte {
|
|
t.Helper()
|
|
r, err := fs.Snapshot(5*time.Second, true, true)
|
|
if err != nil {
|
|
t.Fatalf("Error creating snapshot")
|
|
}
|
|
snapshot, err := io.ReadAll(r.Reader)
|
|
if err != nil {
|
|
t.Fatalf("Error reading snapshot")
|
|
}
|
|
return snapshot
|
|
}
|
|
|
|
// This will unzip the snapshot and create a new filestore that will recover the state.
|
|
// We will compare the states for this vs the original one.
|
|
verifySnapshot := func(snap []byte) {
|
|
t.Helper()
|
|
r := bytes.NewReader(snap)
|
|
tr := tar.NewReader(s2.NewReader(r))
|
|
|
|
rstoreDir := t.TempDir()
|
|
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
break // End of archive
|
|
}
|
|
if err != nil {
|
|
t.Fatalf("Error getting next entry from snapshot: %v", err)
|
|
}
|
|
fpath := filepath.Join(rstoreDir, filepath.Clean(hdr.Name))
|
|
pdir := filepath.Dir(fpath)
|
|
os.MkdirAll(pdir, 0755)
|
|
fd, err := os.OpenFile(fpath, os.O_CREATE|os.O_RDWR, 0600)
|
|
if err != nil {
|
|
t.Fatalf("Error opening file[%s]: %v", fpath, err)
|
|
}
|
|
if _, err := io.Copy(fd, tr); err != nil {
|
|
t.Fatalf("Error writing file[%s]: %v", fpath, err)
|
|
}
|
|
fd.Close()
|
|
}
|
|
|
|
fcfg.StoreDir = rstoreDir
|
|
fsr, err := newFileStoreWithCreated(fcfg, scfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fsr.Stop()
|
|
state := fs.State()
|
|
rstate := fsr.State()
|
|
|
|
// FIXME(dlc)
|
|
// Right now the upper layers in JetStream recover the consumers and do not expect
|
|
// the lower layers to do that. So for now blank that out of our original state.
|
|
// Will have more exhaustive tests in jetstream_test.go.
|
|
state.Consumers = 0
|
|
|
|
// Just check the state.
|
|
if !reflect.DeepEqual(rstate, state) {
|
|
t.Fatalf("Restored state does not match:\n%+v\n\n%+v", rstate, state)
|
|
}
|
|
}
|
|
|
|
// Simple case first.
|
|
snap := snapshot()
|
|
verifySnapshot(snap)
|
|
|
|
// Remove first 100 messages.
|
|
for i := 1; i <= 100; i++ {
|
|
fs.RemoveMsg(uint64(i))
|
|
}
|
|
|
|
snap = snapshot()
|
|
verifySnapshot(snap)
|
|
|
|
// Now sporadic messages inside the stream.
|
|
total := int64(toSend - 100)
|
|
// Delete 50 random messages.
|
|
for i := 0; i < 50; i++ {
|
|
seq := uint64(rand.Int63n(total) + 101)
|
|
fs.RemoveMsg(seq)
|
|
}
|
|
|
|
snap = snapshot()
|
|
verifySnapshot(snap)
|
|
|
|
// Make sure compaction works with snapshots.
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
// Should not call compact on last msg block.
|
|
if mb != fs.lmb {
|
|
mb.mu.Lock()
|
|
err = mb.compact()
|
|
mb.mu.Unlock()
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
fs.mu.RUnlock()
|
|
|
|
snap = snapshot()
|
|
verifySnapshot(snap)
|
|
|
|
// Now check to make sure that we can still delete/erase messages.
|
|
sr, err := fs.Snapshot(5*time.Second, false, true)
|
|
if err != nil {
|
|
t.Fatalf("Error creating snapshot")
|
|
}
|
|
_, err = fs.RemoveMsg(122)
|
|
require_NoError(t, err)
|
|
|
|
// Now make sure we can do these when we close the reader and release the snapshot condition.
|
|
sr.Reader.Close()
|
|
checkFor(t, time.Second, 10*time.Millisecond, func() error {
|
|
fs.mu.RLock()
|
|
defer fs.mu.RUnlock()
|
|
if fs.sips != 0 {
|
|
return errors.New("snapshot is not finished")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Make sure if we do not read properly then it will close the writer and report an error.
|
|
sr, err = fs.Snapshot(25*time.Millisecond, false, false)
|
|
if err != nil {
|
|
t.Fatalf("Error creating snapshot")
|
|
}
|
|
|
|
// Cause snapshot to timeout.
|
|
time.Sleep(50 * time.Millisecond)
|
|
// Read should fail
|
|
var buf [32]byte
|
|
if _, err := sr.Reader.Read(buf[:]); err != io.EOF {
|
|
t.Fatalf("Expected read to produce an error, got none")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreSnapshotAndSyncBlocks(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
scfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, scfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.cancelSyncTimer()
|
|
fs.syncBlocks()
|
|
|
|
fs.mu.Lock()
|
|
if fs.syncTmr == nil {
|
|
fs.mu.Unlock()
|
|
t.Fatal("Expected sync timer to be set")
|
|
}
|
|
// Simulate a snapshot being in progress. This should not prevent us syncing blocks.
|
|
fs.sips++
|
|
fs.mu.Unlock()
|
|
|
|
fs.cancelSyncTimer()
|
|
fs.syncBlocks()
|
|
|
|
fs.mu.Lock()
|
|
if fs.syncTmr == nil {
|
|
fs.mu.Unlock()
|
|
t.Fatal("Expected sync timer to be set")
|
|
}
|
|
fs.mu.Unlock()
|
|
})
|
|
}
|
|
|
|
func TestFileStoreConsumer(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
o, err := fs.ConsumerStore("obs22", time.Time{}, &ConsumerConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if state, err := o.State(); err != nil || state.Delivered.Consumer != 0 {
|
|
t.Fatalf("Unexpected state or error: %v", err)
|
|
}
|
|
|
|
state := &ConsumerState{}
|
|
|
|
updateAndCheck := func() {
|
|
t.Helper()
|
|
if err := o.Update(state); err != nil {
|
|
t.Fatalf("Unexpected error updating state: %v", err)
|
|
}
|
|
s2, err := o.State()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error getting state: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(state, s2) {
|
|
t.Fatalf("State is not the same: wanted %+v got %+v", state, s2)
|
|
}
|
|
}
|
|
|
|
shouldFail := func() {
|
|
t.Helper()
|
|
if err := o.Update(state); err == nil {
|
|
t.Fatalf("Expected an error and got none")
|
|
}
|
|
}
|
|
|
|
state.Delivered.Consumer = 1
|
|
state.Delivered.Stream = 22
|
|
updateAndCheck()
|
|
|
|
state.Delivered.Consumer = 100
|
|
state.Delivered.Stream = 122
|
|
state.AckFloor.Consumer = 50
|
|
state.AckFloor.Stream = 123
|
|
// This should fail, bad state.
|
|
shouldFail()
|
|
// So should this.
|
|
state.AckFloor.Consumer = 200
|
|
state.AckFloor.Stream = 100
|
|
shouldFail()
|
|
|
|
// Should succeed
|
|
state.AckFloor.Consumer = 50
|
|
state.AckFloor.Stream = 72
|
|
updateAndCheck()
|
|
|
|
tn := time.Now().UnixNano()
|
|
|
|
// We should sanity check pending here as well, so will check if a pending value is below
|
|
// ack floor or above delivered.
|
|
state.Pending = map[uint64]*Pending{70: {70, tn}}
|
|
shouldFail()
|
|
state.Pending = map[uint64]*Pending{140: {140, tn}}
|
|
shouldFail()
|
|
state.Pending = map[uint64]*Pending{72: {72, tn}} // exact on floor should fail
|
|
shouldFail()
|
|
|
|
// Put timestamps a second apart.
|
|
// We will downsample to second resolution to save space. So setup our times
|
|
// to reflect that.
|
|
ago := time.Now().Add(-30 * time.Second).Truncate(time.Second)
|
|
nt := func() *Pending {
|
|
ago = ago.Add(time.Second)
|
|
return &Pending{0, ago.UnixNano()}
|
|
}
|
|
// Should succeed.
|
|
state.Pending = map[uint64]*Pending{75: nt(), 80: nt(), 83: nt(), 90: nt(), 111: nt()}
|
|
updateAndCheck()
|
|
|
|
// Now do redlivery, but first with no pending.
|
|
state.Pending = nil
|
|
state.Redelivered = map[uint64]uint64{22: 3, 44: 8}
|
|
updateAndCheck()
|
|
|
|
// All together.
|
|
state.Pending = map[uint64]*Pending{75: nt(), 80: nt(), 83: nt(), 90: nt(), 111: nt()}
|
|
updateAndCheck()
|
|
|
|
// Large one
|
|
state.Delivered.Consumer = 10000
|
|
state.Delivered.Stream = 10000
|
|
state.AckFloor.Consumer = 100
|
|
state.AckFloor.Stream = 100
|
|
// Generate 8k pending.
|
|
state.Pending = make(map[uint64]*Pending)
|
|
for len(state.Pending) < 8192 {
|
|
seq := uint64(rand.Intn(9890) + 101)
|
|
if _, ok := state.Pending[seq]; !ok {
|
|
state.Pending[seq] = nt()
|
|
}
|
|
}
|
|
updateAndCheck()
|
|
|
|
state.Pending = nil
|
|
state.AckFloor.Consumer = 10000
|
|
state.AckFloor.Stream = 10000
|
|
updateAndCheck()
|
|
})
|
|
}
|
|
|
|
func TestFileStoreConsumerEncodeDecodeRedelivered(t *testing.T) {
|
|
state := &ConsumerState{}
|
|
|
|
state.Delivered.Consumer = 100
|
|
state.Delivered.Stream = 100
|
|
state.AckFloor.Consumer = 50
|
|
state.AckFloor.Stream = 50
|
|
|
|
state.Redelivered = map[uint64]uint64{122: 3, 144: 8}
|
|
buf := encodeConsumerState(state)
|
|
|
|
rstate, err := decodeConsumerState(buf)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(state, rstate) {
|
|
t.Fatalf("States do not match: %+v vs %+v", state, rstate)
|
|
}
|
|
}
|
|
|
|
func TestFileStoreConsumerEncodeDecodePendingBelowStreamAckFloor(t *testing.T) {
|
|
state := &ConsumerState{}
|
|
|
|
state.Delivered.Consumer = 1192
|
|
state.Delivered.Stream = 10185
|
|
state.AckFloor.Consumer = 1189
|
|
state.AckFloor.Stream = 10815
|
|
|
|
now := time.Now().Round(time.Second).Add(-10 * time.Second).UnixNano()
|
|
state.Pending = map[uint64]*Pending{
|
|
10782: {1190, now},
|
|
10810: {1191, now + int64(time.Second)},
|
|
10815: {1192, now + int64(2*time.Second)},
|
|
}
|
|
buf := encodeConsumerState(state)
|
|
|
|
rstate, err := decodeConsumerState(buf)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if len(rstate.Pending) != 3 {
|
|
t.Fatalf("Invalid pending: %v", rstate.Pending)
|
|
}
|
|
for k, v := range state.Pending {
|
|
rv, ok := rstate.Pending[k]
|
|
if !ok {
|
|
t.Fatalf("Did not find sseq=%v", k)
|
|
}
|
|
if !reflect.DeepEqual(v, rv) {
|
|
t.Fatalf("Pending for sseq=%v should be %+v, got %+v", k, v, rv)
|
|
}
|
|
}
|
|
state.Pending, rstate.Pending = nil, nil
|
|
if !reflect.DeepEqual(*state, *rstate) {
|
|
t.Fatalf("States do not match: %+v vs %+v", state, rstate)
|
|
}
|
|
}
|
|
|
|
func TestFileStoreWriteFailures(t *testing.T) {
|
|
// This test should be run inside an environment where this directory
|
|
// has a limited size.
|
|
// E.g. Docker
|
|
// docker run -ti --tmpfs /jswf_test:rw,size=32k --rm -v ~/Development/go/src:/go/src -w /go/src/github.com/nats-io/nats-server/ golang:1.21 /bin/bash
|
|
tdir := filepath.Join("/", "jswf_test")
|
|
if stat, err := os.Stat(tdir); err != nil || !stat.IsDir() {
|
|
t.SkipNow()
|
|
}
|
|
|
|
storeDir := filepath.Join(tdir, JetStreamStoreDir)
|
|
os.MkdirAll(storeDir, 0755)
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.StoreDir = storeDir
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello Write Failures!")
|
|
|
|
var lseq uint64
|
|
// msz about will be ~54 bytes, so if limit is 32k trying to send 1000 will fail at some point.
|
|
for i := 1; i <= 1000; i++ {
|
|
if _, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
lseq = uint64(i)
|
|
break
|
|
}
|
|
}
|
|
if lseq == 0 {
|
|
t.Fatalf("Expected to get a failure but did not")
|
|
}
|
|
|
|
state := fs.State()
|
|
|
|
if state.LastSeq != lseq-1 {
|
|
t.Fatalf("Expected last seq to be %d, got %d\n", lseq-1, state.LastSeq)
|
|
}
|
|
if state.Msgs != lseq-1 {
|
|
t.Fatalf("Expected total msgs to be %d, got %d\n", lseq-1, state.Msgs)
|
|
}
|
|
if _, err := fs.LoadMsg(lseq, nil); err == nil {
|
|
t.Fatalf("Expected error loading seq that failed, got none")
|
|
}
|
|
// Loading should still work.
|
|
if _, err := fs.LoadMsg(1, nil); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
// Make sure we recover same state.
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state2 := fs.State()
|
|
|
|
// Ignore lost state.
|
|
state.Lost, state2.Lost = nil, nil
|
|
if !reflect.DeepEqual(state2, state) {
|
|
t.Fatalf("Expected recovered state to be the same\n%+v\nvs\n%+v\n", state2, state)
|
|
}
|
|
|
|
// We should still fail here.
|
|
for i := 1; i <= 100; i++ {
|
|
_, _, err = fs.StoreMsg(subj, nil, msg, 0)
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
require_Error(t, err)
|
|
|
|
lseq = fs.State().LastSeq + 1
|
|
|
|
// Purge should help.
|
|
if _, err := fs.Purge(); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
// Wait for purge to complete its out of band processing.
|
|
time.Sleep(50 * time.Millisecond)
|
|
|
|
// Check we will fail again in same spot.
|
|
for i := 1; i <= 1000; i++ {
|
|
if _, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
if i != int(lseq) {
|
|
t.Fatalf("Expected to fail after purge about the same spot, wanted %d got %d", lseq, i)
|
|
}
|
|
break
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStorePerf(t *testing.T) {
|
|
// Comment out to run, holding place for now.
|
|
t.SkipNow()
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.AsyncFlush = true
|
|
|
|
subj, msg := "foo", make([]byte, 1024-33)
|
|
for i := 0; i < len(msg); i++ {
|
|
msg[i] = 'D'
|
|
}
|
|
storedMsgSize := fileStoreMsgSize(subj, nil, msg)
|
|
|
|
// 5GB
|
|
toStore := 5 * 1024 * 1024 * 1024 / storedMsgSize
|
|
|
|
fmt.Printf("storing %d msgs of %s each, totalling %s\n",
|
|
toStore,
|
|
friendlyBytes(int64(storedMsgSize)),
|
|
friendlyBytes(int64(toStore*storedMsgSize)),
|
|
)
|
|
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
start := time.Now()
|
|
for i := 0; i < int(toStore); i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
fs.Stop()
|
|
|
|
tt := time.Since(start)
|
|
fmt.Printf("time to store is %v\n", tt)
|
|
fmt.Printf("%.0f msgs/sec\n", float64(toStore)/tt.Seconds())
|
|
fmt.Printf("%s per sec\n", friendlyBytes(int64(float64(toStore*storedMsgSize)/tt.Seconds())))
|
|
|
|
fmt.Printf("Filesystem cache flush, paused 5 seconds.\n\n")
|
|
time.Sleep(5 * time.Second)
|
|
|
|
fmt.Printf("Restoring..\n")
|
|
start = time.Now()
|
|
fcfg.AsyncFlush = false
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fmt.Printf("time to restore is %v\n\n", time.Since(start))
|
|
|
|
fmt.Printf("LOAD: reading %d msgs of %s each, totalling %s\n",
|
|
toStore,
|
|
friendlyBytes(int64(storedMsgSize)),
|
|
friendlyBytes(int64(toStore*storedMsgSize)),
|
|
)
|
|
|
|
var smv StoreMsg
|
|
start = time.Now()
|
|
for i := uint64(1); i <= toStore; i++ {
|
|
if _, err := fs.LoadMsg(i, &smv); err != nil {
|
|
t.Fatalf("Error loading %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
tt = time.Since(start)
|
|
fmt.Printf("time to read all back messages is %v\n", tt)
|
|
fmt.Printf("%.0f msgs/sec\n", float64(toStore)/tt.Seconds())
|
|
fmt.Printf("%s per sec\n", friendlyBytes(int64(float64(toStore*storedMsgSize)/tt.Seconds())))
|
|
|
|
// Do again to test skip for hash..
|
|
fmt.Printf("\nSKIP CHECKSUM: reading %d msgs of %s each, totalling %s\n",
|
|
toStore,
|
|
friendlyBytes(int64(storedMsgSize)),
|
|
friendlyBytes(int64(toStore*storedMsgSize)),
|
|
)
|
|
|
|
start = time.Now()
|
|
for i := uint64(1); i <= toStore; i++ {
|
|
if _, err := fs.LoadMsg(i, &smv); err != nil {
|
|
t.Fatalf("Error loading %d: %v", i, err)
|
|
}
|
|
}
|
|
|
|
tt = time.Since(start)
|
|
fmt.Printf("time to read all back messages is %v\n", tt)
|
|
fmt.Printf("%.0f msgs/sec\n", float64(toStore)/tt.Seconds())
|
|
fmt.Printf("%s per sec\n", friendlyBytes(int64(float64(toStore*storedMsgSize)/tt.Seconds())))
|
|
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fmt.Printf("\nremoving [in order] %d msgs of %s each, totalling %s\n",
|
|
toStore,
|
|
friendlyBytes(int64(storedMsgSize)),
|
|
friendlyBytes(int64(toStore*storedMsgSize)),
|
|
)
|
|
|
|
start = time.Now()
|
|
// For reverse order.
|
|
//for i := toStore; i > 0; i-- {
|
|
for i := uint64(1); i <= toStore; i++ {
|
|
fs.RemoveMsg(i)
|
|
}
|
|
fs.Stop()
|
|
|
|
tt = time.Since(start)
|
|
fmt.Printf("time to remove all messages is %v\n", tt)
|
|
fmt.Printf("%.0f msgs/sec\n", float64(toStore)/tt.Seconds())
|
|
fmt.Printf("%s per sec\n", friendlyBytes(int64(float64(toStore*storedMsgSize)/tt.Seconds())))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state := fs.State()
|
|
if state.Msgs != 0 {
|
|
t.Fatalf("Expected no msgs, got %d", state.Msgs)
|
|
}
|
|
if state.Bytes != 0 {
|
|
t.Fatalf("Expected no bytes, got %d", state.Bytes)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreReadBackMsgPerf(t *testing.T) {
|
|
// Comment out to run, holding place for now.
|
|
t.SkipNow()
|
|
|
|
subj := "foo"
|
|
msg := []byte("ABCDEFGH") // Smaller shows problems more.
|
|
|
|
storedMsgSize := fileStoreMsgSize(subj, nil, msg)
|
|
|
|
// Make sure we store 2 blocks.
|
|
toStore := defaultLargeBlockSize * 2 / storedMsgSize
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fmt.Printf("storing %d msgs of %s each, totalling %s\n",
|
|
toStore,
|
|
friendlyBytes(int64(storedMsgSize)),
|
|
friendlyBytes(int64(toStore*storedMsgSize)),
|
|
)
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
start := time.Now()
|
|
for i := 0; i < int(toStore); i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
tt := time.Since(start)
|
|
fmt.Printf("time to store is %v\n", tt)
|
|
fmt.Printf("%.0f msgs/sec\n", float64(toStore)/tt.Seconds())
|
|
fmt.Printf("%s per sec\n", friendlyBytes(int64(float64(toStore*storedMsgSize)/tt.Seconds())))
|
|
|
|
// We should not have cached here with no reads.
|
|
// Pick something towards end of the block.
|
|
index := defaultLargeBlockSize/storedMsgSize - 22
|
|
start = time.Now()
|
|
fs.LoadMsg(index, nil)
|
|
fmt.Printf("Time to load first msg [%d] = %v\n", index, time.Since(start))
|
|
|
|
start = time.Now()
|
|
fs.LoadMsg(index+2, nil)
|
|
fmt.Printf("Time to load second msg [%d] = %v\n", index+2, time.Since(start))
|
|
})
|
|
}
|
|
|
|
// This test is testing an upper level stream with a message or byte limit.
|
|
// Even though this is 1, any limit would trigger same behavior once the limit was reached
|
|
// and we were adding and removing.
|
|
func TestFileStoreStoreLimitRemovePerf(t *testing.T) {
|
|
// Comment out to run, holding place for now.
|
|
t.SkipNow()
|
|
|
|
subj, msg := "foo", make([]byte, 1024-33)
|
|
for i := 0; i < len(msg); i++ {
|
|
msg[i] = 'D'
|
|
}
|
|
storedMsgSize := fileStoreMsgSize(subj, nil, msg)
|
|
|
|
// 1GB
|
|
toStore := 1 * 1024 * 1024 * 1024 / storedMsgSize
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.RegisterStorageUpdates(func(md, bd int64, seq uint64, subj string) {})
|
|
|
|
fmt.Printf("storing and removing (limit 1) %d msgs of %s each, totalling %s\n",
|
|
toStore,
|
|
friendlyBytes(int64(storedMsgSize)),
|
|
friendlyBytes(int64(toStore*storedMsgSize)),
|
|
)
|
|
|
|
start := time.Now()
|
|
for i := 0; i < int(toStore); i++ {
|
|
seq, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error storing message: %v", err)
|
|
}
|
|
if i > 0 {
|
|
fs.RemoveMsg(seq - 1)
|
|
}
|
|
}
|
|
fs.Stop()
|
|
|
|
tt := time.Since(start)
|
|
fmt.Printf("time to store and remove all messages is %v\n", tt)
|
|
fmt.Printf("%.0f msgs/sec\n", float64(toStore)/tt.Seconds())
|
|
fmt.Printf("%s per sec\n", friendlyBytes(int64(float64(toStore*storedMsgSize)/tt.Seconds())))
|
|
})
|
|
}
|
|
|
|
func TestFileStorePubPerfWithSmallBlkSize(t *testing.T) {
|
|
// Comment out to run, holding place for now.
|
|
t.SkipNow()
|
|
|
|
subj, msg := "foo", make([]byte, 1024-33)
|
|
for i := 0; i < len(msg); i++ {
|
|
msg[i] = 'D'
|
|
}
|
|
storedMsgSize := fileStoreMsgSize(subj, nil, msg)
|
|
|
|
// 1GB
|
|
toStore := 1 * 1024 * 1024 * 1024 / storedMsgSize
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fmt.Printf("storing %d msgs of %s each, totalling %s\n",
|
|
toStore,
|
|
friendlyBytes(int64(storedMsgSize)),
|
|
friendlyBytes(int64(toStore*storedMsgSize)),
|
|
)
|
|
|
|
fcfg.BlockSize = FileStoreMinBlkSize
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
start := time.Now()
|
|
for i := 0; i < int(toStore); i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
fs.Stop()
|
|
|
|
tt := time.Since(start)
|
|
fmt.Printf("time to store is %v\n", tt)
|
|
fmt.Printf("%.0f msgs/sec\n", float64(toStore)/tt.Seconds())
|
|
fmt.Printf("%s per sec\n", friendlyBytes(int64(float64(toStore*storedMsgSize)/tt.Seconds())))
|
|
})
|
|
}
|
|
|
|
// Saw this manifest from a restart test with max delivered set for JetStream.
|
|
func TestFileStoreConsumerRedeliveredLost(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
cfg := &ConsumerConfig{AckPolicy: AckExplicit}
|
|
o, err := fs.ConsumerStore("o22", time.Time{}, cfg)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
restartConsumer := func() {
|
|
t.Helper()
|
|
o.Stop()
|
|
time.Sleep(20 * time.Millisecond) // Wait for all things to settle.
|
|
o, err = fs.ConsumerStore("o22", time.Time{}, cfg)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
// Make sure we recovered Redelivered.
|
|
state, err := o.State()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if state == nil {
|
|
t.Fatalf("Did not recover state")
|
|
}
|
|
if len(state.Redelivered) == 0 {
|
|
t.Fatalf("Did not recover redelivered")
|
|
}
|
|
}
|
|
|
|
ts := time.Now().UnixNano()
|
|
o.UpdateDelivered(1, 1, 1, ts)
|
|
o.UpdateDelivered(2, 1, 2, ts)
|
|
o.UpdateDelivered(3, 1, 3, ts)
|
|
o.UpdateDelivered(4, 1, 4, ts)
|
|
o.UpdateDelivered(5, 2, 1, ts)
|
|
|
|
restartConsumer()
|
|
|
|
o.UpdateDelivered(6, 2, 2, ts)
|
|
o.UpdateDelivered(7, 3, 1, ts)
|
|
|
|
restartConsumer()
|
|
if state, _ := o.State(); len(state.Pending) != 3 {
|
|
t.Fatalf("Did not recover pending correctly")
|
|
}
|
|
|
|
o.UpdateAcks(7, 3)
|
|
o.UpdateAcks(6, 2)
|
|
|
|
restartConsumer()
|
|
o.UpdateAcks(4, 1)
|
|
|
|
state, _ := o.State()
|
|
if len(state.Pending) != 0 {
|
|
t.Fatalf("Did not clear pending correctly")
|
|
}
|
|
if len(state.Redelivered) != 0 {
|
|
t.Fatalf("Did not clear redelivered correctly")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreConsumerUpdateAcksFlushesRedelivered(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// MaxDeliver: 1 means that on the second delivery the message is dropped from
|
|
// pending while still being tracked in Redelivered.
|
|
cfg := &ConsumerConfig{AckPolicy: AckExplicit, MaxDeliver: 1}
|
|
o, err := fs.ConsumerStore("o22", time.Time{}, cfg)
|
|
require_NoError(t, err)
|
|
|
|
restartConsumer := func() {
|
|
t.Helper()
|
|
require_NoError(t, o.Stop())
|
|
time.Sleep(200 * time.Millisecond) // Wait for all things to settle.
|
|
o, err = fs.ConsumerStore("o22", time.Time{}, cfg)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
ts := time.Now().UnixNano()
|
|
require_NoError(t, o.UpdateDelivered(1, 1, 1, ts))
|
|
// Redelivery exceeds MaxDeliver, so sseq 1 is removed from pending but kept
|
|
// in Redelivered.
|
|
require_NoError(t, o.UpdateDelivered(2, 1, 2, ts))
|
|
|
|
// Persist and recover so we know the Redelivered entry is on disk.
|
|
restartConsumer()
|
|
state, err := o.State()
|
|
require_NoError(t, err)
|
|
require_Equal(t, len(state.Pending), 0)
|
|
require_Equal(t, len(state.Redelivered), 1)
|
|
|
|
// Acking sseq 1 deletes it from Redelivered, but UpdateAcks bails out with
|
|
// ErrStoreMsgNotFound since it is no longer pending. The deletion must still
|
|
// be flushed.
|
|
require_Error(t, o.UpdateAcks(2, 1), ErrStoreMsgNotFound)
|
|
|
|
restartConsumer()
|
|
defer o.Stop()
|
|
state, err = o.State()
|
|
require_NoError(t, err)
|
|
require_Equal(t, len(state.Redelivered), 0)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreConsumerFlusher(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
o, err := fs.ConsumerStore("o22", time.Time{}, &ConsumerConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
// Get the underlying impl.
|
|
oc := o.(*consumerFileStore)
|
|
// Wait for flusher to be running.
|
|
checkFor(t, time.Second, 20*time.Millisecond, func() error {
|
|
if !oc.inFlusher() {
|
|
return fmt.Errorf("Flusher not running")
|
|
}
|
|
return nil
|
|
})
|
|
// Stop and make sure the flusher goes away
|
|
o.Stop()
|
|
// Wait for flusher to stop.
|
|
checkFor(t, time.Second, 20*time.Millisecond, func() error {
|
|
if oc.inFlusher() {
|
|
return fmt.Errorf("Flusher still running")
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestFileStoreConsumerDeliveredUpdates(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Simple consumer, no ack policy configured.
|
|
o, err := fs.ConsumerStore("o22", time.Time{}, &ConsumerConfig{})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
defer o.Stop()
|
|
|
|
testDelivered := func(dseq, sseq uint64) {
|
|
t.Helper()
|
|
ts := time.Now().UnixNano()
|
|
if err := o.UpdateDelivered(dseq, sseq, 1, ts); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
state, err := o.State()
|
|
if err != nil {
|
|
t.Fatalf("Error getting state: %v", err)
|
|
}
|
|
if state == nil {
|
|
t.Fatalf("No state available")
|
|
}
|
|
expected := SequencePair{dseq, sseq}
|
|
if state.Delivered != expected {
|
|
t.Fatalf("Unexpected state, wanted %+v, got %+v", expected, state.Delivered)
|
|
}
|
|
if state.AckFloor != expected {
|
|
t.Fatalf("Unexpected ack floor state, wanted %+v, got %+v", expected, state.AckFloor)
|
|
}
|
|
if len(state.Pending) != 0 {
|
|
t.Fatalf("Did not expect any pending, got %d pending", len(state.Pending))
|
|
}
|
|
}
|
|
|
|
testDelivered(1, 100)
|
|
testDelivered(2, 110)
|
|
testDelivered(5, 130)
|
|
|
|
// If we try to do an ack this should err since we are not configured with ack policy.
|
|
if err := o.UpdateAcks(1, 100); err != ErrNoAckPolicy {
|
|
t.Fatalf("Expected a no ack policy error on update acks, got %v", err)
|
|
}
|
|
// Also if we do an update with a delivery count of anything but 1 here should also give same error.
|
|
ts := time.Now().UnixNano()
|
|
if err := o.UpdateDelivered(5, 130, 2, ts); err != ErrNoAckPolicy {
|
|
t.Fatalf("Expected a no ack policy error on update delivered with dc > 1, got %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreConsumerDeliveredAndAckUpdates(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Simple consumer, no ack policy configured.
|
|
o, err := fs.ConsumerStore("o22", time.Time{}, &ConsumerConfig{AckPolicy: AckExplicit})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
defer o.Stop()
|
|
|
|
// Track pending.
|
|
var pending int
|
|
|
|
testDelivered := func(dseq, sseq uint64) {
|
|
t.Helper()
|
|
ts := time.Now().Round(time.Second).UnixNano()
|
|
if err := o.UpdateDelivered(dseq, sseq, 1, ts); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
pending++
|
|
state, err := o.State()
|
|
if err != nil {
|
|
t.Fatalf("Error getting state: %v", err)
|
|
}
|
|
if state == nil {
|
|
t.Fatalf("No state available")
|
|
}
|
|
expected := SequencePair{dseq, sseq}
|
|
if state.Delivered != expected {
|
|
t.Fatalf("Unexpected delivered state, wanted %+v, got %+v", expected, state.Delivered)
|
|
}
|
|
if len(state.Pending) != pending {
|
|
t.Fatalf("Expected %d pending, got %d pending", pending, len(state.Pending))
|
|
}
|
|
}
|
|
|
|
testDelivered(1, 100)
|
|
testDelivered(2, 110)
|
|
testDelivered(3, 130)
|
|
testDelivered(4, 150)
|
|
testDelivered(5, 165)
|
|
|
|
testBadAck := func(dseq, sseq uint64) {
|
|
t.Helper()
|
|
if err := o.UpdateAcks(dseq, sseq); err == nil {
|
|
t.Fatalf("Expected error but got none")
|
|
}
|
|
}
|
|
testBadAck(3, 101)
|
|
testBadAck(1, 1)
|
|
|
|
testAck := func(dseq, sseq, dflr, sflr uint64) {
|
|
t.Helper()
|
|
if err := o.UpdateAcks(dseq, sseq); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
pending--
|
|
state, err := o.State()
|
|
if err != nil {
|
|
t.Fatalf("Error getting state: %v", err)
|
|
}
|
|
if state == nil {
|
|
t.Fatalf("No state available")
|
|
}
|
|
if len(state.Pending) != pending {
|
|
t.Fatalf("Expected %d pending, got %d pending", pending, len(state.Pending))
|
|
}
|
|
eflr := SequencePair{dflr, sflr}
|
|
if state.AckFloor != eflr {
|
|
t.Fatalf("Unexpected ack floor state, wanted %+v, got %+v", eflr, state.AckFloor)
|
|
}
|
|
}
|
|
|
|
testAck(1, 100, 1, 109)
|
|
testAck(3, 130, 1, 109)
|
|
testAck(2, 110, 3, 149) // We do not track explicit state on previous stream floors, so we take last known -1
|
|
testAck(5, 165, 3, 149)
|
|
testAck(4, 150, 5, 165)
|
|
|
|
testDelivered(6, 170)
|
|
testDelivered(7, 171)
|
|
testDelivered(8, 172)
|
|
testDelivered(9, 173)
|
|
testDelivered(10, 200)
|
|
|
|
testAck(7, 171, 5, 165)
|
|
testAck(8, 172, 5, 165)
|
|
|
|
state, err := o.State()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error getting state: %v", err)
|
|
}
|
|
o.Stop()
|
|
|
|
o, err = fs.ConsumerStore("o22", time.Time{}, &ConsumerConfig{AckPolicy: AckExplicit})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
defer o.Stop()
|
|
|
|
nstate, err := o.State()
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error getting state: %v", err)
|
|
}
|
|
if !reflect.DeepEqual(nstate, state) {
|
|
t.Fatalf("States don't match! NEW %+v OLD %+v", nstate, state)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreStreamStateDeleted(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, toStore := "foo", uint64(10)
|
|
for i := uint64(1); i <= toStore; i++ {
|
|
msg := []byte(fmt.Sprintf("[%08d] Hello World!", i))
|
|
if _, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
t.Fatalf("Error storing msg: %v", err)
|
|
}
|
|
}
|
|
state := fs.State()
|
|
if len(state.Deleted) != 0 {
|
|
t.Fatalf("Expected deleted to be empty")
|
|
}
|
|
// Now remove some interior messages.
|
|
var expected []uint64
|
|
for seq := uint64(2); seq < toStore; seq += 2 {
|
|
fs.RemoveMsg(seq)
|
|
expected = append(expected, seq)
|
|
}
|
|
state = fs.State()
|
|
if !reflect.DeepEqual(state.Deleted, expected) {
|
|
t.Fatalf("Expected deleted to be %+v, got %+v\n", expected, state.Deleted)
|
|
}
|
|
// Now fill the gap by deleting 1 and 3
|
|
fs.RemoveMsg(1)
|
|
fs.RemoveMsg(3)
|
|
expected = expected[2:]
|
|
state = fs.State()
|
|
if !reflect.DeepEqual(state.Deleted, expected) {
|
|
t.Fatalf("Expected deleted to be %+v, got %+v\n", expected, state.Deleted)
|
|
}
|
|
if state.FirstSeq != 5 {
|
|
t.Fatalf("Expected first seq to be 5, got %d", state.FirstSeq)
|
|
}
|
|
fs.Purge()
|
|
if state = fs.State(); len(state.Deleted) != 0 {
|
|
t.Fatalf("Expected no deleted after purge, got %+v\n", state.Deleted)
|
|
}
|
|
})
|
|
}
|
|
|
|
// We have reports that sometimes under load a stream could complain about a storage directory
|
|
// not being empty.
|
|
func TestFileStoreStreamDeleteDirNotEmpty(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, toStore := "foo", uint64(10)
|
|
for i := uint64(1); i <= toStore; i++ {
|
|
msg := []byte(fmt.Sprintf("[%08d] Hello World!", i))
|
|
if _, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
t.Fatalf("Error storing msg: %v", err)
|
|
}
|
|
}
|
|
|
|
ready := make(chan bool)
|
|
go func() {
|
|
g := filepath.Join(fcfg.StoreDir, "g")
|
|
ready <- true
|
|
for i := 0; i < 100; i++ {
|
|
os.WriteFile(g, []byte("OK"), defaultFilePerms)
|
|
}
|
|
}()
|
|
|
|
<-ready
|
|
if err := fs.Delete(true); err != nil {
|
|
t.Fatalf("Delete returned an error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreConsumerPerf(t *testing.T) {
|
|
// Comment out to run, holding place for now.
|
|
t.SkipNow()
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
o, err := fs.ConsumerStore("o22", time.Time{}, &ConsumerConfig{AckPolicy: AckExplicit})
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
// Get the underlying impl.
|
|
oc := o.(*consumerFileStore)
|
|
// Wait for flusher to br running
|
|
checkFor(t, time.Second, 20*time.Millisecond, func() error {
|
|
if !oc.inFlusher() {
|
|
return fmt.Errorf("not in flusher")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Stop flusher for this benchmark since we will invoke directly.
|
|
oc.mu.Lock()
|
|
qch := oc.qch
|
|
oc.qch = nil
|
|
oc.mu.Unlock()
|
|
close(qch)
|
|
|
|
checkFor(t, time.Second, 20*time.Millisecond, func() error {
|
|
if oc.inFlusher() {
|
|
return fmt.Errorf("still in flusher")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
toStore := uint64(1_000_000)
|
|
|
|
start := time.Now()
|
|
|
|
ts := start.UnixNano()
|
|
|
|
for i := uint64(1); i <= toStore; i++ {
|
|
if err := o.UpdateDelivered(i, i, 1, ts); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
tt := time.Since(start)
|
|
fmt.Printf("time to update %d is %v\n", toStore, tt)
|
|
fmt.Printf("%.0f updates/sec\n", float64(toStore)/tt.Seconds())
|
|
|
|
start = time.Now()
|
|
oc.mu.Lock()
|
|
buf, err := oc.encodeState()
|
|
oc.mu.Unlock()
|
|
if err != nil {
|
|
t.Fatalf("Error encoding state: %v", err)
|
|
}
|
|
fmt.Printf("time to encode %d bytes is %v\n", len(buf), time.Since(start))
|
|
start = time.Now()
|
|
oc.writeState(buf)
|
|
fmt.Printf("time to write is %v\n", time.Since(start))
|
|
})
|
|
}
|
|
|
|
// Reported by Ivan.
|
|
func TestFileStoreStreamDeleteCacheBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.CacheExpire = 50 * time.Millisecond
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
|
|
if _, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if _, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if _, err := fs.EraseMsg(1); err != nil {
|
|
t.Fatalf("Got an error on remove of %d: %v", 1, err)
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
if _, err := fs.LoadMsg(2, nil); err != nil {
|
|
t.Fatalf("Unexpected error looking up msg: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// rip
|
|
func TestFileStoreStreamFailToRollBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 512
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage, MaxBytes: 300}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Make sure we properly roll underlying blocks.
|
|
n, msg := 200, bytes.Repeat([]byte("ABC"), 33) // ~100bytes
|
|
for i := 0; i < n; i++ {
|
|
if _, _, err := fs.StoreMsg("zzz", nil, msg, 0); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// Grab some info for introspection.
|
|
fs.mu.RLock()
|
|
numBlks := len(fs.blks)
|
|
var index uint32
|
|
var blkSize int64
|
|
if numBlks > 0 {
|
|
mb := fs.blks[0]
|
|
mb.mu.RLock()
|
|
index = mb.index
|
|
if fi, _ := os.Stat(mb.mfn); fi != nil {
|
|
blkSize = fi.Size()
|
|
}
|
|
mb.mu.RUnlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
|
|
if numBlks != 1 {
|
|
t.Fatalf("Expected only one block, got %d", numBlks)
|
|
}
|
|
if index < 60 {
|
|
t.Fatalf("Expected a block index > 60, got %d", index)
|
|
}
|
|
if blkSize > 512 {
|
|
t.Fatalf("Expected block to be <= 512, got %d", blkSize)
|
|
}
|
|
})
|
|
}
|
|
|
|
// We had a case where a consumer state had a redelivered record that had seq of 0.
|
|
// This was causing the server to panic.
|
|
func TestFileStoreBadConsumerState(t *testing.T) {
|
|
bs := []byte("\x16\x02\x01\x01\x03\x02\x01\x98\xf4\x8a\x8a\f\x01\x03\x86\xfa\n\x01\x00\x01")
|
|
if cs, err := decodeConsumerState(bs); err != nil || cs == nil {
|
|
t.Fatalf("Expected to not throw error, got %v and %+v", err, cs)
|
|
}
|
|
}
|
|
|
|
func TestFileStoreExpireMsgsOnStart(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
if fcfg.Compression != NoCompression || fcfg.Cipher != NoCipher {
|
|
// TODO(nat): For some reason this test is very flaky in CI when using
|
|
// encryption or compression but passes fine without.
|
|
t.SkipNow()
|
|
}
|
|
|
|
fcfg.BlockSize = 8 * 1024
|
|
ttl := 250 * time.Millisecond
|
|
cfg := StreamConfig{Name: "ORDERS", Subjects: []string{"orders.*"}, Storage: FileStorage, MaxAge: ttl}
|
|
var fs *fileStore
|
|
|
|
startFS := func() *fileStore {
|
|
t.Helper()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
return fs
|
|
}
|
|
|
|
newFS := func() *fileStore {
|
|
t.Helper()
|
|
if fs != nil {
|
|
fs.Stop()
|
|
fs = nil
|
|
}
|
|
removeDir(t, fcfg.StoreDir)
|
|
return startFS()
|
|
}
|
|
|
|
restartFS := func(delay time.Duration) *fileStore {
|
|
if fs != nil {
|
|
fs.Stop()
|
|
fs = nil
|
|
time.Sleep(delay)
|
|
}
|
|
fs = startFS()
|
|
return fs
|
|
}
|
|
|
|
fs = newFS()
|
|
defer fs.Stop()
|
|
|
|
msg := bytes.Repeat([]byte("ABC"), 33) // ~100bytes
|
|
loadMsgs := func(n int) {
|
|
t.Helper()
|
|
for i := 1; i <= n; i++ {
|
|
if _, _, err := fs.StoreMsg(fmt.Sprintf("orders.%d", i%10), nil, msg, 0); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
checkState := func(msgs, first, last uint64) {
|
|
t.Helper()
|
|
if fs == nil {
|
|
t.Fatalf("No fs")
|
|
return
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != msgs {
|
|
t.Fatalf("Expected %d msgs, got %d", msgs, state.Msgs)
|
|
}
|
|
if state.FirstSeq != first {
|
|
t.Fatalf("Expected %d as first, got %d", first, state.FirstSeq)
|
|
}
|
|
if state.LastSeq != last {
|
|
t.Fatalf("Expected %d as last, got %d", last, state.LastSeq)
|
|
}
|
|
}
|
|
|
|
checkNumBlks := func(expected int) {
|
|
t.Helper()
|
|
fs.mu.RLock()
|
|
n := len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
if n != expected {
|
|
t.Fatalf("Expected %d msg blks, got %d", expected, n)
|
|
}
|
|
}
|
|
|
|
// Check the filtered subject state and make sure that is tracked properly.
|
|
checkFiltered := func(subject string, ss SimpleState) {
|
|
t.Helper()
|
|
fss, err := fs.FilteredState(1, subject)
|
|
require_NoError(t, err)
|
|
if fss != ss {
|
|
t.Fatalf("Expected FilteredState of %+v, got %+v", ss, fss)
|
|
}
|
|
}
|
|
|
|
// Make sure state on disk matches (e.g. writeIndexInfo properly called)
|
|
checkBlkState := func(index int) {
|
|
t.Helper()
|
|
fs.mu.RLock()
|
|
if index >= len(fs.blks) {
|
|
t.Fatalf("Out of range, wanted %d but only %d blks", index, len(fs.blks))
|
|
}
|
|
fs.mu.RUnlock()
|
|
}
|
|
|
|
lastSeqForBlk := func() uint64 {
|
|
t.Helper()
|
|
fs.mu.RLock()
|
|
defer fs.mu.RUnlock()
|
|
if len(fs.blks) == 0 {
|
|
t.Fatalf("No blocks?")
|
|
}
|
|
mb := fs.blks[0]
|
|
mb.mu.RLock()
|
|
defer mb.mu.RUnlock()
|
|
return mb.last.seq
|
|
}
|
|
|
|
// Actual testing here.
|
|
|
|
loadMsgs(500)
|
|
restartFS(ttl + 100*time.Millisecond)
|
|
checkState(0, 501, 500)
|
|
// We actually hold onto the last one now to remember our starting sequence.
|
|
checkNumBlks(1)
|
|
|
|
// Now check partial expires and the fss tracking state.
|
|
// Small numbers is to keep them in one block.
|
|
fs = newFS()
|
|
loadMsgs(10)
|
|
time.Sleep(100 * time.Millisecond)
|
|
loadMsgs(10)
|
|
checkFiltered("orders.*", SimpleState{Msgs: 20, First: 1, Last: 20})
|
|
|
|
restartFS(ttl - 100*time.Millisecond + 25*time.Millisecond) // Just want half
|
|
checkState(10, 11, 20)
|
|
checkNumBlks(1)
|
|
checkFiltered("orders.*", SimpleState{Msgs: 10, First: 11, Last: 20})
|
|
checkFiltered("orders.5", SimpleState{Msgs: 1, First: 15, Last: 15})
|
|
checkBlkState(0)
|
|
|
|
fs = newFS()
|
|
loadMsgs(5)
|
|
time.Sleep(100 * time.Millisecond)
|
|
loadMsgs(15)
|
|
restartFS(ttl - 100*time.Millisecond + 25*time.Millisecond) // Just want half
|
|
checkState(15, 6, 20)
|
|
checkFiltered("orders.*", SimpleState{Msgs: 15, First: 6, Last: 20})
|
|
checkFiltered("orders.5", SimpleState{Msgs: 2, First: 10, Last: 20})
|
|
|
|
// Now we want to test that if the end of a msg block is all deletes msgs that we do the right thing.
|
|
fs = newFS()
|
|
loadMsgs(150)
|
|
time.Sleep(100 * time.Millisecond)
|
|
loadMsgs(100)
|
|
|
|
checkNumBlks(5)
|
|
|
|
// Now delete 10 messages from the end of the first block which we will expire on restart.
|
|
// We will expire up to seq 100, so delete 91-100.
|
|
lseq := lastSeqForBlk()
|
|
for seq := lseq; seq > lseq-10; seq-- {
|
|
removed, err := fs.RemoveMsg(seq)
|
|
if err != nil || !removed {
|
|
t.Fatalf("Error removing message: %v", err)
|
|
}
|
|
}
|
|
restartFS(ttl - 100*time.Millisecond + 25*time.Millisecond) // Just want half
|
|
checkState(100, 151, 250)
|
|
checkNumBlks(3) // We should only have 3 blks left.
|
|
checkBlkState(0)
|
|
|
|
// Now make sure that we properly clean up any internal dmap entries (sparse) when expiring.
|
|
fs = newFS()
|
|
loadMsgs(10)
|
|
// Remove some in sparse fashion, adding to dmap.
|
|
fs.RemoveMsg(2)
|
|
fs.RemoveMsg(4)
|
|
fs.RemoveMsg(6)
|
|
time.Sleep(100 * time.Millisecond)
|
|
loadMsgs(10)
|
|
restartFS(ttl - 100*time.Millisecond + 25*time.Millisecond) // Just want half
|
|
checkState(10, 11, 20)
|
|
checkNumBlks(1)
|
|
checkBlkState(0)
|
|
|
|
// Make sure expiring a block with tail deleted messages removes the message block etc.
|
|
fs = newFS()
|
|
loadMsgs(7)
|
|
time.Sleep(100 * time.Millisecond)
|
|
loadMsgs(3)
|
|
fs.RemoveMsg(8)
|
|
fs.RemoveMsg(9)
|
|
fs.RemoveMsg(10)
|
|
restartFS(ttl - 100*time.Millisecond + 25*time.Millisecond)
|
|
checkState(0, 11, 10)
|
|
|
|
fs.Stop()
|
|
// Not for start per se but since we have all the test tooling here check that Compact() does right thing as well.
|
|
fs = newFS()
|
|
defer fs.Stop()
|
|
loadMsgs(100)
|
|
checkFiltered("orders.*", SimpleState{Msgs: 100, First: 1, Last: 100})
|
|
checkFiltered("orders.5", SimpleState{Msgs: 10, First: 5, Last: 95})
|
|
// Check that Compact keeps fss updated, does dmap etc.
|
|
fs.Compact(51)
|
|
checkFiltered("orders.*", SimpleState{Msgs: 50, First: 51, Last: 100})
|
|
checkFiltered("orders.5", SimpleState{Msgs: 5, First: 55, Last: 95})
|
|
checkBlkState(0)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreSparseCompaction(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 1024 * 1024
|
|
cfg := StreamConfig{Name: "KV", Subjects: []string{"kv.>"}, Storage: FileStorage}
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := bytes.Repeat([]byte("ABC"), 33) // ~100bytes
|
|
loadMsgs := func(n int) {
|
|
t.Helper()
|
|
for i := 1; i <= n; i++ {
|
|
if _, _, err := fs.StoreMsg(fmt.Sprintf("kv.%d", i%10), nil, msg, 0); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
checkState := func(msgs, first, last uint64) {
|
|
t.Helper()
|
|
if fs == nil {
|
|
t.Fatalf("No fs")
|
|
return
|
|
}
|
|
state := fs.State()
|
|
if state.Msgs != msgs {
|
|
t.Fatalf("Expected %d msgs, got %d", msgs, state.Msgs)
|
|
}
|
|
if state.FirstSeq != first {
|
|
t.Fatalf("Expected %d as first, got %d", first, state.FirstSeq)
|
|
}
|
|
if state.LastSeq != last {
|
|
t.Fatalf("Expected %d as last, got %d", last, state.LastSeq)
|
|
}
|
|
}
|
|
|
|
deleteMsgs := func(seqs ...uint64) {
|
|
t.Helper()
|
|
for _, seq := range seqs {
|
|
removed, err := fs.RemoveMsg(seq)
|
|
if err != nil || !removed {
|
|
t.Fatalf("Got an error on remove of %d: %v", seq, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
eraseMsgs := func(seqs ...uint64) {
|
|
t.Helper()
|
|
for _, seq := range seqs {
|
|
removed, err := fs.EraseMsg(seq)
|
|
if err != nil || !removed {
|
|
t.Fatalf("Got an error on erase of %d: %v", seq, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
compact := func() {
|
|
t.Helper()
|
|
var ssb, ssa StreamState
|
|
fs.FastState(&ssb)
|
|
tb, ub, _ := fs.Utilization()
|
|
|
|
fs.mu.RLock()
|
|
if len(fs.blks) < 2 {
|
|
t.Fatalf("Not enough blocks?")
|
|
}
|
|
mb := fs.blks[0]
|
|
fs.mu.RUnlock()
|
|
|
|
mb.mu.Lock()
|
|
err = mb.compact()
|
|
mb.mu.Unlock()
|
|
require_NoError(t, err)
|
|
|
|
fs.FastState(&ssa)
|
|
if !reflect.DeepEqual(ssb, ssa) {
|
|
t.Fatalf("States do not match\n; %+v \nvs %+v", ssb, ssa)
|
|
}
|
|
ta, ua, _ := fs.Utilization()
|
|
if ub != ua {
|
|
t.Fatalf("Expected used to be the same, got %d vs %d", ub, ua)
|
|
}
|
|
// When using both encryption and compression, we're not always
|
|
// guaranteed to have a smaller file after compaction.
|
|
if ta >= tb && (fcfg.Cipher == NoCipher || fcfg.Compression == NoCompression) {
|
|
t.Fatalf("Expected total after to be less then before, got %d vs %d", tb, ta)
|
|
}
|
|
}
|
|
|
|
// Actual testing here.
|
|
loadMsgs(1000)
|
|
checkState(1000, 1, 1000)
|
|
|
|
// Create a new lmb, since we'll compact the current one and that's not allowed on lmb.
|
|
fs.mu.RLock()
|
|
blks := len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Len(t, blks, 1)
|
|
state := fs.State()
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
require_NoError(t, fs.writeTombstone(state.LastSeq, state.LastTime.UnixNano()))
|
|
|
|
// Now delete a few messages.
|
|
deleteMsgs(1)
|
|
compact()
|
|
|
|
deleteMsgs(1000, 999, 998, 997)
|
|
compact()
|
|
|
|
eraseMsgs(500, 502, 504, 506, 508, 510)
|
|
compact()
|
|
})
|
|
}
|
|
|
|
func TestFileStoreSparseCompactionWithInteriorDeletes(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "KV", Subjects: []string{"kv.>"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 1; i <= 1000; i++ {
|
|
if _, _, err := fs.StoreMsg(fmt.Sprintf("kv.%d", i%10), nil, []byte("OK"), 0); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// Now do interior deletes.
|
|
for _, seq := range []uint64{500, 600, 700, 800} {
|
|
removed, err := fs.RemoveMsg(seq)
|
|
if err != nil || !removed {
|
|
t.Fatalf("Got an error on remove of %d: %v", seq, err)
|
|
}
|
|
}
|
|
|
|
_, err = fs.LoadMsg(900, nil)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
|
|
// Do compact by hand, make sure we can still access msgs past the interior deletes.
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
if err = lmb.dirtyCloseWithRemove(false); err != nil {
|
|
fs.mu.RUnlock()
|
|
require_NoError(t, err)
|
|
}
|
|
if err = lmb.compact(); err != nil {
|
|
fs.mu.RUnlock()
|
|
require_NoError(t, err)
|
|
}
|
|
fs.mu.RUnlock()
|
|
|
|
if _, err = fs.LoadMsg(900, nil); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// When messages span multiple blocks and we want to purge but keep some amount, say 1, we would remove all.
|
|
// This is because we would not break out of iterator across more message blocks.
|
|
// Issue #2622
|
|
func TestFileStorePurgeExKeepOneBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 128
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fill := bytes.Repeat([]byte("X"), 128)
|
|
|
|
fs.StoreMsg("A", nil, []byte("META"), 0)
|
|
fs.StoreMsg("B", nil, fill, 0)
|
|
fs.StoreMsg("A", nil, []byte("META"), 0)
|
|
fs.StoreMsg("B", nil, fill, 0)
|
|
|
|
fss, err := fs.FilteredState(1, "A")
|
|
require_NoError(t, err)
|
|
if fss.Msgs != 2 {
|
|
t.Fatalf("Expected to find 2 `A` msgs, got %d", fss.Msgs)
|
|
}
|
|
|
|
n, err := fs.PurgeEx("A", 0, 1)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
if n != 1 {
|
|
t.Fatalf("Expected PurgeEx to remove 1 `A` msgs, got %d", n)
|
|
}
|
|
fss, err = fs.FilteredState(1, "A")
|
|
require_NoError(t, err)
|
|
if fss.Msgs != 1 {
|
|
t.Fatalf("Expected to find 1 `A` msgs, got %d", fss.Msgs)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreFilteredPendingBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("foo", nil, []byte("msg"), 0)
|
|
require_NoError(t, err)
|
|
_, _, err = fs.StoreMsg("bar", nil, []byte("msg"), 0)
|
|
require_NoError(t, err)
|
|
_, _, err = fs.StoreMsg("baz", nil, []byte("msg"), 0)
|
|
require_NoError(t, err)
|
|
|
|
fs.mu.Lock()
|
|
mb := fs.lmb
|
|
fs.mu.Unlock()
|
|
|
|
total, f, l, err := mb.filteredPending("foo", false, 3)
|
|
require_NoError(t, err)
|
|
if total != 0 {
|
|
t.Fatalf("Expected total of 0 but got %d", total)
|
|
}
|
|
if f != 0 || l != 0 {
|
|
t.Fatalf("Expected first and last to be 0 as well, but got %d %d", f, l)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Test to optimize the selectMsgBlock with lots of blocks.
|
|
func TestFileStoreFetchPerf(t *testing.T) {
|
|
// Comment out to run.
|
|
t.SkipNow()
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 8192
|
|
fcfg.AsyncFlush = true
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Will create 25k msg blocks.
|
|
n, subj, msg := 100_000, "zzz", bytes.Repeat([]byte("ABC"), 600)
|
|
for i := 0; i < n; i++ {
|
|
if _, _, err := fs.StoreMsg(subj, nil, msg, 0); err != nil {
|
|
t.Fatalf("Unexpected error: %v", err)
|
|
}
|
|
}
|
|
|
|
// Time how long it takes us to load all messages.
|
|
var smv StoreMsg
|
|
now := time.Now()
|
|
for i := 0; i < n; i++ {
|
|
_, err := fs.LoadMsg(uint64(i), &smv)
|
|
if err != nil {
|
|
t.Fatalf("Unexpected error looking up seq %d: %v", i, err)
|
|
}
|
|
}
|
|
fmt.Printf("Elapsed to load all messages is %v\n", time.Since(now))
|
|
})
|
|
}
|
|
|
|
// For things like raft log when we compact and have a message block that could reclaim > 50% of space for block we want to do that.
|
|
// https://github.com/nats-io/nats-server/issues/2936
|
|
func TestFileStoreCompactReclaimHeadSpace(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 4 * 1024 * 1024
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Create random bytes for payload to test for corruption vs repeated.
|
|
msg := make([]byte, 64*1024)
|
|
crand.Read(msg)
|
|
|
|
// This gives us ~63 msgs in first and ~37 in second.
|
|
n, subj := 100, "z"
|
|
for i := 0; i < n; i++ {
|
|
_, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
checkNumBlocks := func(n int) {
|
|
t.Helper()
|
|
fs.mu.RLock()
|
|
defer fs.mu.RUnlock()
|
|
if len(fs.blks) != n {
|
|
t.Fatalf("Expected to have %d blocks, got %d", n, len(fs.blks))
|
|
}
|
|
}
|
|
|
|
getBlock := func(index int) *msgBlock {
|
|
t.Helper()
|
|
fs.mu.RLock()
|
|
defer fs.mu.RUnlock()
|
|
return fs.blks[index]
|
|
}
|
|
|
|
// Check that we did right thing and actually reclaimed since > 50%
|
|
checkBlock := func(mb *msgBlock) {
|
|
t.Helper()
|
|
|
|
mb.mu.RLock()
|
|
rbytes, mfn := mb.rbytes, mb.mfn
|
|
fseq, lseq := mb.first.seq, mb.last.seq
|
|
mb.mu.RUnlock()
|
|
|
|
// Check that sizes match as long as we are not doing compression.
|
|
if fcfg.Compression == NoCompression {
|
|
file, err := os.Open(mfn)
|
|
require_NoError(t, err)
|
|
defer file.Close()
|
|
fi, err := file.Stat()
|
|
require_NoError(t, err)
|
|
if rbytes != uint64(fi.Size()) {
|
|
t.Fatalf("Expected to rbytes == fi.Size, got %d vs %d", rbytes, fi.Size())
|
|
}
|
|
}
|
|
|
|
// Make sure we can pull messages and that they are ok.
|
|
var smv StoreMsg
|
|
sm, err := fs.LoadMsg(fseq, &smv)
|
|
require_NoError(t, err)
|
|
if !bytes.Equal(sm.msg, msg) {
|
|
t.Fatalf("Msgs don't match, original %q vs %q", msg, sm.msg)
|
|
}
|
|
sm, err = fs.LoadMsg(lseq, &smv)
|
|
require_NoError(t, err)
|
|
if !bytes.Equal(sm.msg, msg) {
|
|
t.Fatalf("Msgs don't match, original %q vs %q", msg, sm.msg)
|
|
}
|
|
}
|
|
|
|
checkNumBlocks(2)
|
|
_, err = fs.Compact(33)
|
|
require_NoError(t, err)
|
|
|
|
checkNumBlocks(2)
|
|
checkBlock(getBlock(0))
|
|
checkBlock(getBlock(1))
|
|
|
|
_, err = fs.Compact(85)
|
|
require_NoError(t, err)
|
|
|
|
checkNumBlocks(1)
|
|
checkBlock(getBlock(0))
|
|
|
|
// Make sure we can write.
|
|
_, _, err = fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
|
|
checkNumBlocks(1)
|
|
checkBlock(getBlock(0))
|
|
|
|
// Stop and start again.
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
checkNumBlocks(1)
|
|
checkBlock(getBlock(0))
|
|
|
|
// Make sure we can write.
|
|
_, _, err = fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreRememberLastMsgTime(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
var fs *fileStore
|
|
cfg := StreamConfig{Name: "TEST", Storage: FileStorage, MaxAge: 1 * time.Second}
|
|
|
|
getFS := func() *fileStore {
|
|
t.Helper()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
return fs
|
|
}
|
|
restartFS := func() {
|
|
t.Helper()
|
|
fs.Stop()
|
|
fs = getFS()
|
|
}
|
|
|
|
msg := bytes.Repeat([]byte("X"), 2*1024*1024)
|
|
|
|
// Get first one.
|
|
fs = getFS()
|
|
defer fs.Stop()
|
|
|
|
seq, ts, err := fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
// We will test that last msg time survives from delete, purge and expires after restart.
|
|
removed, err := fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
|
|
lt := time.Unix(0, ts).UTC()
|
|
require_True(t, lt == fs.State().LastTime)
|
|
|
|
// Restart
|
|
restartFS()
|
|
|
|
// Test that last time survived.
|
|
require_True(t, lt == fs.State().LastTime)
|
|
|
|
seq, ts, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
|
|
var smv StoreMsg
|
|
_, err = fs.LoadMsg(seq, &smv)
|
|
require_NoError(t, err)
|
|
|
|
fs.Purge()
|
|
|
|
// Restart
|
|
restartFS()
|
|
|
|
lt = time.Unix(0, ts).UTC()
|
|
require_True(t, lt == fs.State().LastTime)
|
|
|
|
_, _, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
seq, ts, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
|
|
require_True(t, seq == 4)
|
|
|
|
// Wait til messages expire.
|
|
checkFor(t, 5*time.Second, time.Second, func() error {
|
|
state := fs.State()
|
|
if state.Msgs == 0 {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Still has %d msgs", state.Msgs)
|
|
})
|
|
|
|
// Restart
|
|
restartFS()
|
|
|
|
lt = time.Unix(0, ts).UTC()
|
|
require_True(t, lt == fs.State().LastTime)
|
|
|
|
// Now make sure we retain the true last seq.
|
|
_, _, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
seq, ts, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
|
|
require_True(t, seq == 6)
|
|
removed, err = fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
|
|
removed, err = fs.RemoveMsg(seq - 1)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
|
|
// Restart
|
|
restartFS()
|
|
|
|
lt = time.Unix(0, ts).UTC()
|
|
require_True(t, lt == fs.State().LastTime)
|
|
require_True(t, seq == 6)
|
|
})
|
|
}
|
|
|
|
func (fs *fileStore) getFirstBlock() *msgBlock {
|
|
fs.mu.RLock()
|
|
defer fs.mu.RUnlock()
|
|
if len(fs.blks) == 0 {
|
|
return nil
|
|
}
|
|
return fs.blks[0]
|
|
}
|
|
|
|
func TestFileStoreRebuildStateDmapAccountingBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 1024 * 1024
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 0; i < 100; i++ {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
// Delete 2-40.
|
|
for i := 2; i <= 40; i++ {
|
|
_, err := fs.RemoveMsg(uint64(i))
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
mb := fs.getFirstBlock()
|
|
require_True(t, mb != nil)
|
|
|
|
check := func() {
|
|
t.Helper()
|
|
mb.mu.RLock()
|
|
defer mb.mu.RUnlock()
|
|
dmapLen := uint64(mb.dmap.Size())
|
|
if mb.msgs != (mb.last.seq-mb.first.seq+1)-dmapLen {
|
|
t.Fatalf("Consistency check failed: %d != %d -> last %d first %d len(dmap) %d",
|
|
mb.msgs, (mb.last.seq-mb.first.seq+1)-dmapLen, mb.last.seq, mb.first.seq, dmapLen)
|
|
}
|
|
}
|
|
|
|
check()
|
|
|
|
mb.mu.Lock()
|
|
err = mb.compact()
|
|
mb.mu.Unlock()
|
|
require_NoError(t, err)
|
|
|
|
// Now delete first.
|
|
_, err = fs.RemoveMsg(1)
|
|
require_NoError(t, err)
|
|
|
|
mb.mu.Lock()
|
|
_, _, err = mb.rebuildStateLocked()
|
|
mb.mu.Unlock()
|
|
require_NoError(t, err)
|
|
|
|
check()
|
|
})
|
|
}
|
|
|
|
func TestFileStorePurgeExWithSubject(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 1000
|
|
cfg := StreamConfig{Name: "TEST", Subjects: []string{"foo.>"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
payload := make([]byte, 20)
|
|
|
|
_, _, err = fs.StoreMsg("foo.0", nil, payload, 0)
|
|
require_NoError(t, err)
|
|
|
|
total := 200
|
|
for i := 0; i < total; i++ {
|
|
_, _, err = fs.StoreMsg("foo.1", nil, payload, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
_, _, err = fs.StoreMsg("foo.2", nil, []byte("xxxxxx"), 0)
|
|
require_NoError(t, err)
|
|
|
|
// Make sure we have our state file prior to Purge call.
|
|
require_NoError(t, fs.forceWriteFullState())
|
|
|
|
// Capture the current index.db file.
|
|
sfile := filepath.Join(fcfg.StoreDir, msgDir, streamStreamStateFile)
|
|
buf, err := os.ReadFile(sfile)
|
|
require_NoError(t, err)
|
|
require_True(t, len(buf) > 0)
|
|
|
|
// This should purge all "foo.1"
|
|
p, err := fs.PurgeEx("foo.1", 0, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, p, uint64(total))
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, 2)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
|
|
// Make sure we can recover same state.
|
|
require_NoError(t, fs.Stop())
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
before := state
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of %+v, got %+v", before, state)
|
|
}
|
|
|
|
// Also make sure we can recover properly with no index.db present.
|
|
// We want to make sure we preserve any tombstones from the subject based purge.
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of\n %+v, got\n %+v without index.db state", before, state)
|
|
}
|
|
|
|
// If we had an index.db from after PurgeEx but before Stop() would rewrite, make sure we
|
|
// properly can recover with the old index file. This would be a crash after the PurgeEx() call.
|
|
require_NoError(t, fs.Stop())
|
|
err = os.WriteFile(sfile, buf, defaultFilePerms)
|
|
require_NoError(t, err)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of %+v, got %+v with old index.db state", before, state)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStorePurgeExNoTombsOnBlockRemoval(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 1000
|
|
cfg := StreamConfig{Name: "TEST", Subjects: []string{"foo.>"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
payload := make([]byte, 20)
|
|
|
|
total := 100
|
|
for i := 0; i < total; i++ {
|
|
_, _, err = fs.StoreMsg("foo.1", nil, payload, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
_, _, err = fs.StoreMsg("foo.2", nil, payload, 0)
|
|
require_NoError(t, err)
|
|
|
|
require_Equal(t, fs.numMsgBlocks(), 6)
|
|
|
|
// Make sure we have our state file prior to Purge call.
|
|
fs.forceWriteFullState()
|
|
|
|
// Capture the current index.db file if it exists.
|
|
sfile := filepath.Join(fcfg.StoreDir, msgDir, streamStreamStateFile)
|
|
buf, err := os.ReadFile(sfile)
|
|
require_NoError(t, err)
|
|
require_True(t, len(buf) > 0)
|
|
|
|
// This should purge all "foo.1". This will remove the blocks so we want to make sure
|
|
// we do not write excessive tombstones here.
|
|
p, err := fs.PurgeEx("foo.1", 0, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, p, uint64(total))
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, 1)
|
|
require_Equal(t, state.FirstSeq, 101)
|
|
|
|
// Check that we only have 1 msg block.
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
|
|
// Put the old index.db back. We want to make sure without the empty block tombstones that we
|
|
// properly recover state.
|
|
fs.Stop()
|
|
err = os.WriteFile(sfile, buf, defaultFilePerms)
|
|
require_NoError(t, err)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state = fs.State()
|
|
require_Equal(t, state.Msgs, 1)
|
|
require_Equal(t, state.FirstSeq, 101)
|
|
})
|
|
}
|
|
|
|
// When the N.idx file is shorter than the previous write we could fail to recover the idx properly.
|
|
// For instance, with encryption and an expiring stream that has no messages, when a restart happens the decrypt will fail
|
|
// since their are extra bytes, and this could lead to a stream sequence reset to zero.
|
|
//
|
|
// NOTE: We do not use idx files anymore, but keeping test.
|
|
func TestFileStoreShortIndexWriteBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
// Encrypted mode shows, but could effect non-encrypted mode.
|
|
cfg := StreamConfig{Name: "TEST", Storage: FileStorage, MaxAge: time.Second}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 0; i < 100; i++ {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
// Wait til messages all go away.
|
|
checkFor(t, 5*time.Second, 200*time.Millisecond, func() error {
|
|
if state := fs.State(); state.Msgs != 0 {
|
|
return fmt.Errorf("Expected no msgs, got %d", state.Msgs)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if state := fs.State(); state.FirstSeq != 101 {
|
|
t.Fatalf("Expected first sequence of 101 vs %d", state.FirstSeq)
|
|
}
|
|
|
|
// Now restart..
|
|
fs.Stop()
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); state.FirstSeq != 101 || state.LastSeq != 100 {
|
|
t.Fatalf("Expected first sequence of 101 vs %d", state.FirstSeq)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreDoubleCompactWithWriteInBetweenEncryptedBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("ouch")
|
|
for i := 0; i < 5; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
_, err = fs.Compact(5)
|
|
require_NoError(t, err)
|
|
|
|
if state := fs.State(); state.LastSeq != 5 {
|
|
t.Fatalf("Expected last sequence to be 5 but got %d", state.LastSeq)
|
|
}
|
|
for i := 0; i < 5; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
_, err = fs.Compact(10)
|
|
require_NoError(t, err)
|
|
|
|
if state := fs.State(); state.LastSeq != 10 {
|
|
t.Fatalf("Expected last sequence to be 10 but got %d", state.LastSeq)
|
|
}
|
|
})
|
|
}
|
|
|
|
// When we kept the empty block for tracking sequence, we needed to reset the bek
|
|
// counter when encrypted for subsequent writes to be correct. The bek in place could
|
|
// possibly still have a non-zero counter from previous writes.
|
|
// Happens when all messages expire and the are flushed and then subsequent writes occur.
|
|
func TestFileStoreEncryptedKeepIndexNeedBekResetBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
ttl := 1 * time.Second
|
|
cfg := StreamConfig{Name: "zzz", Storage: FileStorage, MaxAge: ttl}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("ouch")
|
|
for i := 0; i < 5; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
// Want to go to 0.
|
|
// This will leave the marker.
|
|
checkFor(t, 5*time.Second, ttl, func() error {
|
|
if state := fs.State(); state.Msgs != 0 {
|
|
return fmt.Errorf("Expected no msgs, got %d", state.Msgs)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Now write additional messages.
|
|
for i := 0; i < 5; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
// Make sure the buffer is cleared.
|
|
fs.mu.RLock()
|
|
mb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
mb.mu.Lock()
|
|
mb.clearCacheAndOffset()
|
|
mb.mu.Unlock()
|
|
|
|
// Now make sure we can read.
|
|
var smv StoreMsg
|
|
_, err = fs.LoadMsg(10, &smv)
|
|
require_NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func (fs *fileStore) reportMeta() (hasPSIM, hasAnyFSS bool) {
|
|
fs.mu.RLock()
|
|
defer fs.mu.RUnlock()
|
|
|
|
hasPSIM = fs.psim != nil
|
|
for _, mb := range fs.blks {
|
|
mb.mu.RLock()
|
|
hasAnyFSS = hasAnyFSS || mb.fss != nil
|
|
mb.mu.RUnlock()
|
|
if hasAnyFSS {
|
|
break
|
|
}
|
|
}
|
|
return hasPSIM, hasAnyFSS
|
|
}
|
|
|
|
func TestFileStoreExpireSubjectMeta(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 1024
|
|
fcfg.CacheExpire = 500 * time.Millisecond
|
|
fcfg.SubjectStateExpire = time.Second
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"kv.>"}, Storage: FileStorage, MaxMsgsPer: 1}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
ns := 100
|
|
for i := 1; i <= ns; i++ {
|
|
subj := fmt.Sprintf("kv.%d", i)
|
|
_, _, err := fs.StoreMsg(subj, nil, []byte("value"), 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Test that on restart we do not have extensize metadata but do have correct number of subjects/keys.
|
|
// Only thing really needed for store state / stream info.
|
|
fs.Stop()
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
if ss.NumSubjects != ns {
|
|
t.Fatalf("Expected NumSubjects of %d, got %d", ns, ss.NumSubjects)
|
|
}
|
|
|
|
// Make sure we clear mb fss meta
|
|
checkFor(t, fcfg.SubjectStateExpire*2, 500*time.Millisecond, func() error {
|
|
if _, hasAnyFSS := fs.reportMeta(); hasAnyFSS {
|
|
return fmt.Errorf("Still have mb fss state")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// LoadLast, which is what KV uses, should load meta and succeed.
|
|
_, err = fs.LoadLastMsg("kv.22", nil)
|
|
require_NoError(t, err)
|
|
// Make sure we clear mb fss meta
|
|
checkFor(t, fcfg.SubjectStateExpire*2, 500*time.Millisecond, func() error {
|
|
if _, hasAnyFSS := fs.reportMeta(); hasAnyFSS {
|
|
return fmt.Errorf("Still have mb fss state")
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMaxMsgsPerSubject(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 128
|
|
fcfg.CacheExpire = time.Second
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"kv.>"}, Storage: FileStorage, MaxMsgsPer: 1}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
ns := 100
|
|
for i := 1; i <= ns; i++ {
|
|
subj := fmt.Sprintf("kv.%d", i)
|
|
_, _, err := fs.StoreMsg(subj, nil, []byte("value"), 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
for i := 1; i <= ns; i++ {
|
|
subj := fmt.Sprintf("kv.%d", i)
|
|
_, _, err := fs.StoreMsg(subj, nil, []byte("value"), 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
if state := fs.State(); state.Msgs != 100 || state.FirstSeq != 101 || state.LastSeq != 200 || len(state.Deleted) != 0 {
|
|
t.Fatalf("Bad state: %+v", state)
|
|
}
|
|
|
|
if nb := fs.numMsgBlocks(); nb != 34 {
|
|
t.Fatalf("Expected 34 blocks, got %d", nb)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Testing the case in https://github.com/nats-io/nats-server/issues/4247
|
|
func TestFileStoreMaxMsgsAndMaxMsgsPerSubject(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 128
|
|
fcfg.CacheExpire = time.Second
|
|
cfg := StreamConfig{
|
|
Name: "zzz",
|
|
Subjects: []string{"kv.>"},
|
|
Storage: FileStorage,
|
|
Discard: DiscardNew, MaxMsgs: 100, // Total stream policy
|
|
DiscardNewPer: true, MaxMsgsPer: 1, // Per-subject policy
|
|
}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 1; i <= 101; i++ {
|
|
subj := fmt.Sprintf("kv.%d", i)
|
|
_, _, err := fs.StoreMsg(subj, nil, []byte("value"), 0)
|
|
if i == 101 {
|
|
// The 101th iteration should fail because MaxMsgs is set to
|
|
// 100 and the policy is DiscardNew.
|
|
require_Error(t, err)
|
|
} else {
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
|
|
for i := 1; i <= 100; i++ {
|
|
subj := fmt.Sprintf("kv.%d", i)
|
|
_, _, err := fs.StoreMsg(subj, nil, []byte("value"), 0)
|
|
// All of these iterations should fail because MaxMsgsPer is set
|
|
// to 1 and DiscardNewPer is set to true, forcing us to reject
|
|
// cases where there is already a message on this subject.
|
|
require_Error(t, err)
|
|
}
|
|
|
|
if state := fs.State(); state.Msgs != 100 || state.FirstSeq != 1 || state.LastSeq != 100 || len(state.Deleted) != 0 {
|
|
// There should be 100 messages exactly, as the 101st subject
|
|
// should have been rejected in the first loop, and any duplicates
|
|
// on the other subjects should have been rejected in the second loop.
|
|
t.Fatalf("Bad state: %+v", state)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreSubjectStateCacheExpiration(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 32
|
|
fcfg.CacheExpire = time.Second
|
|
fcfg.SubjectStateExpire = time.Second
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"kv.>"}, Storage: FileStorage, MaxMsgsPer: 2}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 1; i <= 100; i++ {
|
|
subj := fmt.Sprintf("kv.foo.%d", i)
|
|
_, _, err := fs.StoreMsg(subj, nil, []byte("value"), 0)
|
|
require_NoError(t, err)
|
|
}
|
|
for i := 1; i <= 100; i++ {
|
|
subj := fmt.Sprintf("kv.bar.%d", i)
|
|
_, _, err := fs.StoreMsg(subj, nil, []byte("value"), 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Make sure we clear mb fss meta before asking for SubjectState.
|
|
checkFor(t, 10*time.Second, 500*time.Millisecond, func() error {
|
|
if _, hasAnyFSS := fs.reportMeta(); hasAnyFSS {
|
|
return fmt.Errorf("Still have mb fss state")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
if fss := fs.SubjectsState("kv.bar.>"); len(fss) != 100 {
|
|
t.Fatalf("Expected 100 entries but got %d", len(fss))
|
|
}
|
|
|
|
fss := fs.SubjectsState("kv.bar.99")
|
|
if len(fss) != 1 {
|
|
t.Fatalf("Expected 1 entry but got %d", len(fss))
|
|
}
|
|
expected := SimpleState{Msgs: 1, First: 199, Last: 199}
|
|
if ss := fss["kv.bar.99"]; ss != expected {
|
|
t.Fatalf("Bad subject state, expected %+v but got %+v", expected, ss)
|
|
}
|
|
|
|
// Now add one to end and check as well for non-wildcard.
|
|
_, _, err = fs.StoreMsg("kv.foo.1", nil, []byte("value22"), 0)
|
|
require_NoError(t, err)
|
|
|
|
if state := fs.State(); state.Msgs != 201 {
|
|
t.Fatalf("Expected 201 msgs but got %+v", state)
|
|
}
|
|
|
|
fss = fs.SubjectsState("kv.foo.1")
|
|
if len(fss) != 1 {
|
|
t.Fatalf("Expected 1 entry but got %d", len(fss))
|
|
}
|
|
expected = SimpleState{Msgs: 2, First: 1, Last: 201}
|
|
if ss := fss["kv.foo.1"]; ss != expected {
|
|
t.Fatalf("Bad subject state, expected %+v but got %+v", expected, ss)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreEncrypted(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("aes ftw")
|
|
for i := 0; i < 50; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
o, err := fs.ConsumerStore("o22", time.Time{}, &ConsumerConfig{})
|
|
require_NoError(t, err)
|
|
|
|
state := &ConsumerState{}
|
|
state.Delivered.Consumer = 22
|
|
state.Delivered.Stream = 22
|
|
state.AckFloor.Consumer = 11
|
|
state.AckFloor.Stream = 11
|
|
err = o.Update(state)
|
|
require_NoError(t, err)
|
|
|
|
o.Stop()
|
|
fs.Stop()
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Now make sure we can read.
|
|
var smv StoreMsg
|
|
sm, err := fs.LoadMsg(10, &smv)
|
|
require_NoError(t, err)
|
|
require_True(t, string(sm.msg) == "aes ftw")
|
|
|
|
o, err = fs.ConsumerStore("o22", time.Time{}, &ConsumerConfig{})
|
|
require_NoError(t, err)
|
|
rstate, err := o.State()
|
|
require_NoError(t, err)
|
|
|
|
if rstate.Delivered != state.Delivered || rstate.AckFloor != state.AckFloor {
|
|
t.Fatalf("Bad recovered consumer state, expected %+v got %+v", state, rstate)
|
|
}
|
|
})
|
|
}
|
|
|
|
// Make sure we do not go through block loads when we know no subjects will exists, e.g. raft.
|
|
func TestFileStoreNoFSSWhenNoSubjects(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
n, msg := 100, []byte("raft state")
|
|
for i := 0; i < n; i++ {
|
|
_, _, err := fs.StoreMsg(_EMPTY_, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
state := fs.State()
|
|
require_True(t, state.Msgs == uint64(n))
|
|
|
|
fs.Stop()
|
|
fs, err = newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Make sure we did not load the block trying to generate fss.
|
|
fs.mu.RLock()
|
|
mb := fs.blks[0]
|
|
fs.mu.RUnlock()
|
|
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
|
|
if mb.cloads > 0 {
|
|
t.Fatalf("Expected no cache loads but got %d", mb.cloads)
|
|
}
|
|
if mb.fss != nil {
|
|
t.Fatalf("Expected fss to be nil")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreNoFSSBugAfterRemoveFirst(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 8 * 1024 * 1024
|
|
fcfg.CacheExpire = 200 * time.Millisecond
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo.bar.*"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
n, msg := 100, bytes.Repeat([]byte("ZZZ"), 33) // ~100bytes
|
|
for i := 0; i < n; i++ {
|
|
subj := fmt.Sprintf("foo.bar.%d", i)
|
|
_, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
state := fs.State()
|
|
require_True(t, state.Msgs == uint64(n))
|
|
|
|
// Let fss expire.
|
|
time.Sleep(250 * time.Millisecond)
|
|
|
|
_, err = fs.RemoveMsg(1)
|
|
require_NoError(t, err)
|
|
|
|
sm, _, err := fs.LoadNextMsg("foo.>", true, 1, nil)
|
|
require_NoError(t, err)
|
|
require_True(t, sm.subj == "foo.bar.1")
|
|
|
|
// Make sure mb.fss does not have the entry for foo.bar.0
|
|
fs.mu.Lock()
|
|
mb := fs.blks[0]
|
|
fs.mu.Unlock()
|
|
mb.mu.RLock()
|
|
ss, ok := mb.fss.Find([]byte("foo.bar.0"))
|
|
mb.mu.RUnlock()
|
|
|
|
if ok && ss != nil {
|
|
t.Fatalf("Expected no state for %q, but got %+v\n", "foo.bar.0", ss)
|
|
}
|
|
})
|
|
}
|
|
|
|
// NOTE: We do not use fss files anymore, but leaving test in place.
|
|
func TestFileStoreNoFSSAfterRecover(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
n, msg := 100, []byte("no fss for you!")
|
|
for i := 0; i < n; i++ {
|
|
_, _, err := fs.StoreMsg(_EMPTY_, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
state := fs.State()
|
|
require_True(t, state.Msgs == uint64(n))
|
|
|
|
fs.Stop()
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Make sure we did not load the block trying to generate fss.
|
|
fs.mu.RLock()
|
|
mb := fs.blks[0]
|
|
fs.mu.RUnlock()
|
|
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
|
|
if mb.fss != nil {
|
|
t.Fatalf("Expected no fss post recover")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreFSSCloseAndKeepOnExpireOnRecoverBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
ttl := 100 * time.Millisecond
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage, MaxAge: ttl}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
fs.Stop()
|
|
time.Sleep(2 * ttl)
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); state.NumSubjects != 0 {
|
|
t.Fatalf("Expected no subjects with no messages, got %d", state.NumSubjects)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreExpireOnRecoverSubjectAccounting(t *testing.T) {
|
|
const msgLen = 19
|
|
msg := bytes.Repeat([]byte("A"), msgLen)
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 100
|
|
ttl := 200 * time.Millisecond
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage, MaxAge: ttl}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// These are in first block.
|
|
fs.StoreMsg("A", nil, msg, 0)
|
|
fs.StoreMsg("B", nil, msg, 0)
|
|
time.Sleep(ttl / 2)
|
|
// This one in 2nd block.
|
|
fs.StoreMsg("C", nil, msg, 0)
|
|
|
|
fs.Stop()
|
|
time.Sleep(ttl/2 + 10*time.Millisecond)
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Make sure we take into account PSIM when throwing a whole block away.
|
|
if state := fs.State(); state.NumSubjects != 1 {
|
|
t.Fatalf("Expected 1 subject, got %d", state.NumSubjects)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreFSSExpireNumPendingBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cexp := 100 * time.Millisecond
|
|
fcfg.CacheExpire = cexp
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"KV.>"}, MaxMsgsPer: 1, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Let FSS meta expire.
|
|
time.Sleep(2 * cexp)
|
|
|
|
_, _, err = fs.StoreMsg("KV.X", nil, []byte("Y"), 0)
|
|
require_NoError(t, err)
|
|
|
|
fss, err := fs.FilteredState(1, "KV.X")
|
|
require_NoError(t, err)
|
|
if fss.Msgs != 1 {
|
|
t.Fatalf("Expected only 1 msg, got %d", fss.Msgs)
|
|
}
|
|
})
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/3484
|
|
func TestFileStoreFilteredFirstMatchingBug(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo.>"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("foo.foo", nil, []byte("A"), 0)
|
|
require_NoError(t, err)
|
|
|
|
_, _, err = fs.StoreMsg("foo.foo", nil, []byte("B"), 0)
|
|
require_NoError(t, err)
|
|
|
|
_, _, err = fs.StoreMsg("foo.foo", nil, []byte("C"), 0)
|
|
require_NoError(t, err)
|
|
|
|
fs.mu.RLock()
|
|
mb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
|
|
mb.mu.Lock()
|
|
// Simulate swapping out the fss state and reading it back in with only one subject
|
|
// present in the block.
|
|
if mb.fss != nil {
|
|
mb.fss = nil
|
|
}
|
|
// Now load info back in.
|
|
mb.generatePerSubjectInfo()
|
|
mb.mu.Unlock()
|
|
|
|
// Now add in a different subject.
|
|
_, _, err = fs.StoreMsg("foo.bar", nil, []byte("X"), 0)
|
|
require_NoError(t, err)
|
|
|
|
// Now see if a filtered load would incorrectly succeed.
|
|
sm, _, err := fs.LoadNextMsg("foo.foo", false, 4, nil)
|
|
if err == nil || sm != nil {
|
|
t.Fatalf("Loaded filtered message with wrong subject, wanted %q got %q", "foo.foo", sm.subj)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreOutOfSpaceRebuildState(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("foo", nil, []byte("A"), 0)
|
|
require_NoError(t, err)
|
|
|
|
_, _, err = fs.StoreMsg("bar", nil, []byte("B"), 0)
|
|
require_NoError(t, err)
|
|
|
|
// Grab state.
|
|
state := fs.State()
|
|
ss := fs.SubjectsState(">")
|
|
|
|
// Set mock out of space error to trip.
|
|
fs.mu.RLock()
|
|
mb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
|
|
mb.mu.Lock()
|
|
mb.mockWriteErr = true
|
|
mb.mu.Unlock()
|
|
|
|
_, _, err = fs.StoreMsg("baz", nil, []byte("C"), 0)
|
|
require_Error(t, err, errors.New("mock write error"))
|
|
|
|
nstate := fs.State()
|
|
nss := fs.SubjectsState(">")
|
|
|
|
if !reflect.DeepEqual(state, nstate) {
|
|
t.Fatalf("State expected to be\n %+v\nvs\n %+v", state, nstate)
|
|
}
|
|
|
|
if !reflect.DeepEqual(ss, nss) {
|
|
t.Fatalf("Subject state expected to be\n %+v\nvs\n %+v", ss, nss)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreRebuildStateProperlyWithMaxMsgsPerSubject(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 4096
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo", "bar", "baz"}, Storage: FileStorage, MaxMsgsPer: 1}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Send one to baz at beginning.
|
|
_, _, err = fs.StoreMsg("baz", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
ns := 1000
|
|
for i := 1; i <= ns; i++ {
|
|
_, _, err := fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
_, _, err = fs.StoreMsg("bar", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
if ss.NumSubjects != 3 {
|
|
t.Fatalf("Expected NumSubjects of 3, got %d", ss.NumSubjects)
|
|
}
|
|
if ss.Msgs != 3 {
|
|
t.Fatalf("Expected NumMsgs of 3, got %d", ss.Msgs)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreUpdateMaxMsgsPerSubject(t *testing.T) {
|
|
cfg := StreamConfig{
|
|
Name: "TEST",
|
|
Storage: FileStorage,
|
|
Subjects: []string{"foo"},
|
|
MaxMsgsPer: 10,
|
|
}
|
|
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Make sure this is honored on an update.
|
|
cfg.MaxMsgsPer = 50
|
|
err = fs.UpdateConfig(&cfg)
|
|
require_NoError(t, err)
|
|
|
|
numStored := 22
|
|
for i := 0; i < numStored; i++ {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
ss := fs.SubjectsState("foo")["foo"]
|
|
if ss.Msgs != uint64(numStored) {
|
|
t.Fatalf("Expected to have %d stored, got %d", numStored, ss.Msgs)
|
|
}
|
|
|
|
// Now make sure we trunk if setting to lower value.
|
|
cfg.MaxMsgsPer = 10
|
|
err = fs.UpdateConfig(&cfg)
|
|
require_NoError(t, err)
|
|
|
|
ss = fs.SubjectsState("foo")["foo"]
|
|
if ss.Msgs != 10 {
|
|
t.Fatalf("Expected to have %d stored, got %d", 10, ss.Msgs)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreBadFirstAndFailedExpireAfterRestart(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 256
|
|
ttl := time.Second
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage, MaxAge: ttl}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// With block size of 256 and subject and message below, seq 8 starts new block.
|
|
// Will double check and fail test if not the case since test depends on this.
|
|
subj, msg := "foo", []byte("ZZ")
|
|
// These are all instant and will expire after 1 sec.
|
|
start := time.Now()
|
|
for i := 0; i < 7; i++ {
|
|
_, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Put two more after a delay.
|
|
time.Sleep(1500 * time.Millisecond)
|
|
seq, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
_, _, err = fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
|
|
// Make sure that sequence 8 is first in second block, and break test if that is not true.
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
lmb.mu.RLock()
|
|
first := lmb.first.seq
|
|
lmb.mu.RUnlock()
|
|
require_True(t, first == 8)
|
|
|
|
// Instantly remove first one from second block.
|
|
// On restart this will trigger expire on recover which will set fs.FirstSeq to the deleted one.
|
|
fs.RemoveMsg(seq)
|
|
|
|
// Stop the filstore and wait til first block expires.
|
|
fs.Stop()
|
|
time.Sleep(ttl - time.Since(start) + (time.Second))
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Check that state is correct for first message which should be 9 and have a proper timestamp.
|
|
var state StreamState
|
|
fs.FastState(&state)
|
|
ts := state.FirstTime
|
|
require_True(t, state.Msgs == 1)
|
|
require_True(t, state.FirstSeq == 9)
|
|
require_True(t, !state.FirstTime.IsZero())
|
|
|
|
// Wait and make sure expire timer is still working properly.
|
|
time.Sleep(2 * ttl)
|
|
fs.FastState(&state)
|
|
require_Equal(t, state.Msgs, 0)
|
|
require_Equal(t, state.FirstSeq, 10)
|
|
require_Equal(t, state.LastSeq, 9)
|
|
require_Equal(t, state.LastTime, ts)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreCompactAllWithDanglingLMB(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("ZZ")
|
|
for i := 0; i < 100; i++ {
|
|
_, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
fs.RemoveMsg(100)
|
|
purged, err := fs.Compact(100)
|
|
require_NoError(t, err)
|
|
require_True(t, purged == 99)
|
|
|
|
_, _, err = fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreStateWithBlkFirstDeleted(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 4096
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
toStore := 500
|
|
for i := 0; i < toStore; i++ {
|
|
_, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Delete some messages from the beginning of an interior block.
|
|
fs.mu.RLock()
|
|
require_True(t, len(fs.blks) > 2)
|
|
fseq := fs.blks[1].first.seq
|
|
fs.mu.RUnlock()
|
|
|
|
// Now start from first seq of second blk and delete 10 msgs
|
|
for seq := fseq; seq < fseq+10; seq++ {
|
|
removed, err := fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
}
|
|
|
|
// This bug was in normal detailed state. But check fast state too.
|
|
var fstate StreamState
|
|
fs.FastState(&fstate)
|
|
require_True(t, fstate.NumDeleted == 10)
|
|
state := fs.State()
|
|
require_True(t, state.NumDeleted == 10)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMsgBlkFailOnKernelFaultLostDataReporting(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 4096
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
toStore := 500
|
|
for i := 0; i < toStore; i++ {
|
|
_, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// We want to make sure all of the scenarios report lost data properly.
|
|
// Will run 3 scenarios, 1st block, last block, interior block.
|
|
// The new system does not detect byzantine behavior by default on creating the store.
|
|
// A LoadMsg() of checkMsgs() call will be needed now.
|
|
|
|
// First block
|
|
fs.mu.RLock()
|
|
require_True(t, len(fs.blks) > 0)
|
|
mfn := fs.blks[0].mfn
|
|
fs.mu.RUnlock()
|
|
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Remove(mfn))
|
|
|
|
// Restart.
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, err = fs.LoadMsg(1, nil)
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
|
|
// Load will rebuild fs itself async..
|
|
checkFor(t, time.Second, 50*time.Millisecond, func() error {
|
|
if state := fs.State(); state.Lost != nil {
|
|
return nil
|
|
}
|
|
return errors.New("no ld yet")
|
|
})
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.FirstSeq, 94)
|
|
require_True(t, state.Lost != nil)
|
|
require_Len(t, len(state.Lost.Msgs), 93)
|
|
|
|
// Last block
|
|
fs.mu.RLock()
|
|
require_True(t, len(fs.blks) > 0)
|
|
require_True(t, fs.lmb != nil)
|
|
mfn = fs.lmb.mfn
|
|
fs.mu.RUnlock()
|
|
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Remove(mfn))
|
|
|
|
// Restart.
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state = fs.State()
|
|
require_Equal(t, state.FirstSeq, 94)
|
|
require_Equal(t, state.LastSeq, 500) // Make sure we do not lose last seq.
|
|
require_Equal(t, state.NumDeleted, 35) // These are interiors
|
|
require_True(t, state.Lost != nil)
|
|
require_Len(t, len(state.Lost.Msgs), 35)
|
|
|
|
// Interior block.
|
|
fs.mu.RLock()
|
|
require_True(t, len(fs.blks) > 3)
|
|
mfn = fs.blks[len(fs.blks)-3].mfn
|
|
fs.mu.RUnlock()
|
|
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Remove(mfn))
|
|
|
|
// Restart.
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Need checkMsgs to catch interior one.
|
|
ld, err := fs.checkMsgs()
|
|
require_NoError(t, err)
|
|
require_True(t, ld != nil)
|
|
|
|
state = fs.State()
|
|
require_Equal(t, state.FirstSeq, 94)
|
|
require_Equal(t, state.LastSeq, 500) // Make sure we do not lose last seq.
|
|
require_Equal(t, state.NumDeleted, 128)
|
|
require_True(t, state.Lost != nil)
|
|
require_Len(t, len(state.Lost.Msgs), 93)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreAllFilteredStateWithDeleted(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 1024
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
for i := 0; i < 100; i++ {
|
|
_, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
remove := func(seqs ...uint64) {
|
|
for _, seq := range seqs {
|
|
ok, err := fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
require_True(t, ok)
|
|
}
|
|
}
|
|
|
|
checkFilteredState := func(start, msgs, first, last int) {
|
|
fss, err := fs.FilteredState(uint64(start), _EMPTY_)
|
|
require_NoError(t, err)
|
|
if fss.Msgs != uint64(msgs) {
|
|
t.Fatalf("Expected %d msgs, got %d", msgs, fss.Msgs)
|
|
}
|
|
if fss.First != uint64(first) {
|
|
t.Fatalf("Expected %d to be first, got %d", first, fss.First)
|
|
}
|
|
if fss.Last != uint64(last) {
|
|
t.Fatalf("Expected %d to be last, got %d", last, fss.Last)
|
|
}
|
|
}
|
|
|
|
checkFilteredState(1, 100, 1, 100)
|
|
remove(2)
|
|
checkFilteredState(2, 98, 3, 100)
|
|
remove(3, 4, 5)
|
|
checkFilteredState(2, 95, 6, 100)
|
|
checkFilteredState(6, 95, 6, 100)
|
|
remove(8, 10, 12, 14, 16, 18)
|
|
checkFilteredState(7, 88, 7, 100)
|
|
|
|
// Now check when purged that we return first and last sequences properly.
|
|
fs.Purge()
|
|
checkFilteredState(0, 0, 101, 100)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreStreamTruncateResetMultiBlock(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 128
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
for i := 0; i < 1000; i++ {
|
|
_, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
fs.syncBlocks()
|
|
require_True(t, fs.numMsgBlocks() == 500)
|
|
|
|
// Reset everything
|
|
require_NoError(t, fs.Truncate(0))
|
|
require_True(t, fs.numMsgBlocks() == 0)
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, 0)
|
|
require_Equal(t, state.Bytes, 0)
|
|
require_Equal(t, state.FirstSeq, 0)
|
|
require_Equal(t, state.LastSeq, 0)
|
|
require_Equal(t, state.NumSubjects, 0)
|
|
require_Equal(t, state.NumDeleted, 0)
|
|
|
|
for i := 0; i < 1000; i++ {
|
|
_, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
fs.syncBlocks()
|
|
|
|
state = fs.State()
|
|
require_Equal(t, state.Msgs, 1000)
|
|
require_Equal(t, state.Bytes, 44000)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 1000)
|
|
require_Equal(t, state.NumSubjects, 1)
|
|
require_Equal(t, state.NumDeleted, 0)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreStreamCompactMultiBlockSubjectInfo(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 128
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 0; i < 1000; i++ {
|
|
subj := fmt.Sprintf("foo.%d", i)
|
|
_, _, err := fs.StoreMsg(subj, nil, []byte("Hello World"), 0)
|
|
require_NoError(t, err)
|
|
}
|
|
require_True(t, fs.numMsgBlocks() == 500)
|
|
|
|
// Compact such that we know we throw blocks away from the beginning.
|
|
deleted, err := fs.Compact(501)
|
|
require_NoError(t, err)
|
|
require_True(t, deleted == 500)
|
|
require_True(t, fs.numMsgBlocks() == 250)
|
|
|
|
// Make sure we adjusted for subjects etc.
|
|
state := fs.State()
|
|
require_True(t, state.NumSubjects == 500)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreSubjectsTotals(t *testing.T) {
|
|
// No need for all permutations here.
|
|
storeDir := t.TempDir()
|
|
fcfg := FileStoreConfig{StoreDir: storeDir}
|
|
fs, err := newFileStore(fcfg, StreamConfig{Name: "zzz", Subjects: []string{"*.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fmap := make(map[int]int)
|
|
bmap := make(map[int]int)
|
|
|
|
var m map[int]int
|
|
var ft string
|
|
|
|
for i := 0; i < 10_000; i++ {
|
|
// Flip coin for prefix
|
|
if rand.Intn(2) == 0 {
|
|
ft, m = "foo", fmap
|
|
} else {
|
|
ft, m = "bar", bmap
|
|
}
|
|
dt := rand.Intn(100)
|
|
subj := fmt.Sprintf("%s.%d", ft, dt)
|
|
m[dt]++
|
|
|
|
_, _, err := fs.StoreMsg(subj, nil, []byte("Hello World"), 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Now test SubjectsTotal
|
|
for dt, total := range fmap {
|
|
subj := fmt.Sprintf("foo.%d", dt)
|
|
m := fs.SubjectsTotals(subj)
|
|
if m[subj] != uint64(total) {
|
|
t.Fatalf("Expected %q to have %d total, got %d", subj, total, m[subj])
|
|
}
|
|
}
|
|
|
|
// Check fmap.
|
|
if st := fs.SubjectsTotals("foo.*"); len(st) != len(fmap) {
|
|
t.Fatalf("Expected %d subjects for %q, got %d", len(fmap), "foo.*", len(st))
|
|
} else {
|
|
expected := 0
|
|
for _, n := range fmap {
|
|
expected += n
|
|
}
|
|
received := uint64(0)
|
|
for _, n := range st {
|
|
received += n
|
|
}
|
|
if received != uint64(expected) {
|
|
t.Fatalf("Expected %d total but got %d", expected, received)
|
|
}
|
|
}
|
|
|
|
// Check bmap.
|
|
if st := fs.SubjectsTotals("bar.*"); len(st) != len(bmap) {
|
|
t.Fatalf("Expected %d subjects for %q, got %d", len(bmap), "bar.*", len(st))
|
|
} else {
|
|
expected := 0
|
|
for _, n := range bmap {
|
|
expected += n
|
|
}
|
|
received := uint64(0)
|
|
for _, n := range st {
|
|
received += n
|
|
}
|
|
if received != uint64(expected) {
|
|
t.Fatalf("Expected %d total but got %d", expected, received)
|
|
}
|
|
}
|
|
|
|
// All with pwc match.
|
|
if st, expected := fs.SubjectsTotals("*.*"), len(bmap)+len(fmap); len(st) != expected {
|
|
t.Fatalf("Expected %d subjects for %q, got %d", expected, "*.*", len(st))
|
|
}
|
|
}
|
|
|
|
func TestFileStoreConsumerStoreEncodeAfterRestart(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
state := &ConsumerState{}
|
|
|
|
func() { // for defers
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
o, err := fs.ConsumerStore("o22", time.Time{}, &ConsumerConfig{AckPolicy: AckExplicit})
|
|
require_NoError(t, err)
|
|
defer o.Stop()
|
|
|
|
state.Delivered.Consumer = 22
|
|
state.Delivered.Stream = 22
|
|
state.AckFloor.Consumer = 11
|
|
state.AckFloor.Stream = 11
|
|
err = o.Update(state)
|
|
require_NoError(t, err)
|
|
}()
|
|
|
|
func() { // for defers
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
o, err := fs.ConsumerStore("o22", time.Time{}, &ConsumerConfig{AckPolicy: AckExplicit})
|
|
require_NoError(t, err)
|
|
defer o.Stop()
|
|
|
|
if o.(*consumerFileStore).state.Delivered != state.Delivered {
|
|
t.Fatalf("Consumer state is wrong %+v vs %+v", o.(*consumerFileStore).state, state)
|
|
}
|
|
if o.(*consumerFileStore).state.AckFloor != state.AckFloor {
|
|
t.Fatalf("Consumer state is wrong %+v vs %+v", o.(*consumerFileStore).state, state)
|
|
}
|
|
}()
|
|
})
|
|
}
|
|
|
|
func TestFileStoreNumPendingLargeNumBlks(t *testing.T) {
|
|
// No need for all permutations here.
|
|
storeDir := t.TempDir()
|
|
fcfg := FileStoreConfig{
|
|
StoreDir: storeDir,
|
|
BlockSize: 128, // Small on purpose to create alot of blks.
|
|
}
|
|
fs, err := newFileStore(fcfg, StreamConfig{Name: "zzz", Subjects: []string{"zzz"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "zzz", bytes.Repeat([]byte("X"), 100)
|
|
numMsgs := 10_000
|
|
|
|
for i := 0; i < numMsgs; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
start := time.Now()
|
|
total, _, err := fs.NumPending(4000, "zzz", false)
|
|
require_NoError(t, err)
|
|
require_LessThan(t, time.Since(start), 15*time.Millisecond)
|
|
require_Equal(t, total, 6001)
|
|
|
|
start = time.Now()
|
|
total, _, err = fs.NumPending(6000, "zzz", false)
|
|
require_NoError(t, err)
|
|
require_LessThan(t, time.Since(start), 25*time.Millisecond)
|
|
require_Equal(t, total, 4001)
|
|
|
|
// Now delete a message in first half and second half.
|
|
fs.RemoveMsg(1000)
|
|
fs.RemoveMsg(9000)
|
|
|
|
start = time.Now()
|
|
total, _, err = fs.NumPending(4000, "zzz", false)
|
|
require_NoError(t, err)
|
|
require_LessThan(t, time.Since(start), 50*time.Millisecond)
|
|
require_Equal(t, total, 6000)
|
|
|
|
start = time.Now()
|
|
total, _, err = fs.NumPending(6000, "zzz", false)
|
|
require_NoError(t, err)
|
|
require_LessThan(t, time.Since(start), 50*time.Millisecond)
|
|
require_Equal(t, total, 4000)
|
|
}
|
|
|
|
func TestFileStoreSkipMsgAndNumBlocks(t *testing.T) {
|
|
// No need for all permutations here.
|
|
storeDir := t.TempDir()
|
|
fcfg := FileStoreConfig{
|
|
StoreDir: storeDir,
|
|
BlockSize: 128, // Small on purpose to create alot of blks.
|
|
}
|
|
fs, err := newFileStore(fcfg, StreamConfig{Name: "zzz", Subjects: []string{"zzz"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "zzz", bytes.Repeat([]byte("X"), 100)
|
|
|
|
_, _, err = fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
require_NoError(t, fs.SkipMsgs(0, 10_000))
|
|
_, _, err = fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, fs.numMsgBlocks(), 3)
|
|
}
|
|
|
|
func TestFileStoreRestoreEncryptedWithNoKeyFuncFails(t *testing.T) {
|
|
// No need for all permutations here.
|
|
fcfg := FileStoreConfig{StoreDir: t.TempDir(), Cipher: AES}
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"zzz"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "zzz", bytes.Repeat([]byte("X"), 100)
|
|
numMsgs := 100
|
|
for i := 0; i < numMsgs; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
fs.Stop()
|
|
|
|
// Make sure if we try to restore with no prf (key) that it fails.
|
|
_, err = newFileStoreWithCreated(fcfg, cfg, time.Now(), nil, nil)
|
|
require_Error(t, err, errNoMainKey)
|
|
}
|
|
|
|
func TestFileStoreInitialFirstSeq(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Storage: FileStorage, FirstSeq: 1000}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
seq, _, err := fs.StoreMsg("A", nil, []byte("OK"), 0)
|
|
require_NoError(t, err)
|
|
if seq != 1000 {
|
|
t.Fatalf("Message should have been sequence 1000 but was %d", seq)
|
|
}
|
|
|
|
seq, _, err = fs.StoreMsg("B", nil, []byte("OK"), 0)
|
|
require_NoError(t, err)
|
|
if seq != 1001 {
|
|
t.Fatalf("Message should have been sequence 1001 but was %d", seq)
|
|
}
|
|
|
|
var state StreamState
|
|
fs.FastState(&state)
|
|
switch {
|
|
case state.Msgs != 2:
|
|
t.Fatalf("Expected 2 messages, got %d", state.Msgs)
|
|
case state.FirstSeq != 1000:
|
|
t.Fatalf("Expected first seq 1000, got %d", state.FirstSeq)
|
|
case state.LastSeq != 1001:
|
|
t.Fatalf("Expected last seq 1001, got %d", state.LastSeq)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreRecaluclateFirstForSubjBug(t *testing.T) {
|
|
fs, err := newFileStore(FileStoreConfig{StoreDir: t.TempDir()}, StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.StoreMsg("foo", nil, nil, 0) // 1
|
|
fs.StoreMsg("bar", nil, nil, 0) // 2
|
|
fs.StoreMsg("foo", nil, nil, 0) // 3
|
|
|
|
// Now remove first 2..
|
|
fs.RemoveMsg(1)
|
|
fs.RemoveMsg(2)
|
|
|
|
// Now grab first (and only) block.
|
|
fs.mu.RLock()
|
|
mb := fs.blks[0]
|
|
fs.mu.RUnlock()
|
|
|
|
// Since we lazy update the first, simulate that we have not updated it as of yet.
|
|
ss := &SimpleState{Msgs: 1, First: 1, Last: 3, firstNeedsUpdate: true}
|
|
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
|
|
// Flush the cache.
|
|
mb.clearCacheAndOffset()
|
|
// Now call with start sequence of 1, the old one
|
|
// This will panic without the fix.
|
|
mb.recalculateForSubj("foo", ss)
|
|
// Make sure it was update properly.
|
|
require_True(t, *ss == SimpleState{Msgs: 1, First: 3, Last: 3, firstNeedsUpdate: false})
|
|
}
|
|
|
|
func TestFileStoreKeepWithDeletedMsgsBug(t *testing.T) {
|
|
fs, err := newFileStore(FileStoreConfig{StoreDir: t.TempDir()}, StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := bytes.Repeat([]byte("A"), 19)
|
|
for i := 0; i < 5; i++ {
|
|
fs.StoreMsg("A", nil, msg, 0)
|
|
fs.StoreMsg("B", nil, msg, 0)
|
|
}
|
|
|
|
n, err := fs.PurgeEx("A", 0, 0)
|
|
require_NoError(t, err)
|
|
require_True(t, n == 5)
|
|
|
|
// Purge with keep.
|
|
n, err = fs.PurgeEx(_EMPTY_, 0, 2)
|
|
require_NoError(t, err)
|
|
require_True(t, n == 3)
|
|
}
|
|
|
|
func TestFileStoreRestartWithExpireAndLockingBug(t *testing.T) {
|
|
sd := t.TempDir()
|
|
scfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage}
|
|
fs, err := newFileStore(FileStoreConfig{StoreDir: sd}, scfg)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// 20 total
|
|
msg := []byte("HELLO WORLD")
|
|
for i := 0; i < 10; i++ {
|
|
fs.StoreMsg("A", nil, msg, 0)
|
|
fs.StoreMsg("B", nil, msg, 0)
|
|
}
|
|
fs.Stop()
|
|
|
|
// Now change config underneath of so we will do expires at startup.
|
|
scfg.MaxMsgs = 15
|
|
scfg.MaxMsgsPer = 2
|
|
newCfg := FileStreamInfo{Created: fs.cfg.Created, StreamConfig: scfg}
|
|
|
|
// Replace
|
|
fs.cfg = newCfg
|
|
require_NoError(t, fs.writeStreamMeta())
|
|
|
|
fs, err = newFileStore(FileStoreConfig{StoreDir: sd}, scfg)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
}
|
|
|
|
// Test that loads from lmb under lots of writes do not return errPartialCache.
|
|
func TestFileStoreErrPartialLoad(t *testing.T) {
|
|
fs, err := newFileStore(FileStoreConfig{StoreDir: t.TempDir()}, StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
put := func(num int) {
|
|
for i := 0; i < num; i++ {
|
|
fs.StoreMsg("Z", nil, []byte("ZZZZZZZZZZZZZ"), 0)
|
|
}
|
|
}
|
|
|
|
put(100)
|
|
|
|
// Dump cache of lmb.
|
|
clearCache := func() {
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
lmb.mu.Lock()
|
|
lmb.clearCache()
|
|
lmb.mu.Unlock()
|
|
}
|
|
clearCache()
|
|
|
|
qch := make(chan struct{})
|
|
defer close(qch)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
go func() {
|
|
for {
|
|
select {
|
|
case <-qch:
|
|
return
|
|
default:
|
|
put(5)
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
var smv StoreMsg
|
|
for i := 0; i < 10_000; i++ {
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
lmb.mu.Lock()
|
|
first, last := fs.lmb.first.seq, fs.lmb.last.seq
|
|
if i%100 == 0 {
|
|
lmb.clearCache()
|
|
}
|
|
lmb.mu.Unlock()
|
|
|
|
if spread := int(last - first); spread > 0 {
|
|
seq := first + uint64(rand.Intn(spread))
|
|
_, err = fs.LoadMsg(seq, &smv)
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFileStoreErrPartialLoadOnSyncClose(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir(), BlockSize: 500},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage},
|
|
)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// This yields an internal record length of 50 bytes. So 10 msgs per blk.
|
|
msgLen := 19
|
|
msg := bytes.Repeat([]byte("A"), msgLen)
|
|
|
|
// Load up half the block.
|
|
for _, subj := range []string{"A", "B", "C", "D", "E"} {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
// Now simulate the sync timer closing the last block.
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
require_True(t, lmb != nil)
|
|
|
|
lmb.mu.Lock()
|
|
lmb.tryExpireCacheLocked()
|
|
lmb.dirtyCloseWithRemove(false)
|
|
lmb.mu.Unlock()
|
|
|
|
fs.StoreMsg("Z", nil, msg, 0)
|
|
_, err = fs.LoadMsg(1, nil)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
func TestFileStoreSyncIntervals(t *testing.T) {
|
|
fcfg := FileStoreConfig{StoreDir: t.TempDir(), SyncInterval: 250 * time.Millisecond}
|
|
fs, err := newFileStore(fcfg, StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
checkSyncFlag := func(expected bool) {
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
lmb.mu.RLock()
|
|
syncNeeded := lmb.needSync
|
|
lmb.mu.RUnlock()
|
|
if syncNeeded != expected {
|
|
t.Fatalf("Expected needSync to be %v", expected)
|
|
}
|
|
}
|
|
|
|
checkSyncFlag(false)
|
|
fs.StoreMsg("Z", nil, []byte("hello"), 0)
|
|
checkSyncFlag(true)
|
|
time.Sleep(400 * time.Millisecond)
|
|
checkSyncFlag(false)
|
|
fs.Stop()
|
|
|
|
// Now check always
|
|
fcfg.SyncInterval = 10 * time.Second
|
|
fcfg.SyncAlways = true
|
|
fs, err = newFileStore(fcfg, StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
checkSyncFlag(false)
|
|
fs.StoreMsg("Z", nil, []byte("hello"), 0)
|
|
checkSyncFlag(false)
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/4529
|
|
// Run this wuth --race and you will see the unlocked access that probably caused this.
|
|
func TestFileStoreRecalcFirstSequenceBug(t *testing.T) {
|
|
fcfg := FileStoreConfig{StoreDir: t.TempDir()}
|
|
fs, err := newFileStore(fcfg, StreamConfig{Name: "zzz", Subjects: []string{"*"}, MaxMsgsPer: 2, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := bytes.Repeat([]byte("A"), 22)
|
|
|
|
for _, subj := range []string{"A", "A", "B", "B"} {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
// Make sure the buffer is cleared.
|
|
clearLMBCache := func() {
|
|
fs.mu.RLock()
|
|
mb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
mb.mu.Lock()
|
|
mb.clearCacheAndOffset()
|
|
mb.mu.Unlock()
|
|
}
|
|
|
|
clearLMBCache()
|
|
|
|
// Do first here.
|
|
fs.StoreMsg("A", nil, msg, 0)
|
|
|
|
var wg sync.WaitGroup
|
|
start := make(chan bool)
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
<-start
|
|
for i := 0; i < 1_000; i++ {
|
|
fs.LoadLastMsg("A", nil)
|
|
clearLMBCache()
|
|
}
|
|
}()
|
|
|
|
wg.Add(1)
|
|
go func() {
|
|
defer wg.Done()
|
|
<-start
|
|
for i := 0; i < 1_000; i++ {
|
|
fs.StoreMsg("A", nil, msg, 0)
|
|
}
|
|
}()
|
|
|
|
close(start)
|
|
wg.Wait()
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// New WAL based architecture tests
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
func TestFileStoreFullStateBasics(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 100
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// This yields an internal record length of 50 bytes. So 2 msgs per blk.
|
|
subj, msgLen, recLen := "A", 19, uint64(50)
|
|
msgA := bytes.Repeat([]byte("A"), msgLen)
|
|
msgZ := bytes.Repeat([]byte("Z"), msgLen)
|
|
|
|
// Send 2 msgs and stop, check for presence of our full state file.
|
|
_, _, err = fs.StoreMsg(subj, nil, msgA, 0)
|
|
require_NoError(t, err)
|
|
_, _, err = fs.StoreMsg(subj, nil, msgZ, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
|
|
// Make sure there is a full state file after we do a stop.
|
|
require_NoError(t, fs.Stop())
|
|
|
|
sfile := filepath.Join(fcfg.StoreDir, msgDir, streamStreamStateFile)
|
|
if _, err := os.Stat(sfile); err != nil {
|
|
t.Fatalf("Expected stream state file but got %v", err)
|
|
}
|
|
|
|
// Read it in and make sure len > 0.
|
|
buf, err := os.ReadFile(sfile)
|
|
require_NoError(t, err)
|
|
require_True(t, len(buf) > 0)
|
|
|
|
// Now make sure we recover properly.
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Make sure there are no old idx or fss files.
|
|
matches, err := filepath.Glob(filepath.Join(fcfg.StoreDir, msgDir, "%d.fss"))
|
|
require_NoError(t, err)
|
|
require_Equal(t, len(matches), 0)
|
|
matches, err = filepath.Glob(filepath.Join(fcfg.StoreDir, msgDir, "%d.idx"))
|
|
require_NoError(t, err)
|
|
require_Equal(t, len(matches), 0)
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, 2)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 2)
|
|
|
|
// Now make sure we can read in values.
|
|
var smv StoreMsg
|
|
sm, err := fs.LoadMsg(1, &smv)
|
|
require_NoError(t, err)
|
|
require_True(t, bytes.Equal(sm.msg, msgA))
|
|
|
|
sm, err = fs.LoadMsg(2, &smv)
|
|
require_NoError(t, err)
|
|
require_True(t, bytes.Equal(sm.msg, msgZ))
|
|
|
|
// Now add in 1 more here to split the lmb.
|
|
_, _, err = fs.StoreMsg(subj, nil, msgZ, 0)
|
|
require_NoError(t, err)
|
|
|
|
// Now stop the filestore and replace the old stream state and make sure we recover correctly.
|
|
require_NoError(t, fs.Stop())
|
|
|
|
// Regrab the stream state
|
|
buf, err = os.ReadFile(sfile)
|
|
require_NoError(t, err)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Add in one more.
|
|
_, _, err = fs.StoreMsg(subj, nil, msgZ, 0)
|
|
require_NoError(t, err)
|
|
require_NoError(t, fs.Stop())
|
|
|
|
// Put old stream state back with only 3.
|
|
err = os.WriteFile(sfile, buf, defaultFilePerms)
|
|
require_NoError(t, err)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state = fs.State()
|
|
require_Equal(t, state.Msgs, 4)
|
|
require_Equal(t, state.Bytes, 4*recLen)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 4)
|
|
require_Equal(t, fs.numMsgBlocks(), 2)
|
|
|
|
// Make sure we are tracking subjects correctly.
|
|
fs.mu.RLock()
|
|
info, _ := fs.psim.Find(stringToBytes(subj))
|
|
psi := *info
|
|
fs.mu.RUnlock()
|
|
|
|
require_Equal(t, psi.total, 4)
|
|
require_Equal(t, psi.fblk, 1)
|
|
require_Equal(t, psi.lblk, 2)
|
|
|
|
// Store 1 more
|
|
_, _, err = fs.StoreMsg(subj, nil, msgA, 0)
|
|
require_NoError(t, err)
|
|
require_NoError(t, fs.Stop())
|
|
// Put old stream state back with only 3.
|
|
err = os.WriteFile(sfile, buf, defaultFilePerms)
|
|
require_NoError(t, err)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
state = fs.State()
|
|
require_Equal(t, state.Msgs, 5)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 5)
|
|
require_Equal(t, fs.numMsgBlocks(), 3)
|
|
// Make sure we are tracking subjects correctly.
|
|
fs.mu.RLock()
|
|
info, _ = fs.psim.Find(stringToBytes(subj))
|
|
psi = *info
|
|
fs.mu.RUnlock()
|
|
require_Equal(t, psi.total, 5)
|
|
require_Equal(t, psi.fblk, 1)
|
|
require_Equal(t, psi.lblk, 3)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreFullStatePurge(t *testing.T) {
|
|
testFileStoreFullStatePurge(t, false)
|
|
}
|
|
|
|
func TestFileStoreFullStatePurgeFullRecovery(t *testing.T) {
|
|
testFileStoreFullStatePurge(t, true)
|
|
}
|
|
|
|
func testFileStoreFullStatePurge(t *testing.T, checkFullRecovery bool) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 132 // Leave room for tombstones.
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// This yields an internal record length of 50 bytes. So 2 msgs per blk.
|
|
subj, msg := "A", bytes.Repeat([]byte("A"), 19)
|
|
|
|
// Should be 2 per block, so 5 blocks.
|
|
for i := 0; i < 10; i++ {
|
|
_, _, err = fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
n, err := fs.Purge()
|
|
require_NoError(t, err)
|
|
require_Equal(t, n, 10)
|
|
state := fs.State()
|
|
require_NoError(t, fs.Stop())
|
|
if checkFullRecovery {
|
|
require_NoError(t, os.Remove(filepath.Join(fcfg.StoreDir, msgDir, streamStreamStateFile)))
|
|
}
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state after purge does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
|
|
if checkFullRecovery {
|
|
fs.rebuildState(nil)
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state after purge does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
}
|
|
|
|
// Add in more 10 more total, some B some C.
|
|
for i := 0; i < 5; i++ {
|
|
_, _, err = fs.StoreMsg("B", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
_, _, err = fs.StoreMsg("C", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
n, err = fs.PurgeEx("B", 0, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, n, 5)
|
|
|
|
state = fs.State()
|
|
require_NoError(t, fs.Stop())
|
|
if checkFullRecovery {
|
|
require_NoError(t, os.Remove(filepath.Join(fcfg.StoreDir, msgDir, streamStreamStateFile)))
|
|
}
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state after purge does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
|
|
if checkFullRecovery {
|
|
fs.rebuildState(nil)
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state after purge does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
}
|
|
|
|
// Purge with keep.
|
|
n, err = fs.PurgeEx(_EMPTY_, 0, 2)
|
|
require_NoError(t, err)
|
|
require_Equal(t, n, 3)
|
|
|
|
state = fs.State()
|
|
|
|
// Do some quick checks here, keep had a bug.
|
|
require_Equal(t, state.Msgs, 2)
|
|
require_Equal(t, state.FirstSeq, 18)
|
|
require_Equal(t, state.LastSeq, 20)
|
|
|
|
require_NoError(t, fs.Stop())
|
|
if checkFullRecovery {
|
|
require_NoError(t, os.Remove(filepath.Join(fcfg.StoreDir, msgDir, streamStreamStateFile)))
|
|
}
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state after purge does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
|
|
if checkFullRecovery {
|
|
fs.rebuildState(nil)
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state after purge does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
}
|
|
|
|
// Make sure we can survive a purge with no full stream state and have the correct first sequence.
|
|
// This used to be provided by the idx file and is now tombstones and the full stream state snapshot.
|
|
n, err = fs.Purge()
|
|
require_NoError(t, err)
|
|
require_Equal(t, n, 2)
|
|
state = fs.State()
|
|
require_NoError(t, fs.Stop())
|
|
if checkFullRecovery {
|
|
require_NoError(t, os.Remove(filepath.Join(fcfg.StoreDir, msgDir, streamStreamStateFile)))
|
|
}
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state after purge does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
|
|
if checkFullRecovery {
|
|
fs.rebuildState(nil)
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state after purge does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreFullStateTestUserRemoveWAL(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 132 // Leave room for tombstones.
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// This yields an internal record length of 50 bytes. So 2 msgs per blk.
|
|
msgLen := 19
|
|
msgA := bytes.Repeat([]byte("A"), msgLen)
|
|
msgZ := bytes.Repeat([]byte("Z"), msgLen)
|
|
|
|
// Store 2 msgs and delete first.
|
|
fs.StoreMsg("A", nil, msgA, 0)
|
|
fs.StoreMsg("Z", nil, msgZ, 0)
|
|
fs.RemoveMsg(1)
|
|
|
|
// Check we can load things properly since the block will have a tombstone now for seq 1.
|
|
sm, err := fs.LoadMsg(2, nil)
|
|
require_NoError(t, err)
|
|
require_True(t, bytes.Equal(sm.msg, msgZ))
|
|
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
state := fs.State()
|
|
fs.Stop()
|
|
|
|
// Grab the state from this stop.
|
|
sfile := filepath.Join(fcfg.StoreDir, msgDir, streamStreamStateFile)
|
|
buf, err := os.ReadFile(sfile)
|
|
require_NoError(t, err)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Check we can load things properly since the block will have a tombstone now for seq 1.
|
|
_, err = fs.LoadMsg(2, nil)
|
|
require_NoError(t, err)
|
|
_, err = fs.LoadMsg(1, nil)
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
require_True(t, !state.FirstTime.IsZero())
|
|
|
|
// Store 2 more msgs and delete 2 & 4.
|
|
fs.StoreMsg("A", nil, msgA, 0)
|
|
fs.StoreMsg("Z", nil, msgZ, 0)
|
|
fs.RemoveMsg(2)
|
|
fs.RemoveMsg(4)
|
|
|
|
state = fs.State()
|
|
require_Equal(t, len(state.Deleted), state.NumDeleted)
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
require_True(t, !state.FirstTime.IsZero())
|
|
|
|
// Now close again and put back old stream state.
|
|
// This will test that we can remember user deletes by placing tombstones in the lmb/wal.
|
|
fs.Stop()
|
|
err = os.WriteFile(sfile, buf, defaultFilePerms)
|
|
require_NoError(t, err)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
newState := fs.State()
|
|
// We will properly detect lost data for sequence #2 here.
|
|
require_True(t, newState.Lost != nil)
|
|
require_Equal(t, len(newState.Lost.Msgs), 1)
|
|
require_Equal(t, newState.Lost.Msgs[0], 2)
|
|
// Clear for deep equal compare below.
|
|
newState.Lost = nil
|
|
|
|
if !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
require_True(t, !state.FirstTime.IsZero())
|
|
})
|
|
}
|
|
|
|
func TestFileStoreFullStateTestSysRemovals(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 100
|
|
cfg := StreamConfig{
|
|
Name: "zzz",
|
|
Subjects: []string{"*"},
|
|
MaxMsgs: 10,
|
|
MaxMsgsPer: 1,
|
|
Storage: FileStorage,
|
|
}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// This yields an internal record length of 50 bytes. So 2 msgs per blk.
|
|
msgLen := 19
|
|
msg := bytes.Repeat([]byte("A"), msgLen)
|
|
|
|
for _, subj := range []string{"A", "B", "A", "B"} {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, 2)
|
|
require_Equal(t, state.FirstSeq, 3)
|
|
require_Equal(t, state.LastSeq, 4)
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state after purge does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
|
|
for _, subj := range []string{"C", "D", "E", "F", "G", "H", "I", "J"} {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
state = fs.State()
|
|
require_Equal(t, state.Msgs, 10)
|
|
require_Equal(t, state.FirstSeq, 3)
|
|
require_Equal(t, state.LastSeq, 12)
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state after purge does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
|
|
// Goes over limit
|
|
fs.StoreMsg("ZZZ", nil, msg, 0)
|
|
|
|
state = fs.State()
|
|
require_Equal(t, state.Msgs, 10)
|
|
require_Equal(t, state.FirstSeq, 4)
|
|
require_Equal(t, state.LastSeq, 13)
|
|
fs.Stop()
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state after purge does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreSelectBlockWithFirstSeqRemovals(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 100
|
|
cfg := StreamConfig{
|
|
Name: "zzz",
|
|
Subjects: []string{"*"},
|
|
MaxMsgsPer: 1,
|
|
Storage: FileStorage,
|
|
}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// This yields an internal record length of 50 bytes. So 2 msgs per blk.
|
|
msgLen := 19
|
|
msg := bytes.Repeat([]byte("A"), msgLen)
|
|
|
|
subjects := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz+@$^"
|
|
// We need over 32 blocks to kick in binary search. So 32*2+1 (65) msgs to get 33 blocks.
|
|
for i := 0; i < 32*2+1; i++ {
|
|
subj := string(subjects[i])
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
require_Equal(t, fs.numMsgBlocks(), 33)
|
|
|
|
// Now we want to delete the first msg of each block to move the first sequence.
|
|
// Want to do this via system removes, not user initiated moves.
|
|
for i := 0; i < len(subjects); i += 2 {
|
|
subj := string(subjects[i])
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
|
|
// We want to make sure that select always returns an index and a non-nil mb.
|
|
for seq := ss.FirstSeq; seq <= ss.LastSeq; seq++ {
|
|
fs.mu.RLock()
|
|
index, mb := fs.selectMsgBlockWithIndex(seq)
|
|
fs.mu.RUnlock()
|
|
require_True(t, index >= 0)
|
|
require_True(t, mb != nil)
|
|
require_Equal(t, (seq-1)/2, uint64(index))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMsgBlockHolesAndIndexing(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage, MaxMsgsPer: 1},
|
|
)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Grab the message block by hand and manipulate at that level.
|
|
mb := fs.getFirstBlock()
|
|
writeMsg := func(subj string, seq uint64) {
|
|
rl := fileStoreMsgSize(subj, nil, []byte(subj))
|
|
require_NoError(t, mb.writeMsgRecord(rl, seq, subj, nil, []byte(subj), time.Now().UnixNano(), true))
|
|
fs.rebuildState(nil)
|
|
}
|
|
readMsg := func(seq uint64, expectedSubj string) {
|
|
// Clear cache so we load back in from disk and need to properly process any holes.
|
|
ld, tombs, err := mb.rebuildState()
|
|
require_NoError(t, err)
|
|
require_Equal(t, ld, nil)
|
|
require_Equal(t, len(tombs), 0)
|
|
fs.rebuildState(nil)
|
|
sm, _, err := mb.fetchMsg(seq, nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, sm.subj, expectedSubj)
|
|
require_True(t, bytes.Equal(sm.buf[:len(expectedSubj)], []byte(expectedSubj)))
|
|
}
|
|
|
|
writeMsg("A", 2)
|
|
require_Equal(t, mb.first.seq, 2)
|
|
require_Equal(t, mb.last.seq, 2)
|
|
|
|
writeMsg("B", 4)
|
|
require_Equal(t, mb.first.seq, 2)
|
|
require_Equal(t, mb.last.seq, 4)
|
|
|
|
writeMsg("C", 12)
|
|
|
|
readMsg(4, "B")
|
|
require_True(t, mb.dmap.Exists(3))
|
|
|
|
readMsg(12, "C")
|
|
readMsg(2, "A")
|
|
|
|
// Check that we get deleted for the right ones etc.
|
|
checkDeleted := func(seq uint64) {
|
|
_, _, err = mb.fetchMsg(seq, nil)
|
|
require_Error(t, err, ErrStoreMsgNotFound, errDeletedMsg)
|
|
mb.mu.RLock()
|
|
shouldExist, exists := seq >= mb.first.seq, mb.dmap.Exists(seq)
|
|
mb.mu.RUnlock()
|
|
if shouldExist {
|
|
require_True(t, exists)
|
|
}
|
|
}
|
|
checkDeleted(1)
|
|
checkDeleted(3)
|
|
for seq := 5; seq < 12; seq++ {
|
|
checkDeleted(uint64(seq))
|
|
}
|
|
}
|
|
|
|
func TestFileStoreMsgBlockCompactionAndHoles(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage, MaxMsgsPer: 1},
|
|
)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := bytes.Repeat([]byte("Z"), 1024)
|
|
for _, subj := range []string{"A", "B", "C", "D", "E", "F", "G", "H", "I", "J"} {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
// Leave first one but delete the rest.
|
|
for seq := uint64(2); seq < 10; seq++ {
|
|
fs.RemoveMsg(seq)
|
|
}
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
mb := fs.getFirstBlock()
|
|
require_NotNil(t, mb)
|
|
|
|
_, ub, _ := fs.Utilization()
|
|
|
|
// Do compaction, should remove all excess now.
|
|
mb.mu.Lock()
|
|
err = mb.compact()
|
|
mb.mu.Unlock()
|
|
require_NoError(t, err)
|
|
|
|
ta, ua, _ := fs.Utilization()
|
|
require_Equal(t, ub, ua)
|
|
require_Equal(t, ta, ua)
|
|
}
|
|
|
|
func TestFileStoreRemoveLastNoDoubleTombstones(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage, MaxMsgsPer: 1},
|
|
)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.StoreMsg("A", nil, []byte("hello"), 0)
|
|
fs.mu.Lock()
|
|
fs.removeMsgViaLimits(1)
|
|
fs.mu.Unlock()
|
|
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
mb := fs.getFirstBlock()
|
|
require_NotNil(t, mb)
|
|
mb.loadMsgs()
|
|
rbytes, _, err := fs.Utilization()
|
|
require_NoError(t, err)
|
|
require_Equal(t, rbytes, emptyRecordLen)
|
|
}
|
|
|
|
func TestFileStoreFullStateMultiBlockPastWAL(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 100
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// This yields an internal record length of 50 bytes. So 2 msgs per blk.
|
|
msgLen := 19
|
|
msgA := bytes.Repeat([]byte("A"), msgLen)
|
|
msgZ := bytes.Repeat([]byte("Z"), msgLen)
|
|
|
|
// Store 2 msgs
|
|
fs.StoreMsg("A", nil, msgA, 0)
|
|
fs.StoreMsg("B", nil, msgZ, 0)
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
fs.Stop()
|
|
|
|
// Grab the state from this stop.
|
|
sfile := filepath.Join(fcfg.StoreDir, msgDir, streamStreamStateFile)
|
|
buf, err := os.ReadFile(sfile)
|
|
require_NoError(t, err)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Store 6 more msgs.
|
|
fs.StoreMsg("C", nil, msgA, 0)
|
|
fs.StoreMsg("D", nil, msgZ, 0)
|
|
fs.StoreMsg("E", nil, msgA, 0)
|
|
fs.StoreMsg("F", nil, msgZ, 0)
|
|
fs.StoreMsg("G", nil, msgA, 0)
|
|
fs.StoreMsg("H", nil, msgZ, 0)
|
|
require_Equal(t, fs.numMsgBlocks(), 4)
|
|
state := fs.State()
|
|
fs.Stop()
|
|
|
|
// Put back old stream state.
|
|
// This will test that we properly walk multiple blocks past where we snapshotted state.
|
|
err = os.WriteFile(sfile, buf, defaultFilePerms)
|
|
require_NoError(t, err)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
require_True(t, !state.FirstTime.IsZero())
|
|
})
|
|
}
|
|
|
|
// This tests we can successfully recover without having to rebuild the whole stream from a mid block index.db marker
|
|
// when the updated block has a removed entry.
|
|
// Make sure this does not cause a recover of the full state.
|
|
func TestFileStoreFullStateMidBlockPastWAL(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage, MaxMsgsPer: 1}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// This yields an internal record length of 50 bytes. So 2 msgs per blk.
|
|
msg := bytes.Repeat([]byte("Z"), 19)
|
|
|
|
// Store 5 msgs
|
|
fs.StoreMsg("A", nil, msg, 0)
|
|
fs.StoreMsg("B", nil, msg, 0)
|
|
fs.StoreMsg("C", nil, msg, 0)
|
|
fs.StoreMsg("D", nil, msg, 0)
|
|
fs.StoreMsg("E", nil, msg, 0)
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
fs.Stop()
|
|
|
|
// Grab the state from this stop.
|
|
sfile := filepath.Join(fcfg.StoreDir, msgDir, streamStreamStateFile)
|
|
buf, err := os.ReadFile(sfile)
|
|
require_NoError(t, err)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Store 5 more messages, then remove seq 2, "B".
|
|
fs.StoreMsg("F", nil, msg, 0)
|
|
fs.StoreMsg("G", nil, msg, 0)
|
|
fs.StoreMsg("H", nil, msg, 0)
|
|
fs.StoreMsg("I", nil, msg, 0)
|
|
fs.StoreMsg("J", nil, msg, 0)
|
|
fs.RemoveMsg(2)
|
|
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
state := fs.State()
|
|
fs.Stop()
|
|
|
|
// Put back old stream state.
|
|
// This will test that we properly walk multiple blocks past where we snapshotted state.
|
|
err = os.WriteFile(sfile, buf, defaultFilePerms)
|
|
require_NoError(t, err)
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if newState := fs.State(); !reflect.DeepEqual(state, newState) {
|
|
t.Fatalf("Restore state does not match:\n%+v\n%+v",
|
|
state, newState)
|
|
}
|
|
// Check that index.db is still there. If we recover by raw data on a corrupt state we delete this.
|
|
_, err = os.Stat(sfile)
|
|
require_NoError(t, err)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreCompactingBlocksOnSync(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 1000 // 20 msgs per block.
|
|
fcfg.SyncInterval = 100 * time.Millisecond
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage, MaxMsgsPer: 1}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// This yields an internal record length of 50 bytes. So 20 msgs per blk.
|
|
msg := bytes.Repeat([]byte("Z"), 19)
|
|
subjects := "ABCDEFGHIJKLMNOPQRST"
|
|
for _, subj := range subjects {
|
|
fs.StoreMsg(string(subj), nil, msg, 0)
|
|
}
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
total, reported, err := fs.Utilization()
|
|
require_NoError(t, err)
|
|
|
|
require_Equal(t, total, reported)
|
|
|
|
// Now start removing, since we are small this should not kick in any inline logic.
|
|
// Remove all interior messages, leave 1 and 20. So write B-S
|
|
for i := 1; i < 19; i++ {
|
|
fs.StoreMsg(string(subjects[i]), nil, msg, 0)
|
|
}
|
|
require_Equal(t, fs.numMsgBlocks(), 2)
|
|
|
|
blkUtil := func() (uint64, uint64) {
|
|
fs.mu.RLock()
|
|
fmb := fs.blks[0]
|
|
fs.mu.RUnlock()
|
|
fmb.mu.RLock()
|
|
defer fmb.mu.RUnlock()
|
|
return fmb.rbytes, fmb.bytes
|
|
}
|
|
|
|
total, reported = blkUtil()
|
|
require_Equal(t, reported, 100)
|
|
// Raw bytes will be 1000, but due to compression could be less.
|
|
if fcfg.Compression != NoCompression {
|
|
require_True(t, total > reported)
|
|
} else {
|
|
require_Equal(t, total, 1000)
|
|
}
|
|
|
|
// Make sure the sync interval when kicked in compacts down to rbytes == 100.
|
|
checkFor(t, time.Second, 100*time.Millisecond, func() error {
|
|
if total, reported := blkUtil(); total <= reported {
|
|
return nil
|
|
}
|
|
return fmt.Errorf("Not compacted yet, raw %v vs reported %v",
|
|
friendlyBytes(total), friendlyBytes(reported))
|
|
})
|
|
})
|
|
}
|
|
|
|
// Make sure a call to Compact() updates PSIM correctly.
|
|
func TestFileStoreCompactAndPSIMWhenDeletingBlocks(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir(), BlockSize: 512},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "A", bytes.Repeat([]byte("ABC"), 33) // ~100bytes
|
|
|
|
// Add in 10 As
|
|
for i := 0; i < 10; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
require_Equal(t, fs.numMsgBlocks(), 4)
|
|
|
|
// Should leave 1.
|
|
n, err := fs.Compact(10)
|
|
require_NoError(t, err)
|
|
require_Equal(t, n, 9)
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
|
|
fs.mu.RLock()
|
|
info, _ := fs.psim.Find(stringToBytes(subj))
|
|
psi := *info
|
|
fs.mu.RUnlock()
|
|
|
|
// PSIM remains the same, since we'll not always know
|
|
// that the head was removed, instead of the tail.
|
|
require_Equal(t, psi.total, 1)
|
|
require_Equal(t, psi.fblk, 1)
|
|
require_Equal(t, psi.lblk, 4)
|
|
}
|
|
|
|
func TestFileStoreTrackSubjLenForPSIM(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{">"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Place 1000 msgs with varying subjects.
|
|
// Make sure we track the subject length properly.
|
|
smap := make(map[string]int, 1000)
|
|
buf := make([]byte, 10)
|
|
for i := 0; i < 1000; i++ {
|
|
var b strings.Builder
|
|
// 1-6 tokens.
|
|
numTokens := rand.Intn(6) + 1
|
|
for i := 0; i < numTokens; i++ {
|
|
tlen := rand.Intn(4) + 2
|
|
tok := buf[:tlen]
|
|
crand.Read(tok)
|
|
b.WriteString(hex.EncodeToString(tok))
|
|
if i != numTokens-1 {
|
|
b.WriteString(".")
|
|
}
|
|
}
|
|
subj := b.String()
|
|
// Avoid dupes since will cause check to fail after we delete messages.
|
|
if _, ok := smap[subj]; ok {
|
|
continue
|
|
}
|
|
smap[subj] = len(subj)
|
|
fs.StoreMsg(subj, nil, nil, 0)
|
|
}
|
|
|
|
check := func() {
|
|
t.Helper()
|
|
var total int
|
|
for _, slen := range smap {
|
|
total += slen
|
|
}
|
|
fs.mu.RLock()
|
|
tsl := fs.tsl
|
|
fs.mu.RUnlock()
|
|
require_Equal(t, tsl, total)
|
|
}
|
|
|
|
check()
|
|
|
|
// Delete ~half
|
|
var smv StoreMsg
|
|
for i := 0; i < 500; i++ {
|
|
seq := uint64(rand.Intn(1000) + 1)
|
|
sm, err := fs.LoadMsg(seq, &smv)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
fs.RemoveMsg(seq)
|
|
delete(smap, sm.subj)
|
|
}
|
|
|
|
check()
|
|
|
|
// Make sure we can recover same after restart.
|
|
fs.Stop()
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{">"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
check()
|
|
|
|
// Drain the rest through purge.
|
|
fs.Purge()
|
|
smap = nil
|
|
check()
|
|
}
|
|
|
|
// This was used to make sure our estimate was correct, but not needed normally.
|
|
func TestFileStoreLargeFullStatePSIM(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{">"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
buf := make([]byte, 20)
|
|
for i := 0; i < 100_000; i++ {
|
|
var b strings.Builder
|
|
// 1-6 tokens.
|
|
numTokens := rand.Intn(6) + 1
|
|
for i := 0; i < numTokens; i++ {
|
|
tlen := rand.Intn(8) + 2
|
|
tok := buf[:tlen]
|
|
crand.Read(tok)
|
|
b.WriteString(hex.EncodeToString(tok))
|
|
if i != numTokens-1 {
|
|
b.WriteString(".")
|
|
}
|
|
}
|
|
subj := b.String()
|
|
fs.StoreMsg(subj, nil, nil, 0)
|
|
}
|
|
fs.Stop()
|
|
}
|
|
|
|
func TestFileStoreLargeFullStateMetaCleanup(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{">"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo.bar.baz", bytes.Repeat([]byte("ABC"), 33) // ~100bytes
|
|
for i := 0; i < 1000; i++ {
|
|
fs.StoreMsg(subj, nil, nil, 0)
|
|
}
|
|
fs.Stop()
|
|
|
|
mdir := filepath.Join(sd, msgDir)
|
|
idxFile := filepath.Join(mdir, "1.idx")
|
|
fssFile := filepath.Join(mdir, "1.fss")
|
|
require_NoError(t, os.WriteFile(idxFile, msg, defaultFilePerms))
|
|
require_NoError(t, os.WriteFile(fssFile, msg, defaultFilePerms))
|
|
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{">"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
checkFor(t, time.Second, 50*time.Millisecond, func() error {
|
|
if _, err := os.Stat(idxFile); err == nil {
|
|
return errors.New("idx file still exists")
|
|
}
|
|
if _, err := os.Stat(fssFile); err == nil {
|
|
return errors.New("fss file still exists")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestFileStoreIndexDBExistsAfterShutdown(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{">"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj := "foo.bar.baz"
|
|
for i := 0; i < 1000; i++ {
|
|
fs.StoreMsg(subj, nil, nil, 0)
|
|
}
|
|
|
|
idxFile := filepath.Join(sd, msgDir, streamStreamStateFile)
|
|
|
|
fs.mu.Lock()
|
|
fs.dirty = 1
|
|
if err := os.Remove(idxFile); err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
t.Fatal(err)
|
|
}
|
|
fs.mu.Unlock()
|
|
|
|
fs.Stop()
|
|
|
|
checkFor(t, time.Second, 50*time.Millisecond, func() error {
|
|
if _, err := os.Stat(idxFile); err != nil {
|
|
return fmt.Errorf("%q doesn't exist", idxFile)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/4842
|
|
func TestFileStoreSubjectCorruption(t *testing.T) {
|
|
sd, blkSize := t.TempDir(), uint64(2*1024*1024)
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: blkSize},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
numSubjects := 100
|
|
msgs := [][]byte{bytes.Repeat([]byte("ABC"), 333), bytes.Repeat([]byte("ABC"), 888), bytes.Repeat([]byte("ABC"), 555)}
|
|
for i := 0; i < 10_000; i++ {
|
|
subj := fmt.Sprintf("foo.%d", rand.Intn(numSubjects)+1)
|
|
msg := msgs[rand.Intn(len(msgs))]
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
fs.Stop()
|
|
|
|
require_NoError(t, os.Remove(filepath.Join(sd, msgDir, streamStreamStateFile)))
|
|
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: blkSize},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for subj := range fs.SubjectsTotals(">") {
|
|
var n int
|
|
_, err := fmt.Sscanf(subj, "foo.%d", &n)
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
|
|
// Since 2.10 we no longer have fss, and the approach for calculating NumPending would branch
|
|
// based on the old fss metadata being present. This meant that calculating NumPending in >= 2.10.x
|
|
// would load all blocks to complete. This test makes sure we do not do that anymore.
|
|
func TestFileStoreNumPendingLastBySubject(t *testing.T) {
|
|
sd, blkSize := t.TempDir(), uint64(1024)
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: blkSize},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
numSubjects := 20
|
|
msg := bytes.Repeat([]byte("ABC"), 25)
|
|
for i := 1; i <= 1000; i++ {
|
|
subj := fmt.Sprintf("foo.%d.%d", rand.Intn(numSubjects)+1, i)
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
// Each block has ~8 msgs.
|
|
require_True(t, fs.numMsgBlocks() > 100)
|
|
|
|
calcCacheLoads := func() (cloads uint64) {
|
|
fs.mu.RLock()
|
|
defer fs.mu.RUnlock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.RLock()
|
|
cloads += mb.cloads
|
|
mb.mu.RUnlock()
|
|
}
|
|
return cloads
|
|
}
|
|
|
|
total, _, err := fs.NumPending(0, "foo.*.*", true)
|
|
require_NoError(t, err)
|
|
require_Equal(t, total, 1000)
|
|
// Make sure no blocks were loaded to calculate this as a new consumer.
|
|
require_Equal(t, calcCacheLoads(), 0)
|
|
|
|
checkResult := func(sseq, np uint64, filter string) {
|
|
t.Helper()
|
|
var checkTotal uint64
|
|
var smv StoreMsg
|
|
for seq := sseq; seq <= 1000; seq++ {
|
|
sm, err := fs.LoadMsg(seq, &smv)
|
|
require_NoError(t, err)
|
|
if subjectIsSubsetMatch(sm.subj, filter) {
|
|
checkTotal++
|
|
}
|
|
}
|
|
require_Equal(t, np, checkTotal)
|
|
}
|
|
|
|
// Make sure partials work properly.
|
|
for _, filter := range []string{"foo.10.*", "*.22.*", "*.*.222", "foo.5.999", "*.2.*"} {
|
|
sseq := uint64(rand.Intn(250) + 200) // Between 200-450
|
|
total, _, err = fs.NumPending(sseq, filter, true)
|
|
require_NoError(t, err)
|
|
checkResult(sseq, total, filter)
|
|
}
|
|
}
|
|
|
|
// We had a bug that could cause internal memory corruption of the psim keys in memory
|
|
// which could have been written to disk via index.db.
|
|
func TestFileStoreCorruptPSIMOnDisk(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.StoreMsg("foo.bar", nil, []byte("ABC"), 0)
|
|
fs.StoreMsg("foo.baz", nil, []byte("XYZ"), 0)
|
|
|
|
// Force bad subject.
|
|
fs.mu.Lock()
|
|
psi, _ := fs.psim.Find(stringToBytes("foo.bar"))
|
|
bad := []rune("foo.bar")
|
|
bad[3] = utf8.RuneError
|
|
fs.psim.Insert(stringToBytes(string(bad)), *psi)
|
|
fs.psim.Delete(stringToBytes("foo.bar"))
|
|
fs.dirty++
|
|
fs.mu.Unlock()
|
|
|
|
// Restart
|
|
fs.Stop()
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
sm, err := fs.LoadLastMsg("foo.bar", nil)
|
|
require_NoError(t, err)
|
|
require_True(t, bytes.Equal(sm.msg, []byte("ABC")))
|
|
|
|
sm, err = fs.LoadLastMsg("foo.baz", nil)
|
|
require_NoError(t, err)
|
|
require_True(t, bytes.Equal(sm.msg, []byte("XYZ")))
|
|
}
|
|
|
|
func TestFileStorePurgeExBufPool(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 1024},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := bytes.Repeat([]byte("ABC"), 33) // ~100bytes
|
|
for i := 0; i < 1000; i++ {
|
|
fs.StoreMsg("foo.foo", nil, msg, 0)
|
|
fs.StoreMsg("foo.bar", nil, msg, 0)
|
|
}
|
|
|
|
p, err := fs.PurgeEx("foo.bar", 0, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, p, 1000)
|
|
|
|
// Now make sure we do not have all of the msg blocks cache's loaded.
|
|
var loaded int
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.RLock()
|
|
if mb.cacheAlreadyLoaded() {
|
|
loaded++
|
|
}
|
|
mb.mu.RUnlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
require_Equal(t, loaded, 1)
|
|
}
|
|
|
|
func TestFileStoreFSSMeta(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 100, CacheExpire: 200 * time.Millisecond, SubjectStateExpire: time.Second},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// This yields an internal record length of 50 bytes. So 2 msgs per blk with subject len of 1, e.g. "A" or "Z".
|
|
msg := bytes.Repeat([]byte("Z"), 19)
|
|
|
|
// Should leave us with |A-Z| |Z-Z| |Z-Z| |Z-A|
|
|
fs.StoreMsg("A", nil, msg, 0)
|
|
for i := 0; i < 6; i++ {
|
|
fs.StoreMsg("Z", nil, msg, 0)
|
|
}
|
|
fs.StoreMsg("A", nil, msg, 0)
|
|
|
|
// Let cache's expire before PurgeEx which will load them back in.
|
|
time.Sleep(500 * time.Millisecond)
|
|
|
|
p, err := fs.PurgeEx("A", 0, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, p, 2)
|
|
|
|
// Make sure cache is not loaded.
|
|
var stillHasCache bool
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.RLock()
|
|
stillHasCache = stillHasCache || mb.cacheAlreadyLoaded()
|
|
mb.mu.RUnlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
|
|
require_False(t, stillHasCache)
|
|
|
|
// Let fss expire via SubjectStateExpire.
|
|
time.Sleep(1500 * time.Millisecond)
|
|
|
|
var noFSS bool
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.RLock()
|
|
noFSS = noFSS || mb.fssNotLoaded()
|
|
mb.mu.RUnlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
|
|
require_True(t, noFSS)
|
|
}
|
|
|
|
func TestFileStoreExpireCacheOnLinearWalk(t *testing.T) {
|
|
sd := t.TempDir()
|
|
expire := 250 * time.Millisecond
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, CacheExpire: expire},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// This yields an internal record length of 50 bytes.
|
|
subj, msg := "Z", bytes.Repeat([]byte("Z"), 19)
|
|
|
|
// Store 10 messages, so 5 blocks.
|
|
for i := 0; i < 10; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
// Let them all expire. This way we load as we walk and can test that we expire all blocks without
|
|
// needing to worry about last write times blocking forced expiration.
|
|
time.Sleep(expire + ats.TickInterval*2)
|
|
|
|
checkNoCache := func() {
|
|
t.Helper()
|
|
fs.mu.RLock()
|
|
var stillHasCache bool
|
|
for _, mb := range fs.blks {
|
|
mb.mu.RLock()
|
|
stillHasCache = stillHasCache || mb.cacheAlreadyLoaded()
|
|
mb.mu.RUnlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
require_False(t, stillHasCache)
|
|
}
|
|
|
|
// Walk forward.
|
|
var smv StoreMsg
|
|
for seq := uint64(1); seq <= 10; seq++ {
|
|
_, err := fs.LoadMsg(seq, &smv)
|
|
require_NoError(t, err)
|
|
}
|
|
checkNoCache()
|
|
|
|
// No test walking backwards. We have this scenario when we search for starting points for sourced streams.
|
|
// Noticed some memory bloat when we have to search many blocks looking for a source that may be closer to the
|
|
// beginning of the stream (infrequently updated sourced stream).
|
|
for seq := uint64(10); seq >= 1; seq-- {
|
|
_, err := fs.LoadMsg(seq, &smv)
|
|
require_NoError(t, err)
|
|
}
|
|
checkNoCache()
|
|
|
|
// Now make sure still expires properly on linear scans with deleted msgs.
|
|
// We want to make sure we track linear updates even if message deleted.
|
|
_, err = fs.RemoveMsg(2)
|
|
require_NoError(t, err)
|
|
_, err = fs.RemoveMsg(9)
|
|
require_NoError(t, err)
|
|
|
|
// Walk forward.
|
|
for seq := uint64(1); seq <= 10; seq++ {
|
|
_, err := fs.LoadMsg(seq, &smv)
|
|
if seq == 2 || seq == 9 {
|
|
require_Error(t, err, errDeletedMsg)
|
|
} else {
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
checkNoCache()
|
|
}
|
|
|
|
func TestFileStoreSkipMsgs(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir(), BlockSize: 1024},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Test on empty FS first.
|
|
// Make sure wrong starting sequence fails.
|
|
err = fs.SkipMsgs(10, 100)
|
|
require_Error(t, err, ErrSequenceMismatch)
|
|
|
|
err = fs.SkipMsgs(1, 100)
|
|
require_NoError(t, err)
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.FirstSeq, 101)
|
|
require_Equal(t, state.LastSeq, 100)
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
|
|
// Now add alot.
|
|
err = fs.SkipMsgs(101, 100_000)
|
|
require_NoError(t, err)
|
|
state = fs.State()
|
|
require_Equal(t, state.FirstSeq, 100_101)
|
|
require_Equal(t, state.LastSeq, 100_100)
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
|
|
// Now add in a message, and then skip to check dmap.
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir(), BlockSize: 1024},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.StoreMsg("foo", nil, nil, 0)
|
|
err = fs.SkipMsgs(2, 10)
|
|
require_NoError(t, err)
|
|
state = fs.State()
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 11)
|
|
require_Equal(t, state.Msgs, 1)
|
|
require_Equal(t, state.NumDeleted, 10)
|
|
require_Equal(t, len(state.Deleted), 10)
|
|
|
|
// Check Fast State too.
|
|
state.Deleted = nil
|
|
fs.FastState(&state)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 11)
|
|
require_Equal(t, state.Msgs, 1)
|
|
require_Equal(t, state.NumDeleted, 10)
|
|
}
|
|
|
|
func TestFileStoreOptimizeFirstLoadNextMsgWithSequenceZero(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 4096},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := bytes.Repeat([]byte("ZZZ"), 33) // ~100bytes
|
|
|
|
for i := 0; i < 5000; i++ {
|
|
fs.StoreMsg("foo.A", nil, msg, 0)
|
|
}
|
|
// This will create alot of blocks, ~167.
|
|
// Just used to check that we do not load these in when searching.
|
|
// Now add in 10 for foo.bar at the end.
|
|
for i := 0; i < 10; i++ {
|
|
fs.StoreMsg("foo.B", nil, msg, 0)
|
|
}
|
|
// The bug would not be visible on running server per se since we would have had fss loaded
|
|
// and that sticks around a bit longer, we would use that to skip over the early blocks. So stop
|
|
// and restart the filestore.
|
|
fs.Stop()
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 4096},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Now fetch the next message for foo.B but set starting sequence to 0.
|
|
_, nseq, err := fs.LoadNextMsg("foo.B", false, 0, nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, nseq, 5001)
|
|
// Now check how many blks are loaded, should be only 1.
|
|
require_Equal(t, fs.cacheLoads(), 1)
|
|
}
|
|
|
|
func TestFileStoreWriteFullStateHighSubjectCardinality(t *testing.T) {
|
|
t.Skip()
|
|
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 4096},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte{1, 2, 3}
|
|
|
|
for i := 0; i < 1_000_000; i++ {
|
|
subj := fmt.Sprintf("subj_%d", i)
|
|
_, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
start := time.Now()
|
|
require_NoError(t, fs.writeFullState())
|
|
t.Logf("Took %s to writeFullState", time.Since(start))
|
|
}
|
|
|
|
func TestFileStoreEraseMsgWithDbitSlots(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.StoreMsg("foo", nil, []byte("abd"), 0)
|
|
for i := 0; i < 10; i++ {
|
|
fs.SkipMsg(0)
|
|
}
|
|
fs.StoreMsg("foo", nil, []byte("abd"), 0)
|
|
// Now grab that first block and compact away the skips which will
|
|
// introduce dbits into our idx.
|
|
fs.mu.RLock()
|
|
mb := fs.blks[0]
|
|
fs.mu.RUnlock()
|
|
// Compact.
|
|
mb.mu.Lock()
|
|
err = mb.compact()
|
|
mb.mu.Unlock()
|
|
require_NoError(t, err)
|
|
|
|
removed, err := fs.EraseMsg(1)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
}
|
|
|
|
func TestFileStoreEraseMsgWithAllTrailingDbitSlots(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.StoreMsg("foo", nil, []byte("abc"), 0)
|
|
fs.StoreMsg("foo", nil, []byte("abcdefg"), 0)
|
|
|
|
for i := 0; i < 10; i++ {
|
|
fs.SkipMsg(0)
|
|
}
|
|
// Now grab that first block and compact away the skips which will
|
|
// introduce dbits into our idx.
|
|
fs.mu.RLock()
|
|
mb := fs.blks[0]
|
|
fs.mu.RUnlock()
|
|
// Compact.
|
|
mb.mu.Lock()
|
|
err = mb.compact()
|
|
mb.mu.Unlock()
|
|
require_NoError(t, err)
|
|
|
|
removed, err := fs.EraseMsg(2)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
}
|
|
|
|
func TestFileStoreMultiLastSeqs(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir(), BlockSize: 256}, // Make block size small to test multiblock selections with maxSeq
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*", "bar.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("abc")
|
|
for i := 0; i < 33; i++ {
|
|
fs.StoreMsg("foo.foo", nil, msg, 0)
|
|
fs.StoreMsg("foo.bar", nil, msg, 0)
|
|
fs.StoreMsg("foo.baz", nil, msg, 0)
|
|
}
|
|
for i := 0; i < 33; i++ {
|
|
fs.StoreMsg("bar.foo", nil, msg, 0)
|
|
fs.StoreMsg("bar.bar", nil, msg, 0)
|
|
fs.StoreMsg("bar.baz", nil, msg, 0)
|
|
}
|
|
|
|
checkResults := func(seqs, expected []uint64) {
|
|
t.Helper()
|
|
if len(seqs) != len(expected) {
|
|
t.Fatalf("Expected %+v got %+v", expected, seqs)
|
|
}
|
|
for i := range seqs {
|
|
if seqs[i] != expected[i] {
|
|
t.Fatalf("Expected %+v got %+v", expected, seqs)
|
|
}
|
|
}
|
|
}
|
|
|
|
// UpTo sequence 3. Tests block split.
|
|
seqs, err := fs.MultiLastSeqs([]string{"foo.*"}, 3, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, []uint64{1, 2, 3})
|
|
// Up to last sequence of the stream.
|
|
seqs, err = fs.MultiLastSeqs([]string{"foo.*"}, 0, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, []uint64{97, 98, 99})
|
|
// Check for bar.* at the end.
|
|
seqs, err = fs.MultiLastSeqs([]string{"bar.*"}, 0, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, []uint64{196, 197, 198})
|
|
// This should find nothing.
|
|
seqs, err = fs.MultiLastSeqs([]string{"bar.*"}, 99, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, nil)
|
|
|
|
// Do multiple subjects explicitly.
|
|
seqs, err = fs.MultiLastSeqs([]string{"foo.foo", "foo.bar", "foo.baz"}, 3, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, []uint64{1, 2, 3})
|
|
seqs, err = fs.MultiLastSeqs([]string{"foo.foo", "foo.bar", "foo.baz"}, 0, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, []uint64{97, 98, 99})
|
|
seqs, err = fs.MultiLastSeqs([]string{"bar.foo", "bar.bar", "bar.baz"}, 0, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, []uint64{196, 197, 198})
|
|
seqs, err = fs.MultiLastSeqs([]string{"bar.foo", "bar.bar", "bar.baz"}, 99, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, nil)
|
|
|
|
// Check single works
|
|
seqs, err = fs.MultiLastSeqs([]string{"foo.foo"}, 3, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, []uint64{1})
|
|
|
|
// Now test that we properly de-duplicate between filters.
|
|
seqs, err = fs.MultiLastSeqs([]string{"foo.*", "foo.bar"}, 3, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, []uint64{1, 2, 3})
|
|
seqs, err = fs.MultiLastSeqs([]string{"bar.>", "bar.bar", "bar.baz"}, 0, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, []uint64{196, 197, 198})
|
|
|
|
// All
|
|
seqs, err = fs.MultiLastSeqs([]string{">"}, 0, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, []uint64{97, 98, 99, 196, 197, 198})
|
|
seqs, err = fs.MultiLastSeqs([]string{">"}, 99, -1)
|
|
require_NoError(t, err)
|
|
checkResults(seqs, []uint64{97, 98, 99})
|
|
}
|
|
|
|
func TestFileStoreMultiLastSeqsMaxAllowed(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("abc")
|
|
for i := 1; i <= 100; i++ {
|
|
fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
}
|
|
// Test that if we specify maxAllowed that we get the correct error.
|
|
seqs, err := fs.MultiLastSeqs([]string{"foo.*"}, 0, 10)
|
|
require_True(t, seqs == nil)
|
|
require_Error(t, err, ErrTooManyResults)
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/5236
|
|
// Unclear how the sequences get off here, this is just forcing the situation reported.
|
|
func TestFileStoreMsgBlockFirstAndLastSeqCorrupt(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("abc")
|
|
for i := 1; i <= 10; i++ {
|
|
fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
}
|
|
fs.Purge()
|
|
|
|
fs.mu.RLock()
|
|
mb := fs.blks[0]
|
|
fs.mu.RUnlock()
|
|
|
|
mb.mu.Lock()
|
|
mb.tryForceExpireCacheLocked()
|
|
atomic.StoreUint64(&mb.last.seq, 9)
|
|
mb.mu.Unlock()
|
|
|
|
// We should rebuild here and return no error.
|
|
require_NoError(t, mb.loadMsgs())
|
|
mb.mu.RLock()
|
|
fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
|
|
mb.mu.RUnlock()
|
|
require_Equal(t, fseq, 11)
|
|
require_Equal(t, lseq, 10)
|
|
}
|
|
|
|
func TestFileStoreWriteFullStateAfterPurgeEx(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("abc")
|
|
for i := 1; i <= 10; i++ {
|
|
fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
}
|
|
fs.RemoveMsg(8)
|
|
fs.RemoveMsg(9)
|
|
fs.RemoveMsg(10)
|
|
|
|
n, err := fs.PurgeEx(">", 8, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, n, 7)
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 11)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
|
|
// Make sure this does not reset our state due to skew with msg blocks.
|
|
fs.writeFullState()
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 11)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
}
|
|
|
|
func TestFileStoreFSSExpire(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir(), BlockSize: 8192, CacheExpire: 1 * time.Second, SubjectStateExpire: time.Second},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, MaxMsgsPer: 1, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("abc")
|
|
for i := 1; i <= 1000; i++ {
|
|
fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
}
|
|
// Flush fss by hand, cache should be flushed as well.
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.Lock()
|
|
mb.fss = nil
|
|
mb.mu.Unlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
|
|
fs.StoreMsg("foo.11", nil, msg, 0)
|
|
time.Sleep(900 * time.Millisecond)
|
|
// This should keep fss alive in the first block..
|
|
// As well as cache itself due to remove activity.
|
|
fs.StoreMsg("foo.22", nil, msg, 0)
|
|
time.Sleep(300 * time.Millisecond)
|
|
// Check that fss and the cache are still loaded.
|
|
fs.mu.RLock()
|
|
mb := fs.blks[0]
|
|
fs.mu.RUnlock()
|
|
mb.mu.RLock()
|
|
cache, fss := mb.ecache.Value(), mb.fss
|
|
mb.mu.RUnlock()
|
|
require_True(t, fss != nil)
|
|
require_True(t, cache != nil)
|
|
}
|
|
|
|
func TestFileStoreFSSExpireNumPending(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir(), BlockSize: 8192, CacheExpire: 1 * time.Second, SubjectStateExpire: 2 * time.Second},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, MaxMsgsPer: 1, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("abc")
|
|
for i := 1; i <= 100_000; i++ {
|
|
fs.StoreMsg(fmt.Sprintf("foo.A.%d", i), nil, msg, 0)
|
|
fs.StoreMsg(fmt.Sprintf("foo.B.%d", i), nil, msg, 0)
|
|
}
|
|
// Flush fss by hand, cache should be flushed as well.
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.Lock()
|
|
mb.fss = nil
|
|
mb.mu.Unlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
|
|
nb := fs.numMsgBlocks()
|
|
// Now execute NumPending() such that we load lots of blocks and make sure fss do not expire.
|
|
start := time.Now()
|
|
n, _, err := fs.NumPending(100_000, "foo.A.*", false)
|
|
require_NoError(t, err)
|
|
elapsed := time.Since(start)
|
|
|
|
require_Equal(t, n, 50_000)
|
|
// Make sure we did not force expire the fss. We would have loaded first half of blocks.
|
|
var noFss bool
|
|
last := nb/2 - 1
|
|
fs.mu.RLock()
|
|
for i, mb := range fs.blks {
|
|
mb.mu.RLock()
|
|
noFss = mb.fss == nil
|
|
mb.mu.RUnlock()
|
|
if noFss || i == last {
|
|
break
|
|
}
|
|
}
|
|
fs.mu.RUnlock()
|
|
require_False(t, noFss)
|
|
|
|
// Run again, make sure faster. This is consequence of fss being loaded now.
|
|
start = time.Now()
|
|
fs.NumPending(100_000, "foo.A.*", false)
|
|
require_True(t, elapsed > 2*time.Since(start))
|
|
|
|
// Now do with start past the mid-point.
|
|
start = time.Now()
|
|
fs.NumPending(150_000, "foo.B.*", false)
|
|
elapsed = time.Since(start)
|
|
time.Sleep(time.Second)
|
|
start = time.Now()
|
|
fs.NumPending(150_000, "foo.B.*", false)
|
|
require_True(t, elapsed > time.Since(start))
|
|
|
|
// Sleep enough so that all mb.fss should expire, which is 2s above.
|
|
time.Sleep(4 * time.Second)
|
|
fs.mu.RLock()
|
|
for i, mb := range fs.blks {
|
|
mb.mu.RLock()
|
|
fss := mb.fss
|
|
mb.mu.RUnlock()
|
|
if fss != nil {
|
|
fs.mu.RUnlock()
|
|
t.Fatalf("Detected loaded fss for mb %d (size %d)", i, fss.Size())
|
|
}
|
|
}
|
|
fs.mu.RUnlock()
|
|
}
|
|
|
|
// We want to ensure that recovery of deleted messages survives no index.db and compactions.
|
|
func TestFileStoreRecoverWithRemovesAndNoIndexDB(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 250},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("abc")
|
|
for i := 1; i <= 10; i++ {
|
|
fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
}
|
|
fs.RemoveMsg(1)
|
|
fs.RemoveMsg(2)
|
|
fs.RemoveMsg(8)
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 3)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
require_Equal(t, ss.Msgs, 7)
|
|
|
|
// Compact last block.
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
lmb.mu.Lock()
|
|
err = lmb.compact()
|
|
lmb.mu.Unlock()
|
|
require_NoError(t, err)
|
|
// Stop but remove index.db
|
|
sfile := filepath.Join(sd, msgDir, streamStreamStateFile)
|
|
fs.Stop()
|
|
os.Remove(sfile)
|
|
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 3)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
require_Equal(t, ss.Msgs, 7)
|
|
}
|
|
|
|
func TestFileStoreReloadAndLoseLastSequence(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 0; i < 22; i++ {
|
|
fs.SkipMsg(0)
|
|
}
|
|
|
|
// Restart 5 times.
|
|
for i := 0; i < 5; i++ {
|
|
fs.Stop()
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 23)
|
|
require_Equal(t, ss.LastSeq, 22)
|
|
}
|
|
}
|
|
|
|
func TestFileStoreReloadAndLoseLastSequenceWithSkipMsgs(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Make sure same works with SkipMsgs which can kick in from delete blocks to replicas.
|
|
require_NoError(t, fs.SkipMsgs(0, 22))
|
|
|
|
// Restart 5 times.
|
|
for i := 0; i < 5; i++ {
|
|
fs.Stop()
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: sd},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 23)
|
|
require_Equal(t, ss.LastSeq, 22)
|
|
}
|
|
}
|
|
|
|
func TestFileStoreLoadLastWildcard(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 512},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("hello")
|
|
fs.StoreMsg("foo.22.baz", nil, msg, 0)
|
|
fs.StoreMsg("foo.22.bar", nil, msg, 0)
|
|
for i := 0; i < 1000; i++ {
|
|
fs.StoreMsg("foo.11.foo", nil, msg, 0)
|
|
}
|
|
|
|
// Make sure we remove fss since that would mask the problem that we walk
|
|
// all the blocks.
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.Lock()
|
|
mb.fss = nil
|
|
mb.mu.Unlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
|
|
// Attempt to load the last msg using a wildcarded subject.
|
|
sm, err := fs.LoadLastMsg("foo.22.*", nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, sm.seq, 2)
|
|
|
|
// Make sure that we took advantage of psim meta data and only load one block.
|
|
var cloads uint64
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.RLock()
|
|
cloads += mb.cloads
|
|
mb.mu.RUnlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
require_Equal(t, cloads, 1)
|
|
}
|
|
|
|
func TestFileStoreLoadLastWildcardWithPresenceMultipleBlocks(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 64},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Make sure we have "foo.222.bar" in multiple blocks to show bug.
|
|
fs.StoreMsg("foo.22.bar", nil, []byte("hello"), 0)
|
|
fs.StoreMsg("foo.22.baz", nil, []byte("ok"), 0)
|
|
fs.StoreMsg("foo.22.baz", nil, []byte("ok"), 0)
|
|
fs.StoreMsg("foo.22.bar", nil, []byte("hello22"), 0)
|
|
require_True(t, fs.numMsgBlocks() > 1)
|
|
sm, err := fs.LoadLastMsg("foo.*.bar", nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, "hello22", string(sm.msg))
|
|
}
|
|
|
|
// We want to make sure that we update psim correctly on a miss.
|
|
func TestFileStoreFilteredPendingPSIMFirstBlockUpdate(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 512},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// When PSIM detects msgs == 1 will catch up, so msgs needs to be > 1.
|
|
// Then create a huge block gap.
|
|
msg := []byte("hello")
|
|
fs.StoreMsg("foo.baz", nil, msg, 0)
|
|
for i := 0; i < 1000; i++ {
|
|
fs.StoreMsg("foo.foo", nil, msg, 0)
|
|
}
|
|
// Bookend with 2 more foo.baz
|
|
fs.StoreMsg("foo.baz", nil, msg, 0)
|
|
fs.StoreMsg("foo.baz", nil, msg, 0)
|
|
// Now remove first one.
|
|
removed, err := fs.RemoveMsg(1)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
// 84 blocks.
|
|
require_Equal(t, fs.numMsgBlocks(), 84)
|
|
fs.mu.RLock()
|
|
psi, ok := fs.psim.Find([]byte("foo.baz"))
|
|
fs.mu.RUnlock()
|
|
require_True(t, ok)
|
|
require_Equal(t, psi.total, 2)
|
|
require_Equal(t, psi.fblk, 1)
|
|
require_Equal(t, psi.lblk, 84)
|
|
|
|
// No make sure that a call to numFilterPending which will initially walk all blocks if starting from seq 1 updates psi.
|
|
var ss SimpleState
|
|
fs.mu.RLock()
|
|
err = fs.numFilteredPending("foo.baz", &ss)
|
|
fs.mu.RUnlock()
|
|
require_NoError(t, err)
|
|
require_Equal(t, ss.Msgs, 2)
|
|
require_Equal(t, ss.First, 1002)
|
|
require_Equal(t, ss.Last, 1003)
|
|
|
|
// Check psi was updated. This is done in separate go routine to acquire
|
|
// the write lock now.
|
|
checkFor(t, time.Second, 100*time.Millisecond, func() error {
|
|
fs.mu.RLock()
|
|
psi, ok = fs.psim.Find([]byte("foo.baz"))
|
|
total, fblk, lblk := psi.total, psi.fblk, psi.lblk
|
|
fs.mu.RUnlock()
|
|
require_True(t, ok)
|
|
require_Equal(t, total, 2)
|
|
require_Equal(t, lblk, 84)
|
|
if fblk != 84 {
|
|
return fmt.Errorf("fblk should be 84, still %d", fblk)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestFileStoreWildcardFilteredPendingPSIMFirstBlockUpdate(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 512},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// When PSIM detects msgs == 1 will catch up, so msgs needs to be > 1.
|
|
// Then create a huge block gap.
|
|
msg := []byte("hello")
|
|
fs.StoreMsg("foo.22.baz", nil, msg, 0)
|
|
fs.StoreMsg("foo.22.bar", nil, msg, 0)
|
|
for i := 0; i < 1000; i++ {
|
|
fs.StoreMsg("foo.1.foo", nil, msg, 0)
|
|
}
|
|
// Bookend with 3 more, two foo.baz and two foo.bar.
|
|
fs.StoreMsg("foo.22.baz", nil, msg, 0)
|
|
fs.StoreMsg("foo.22.baz", nil, msg, 0)
|
|
fs.StoreMsg("foo.22.bar", nil, msg, 0)
|
|
fs.StoreMsg("foo.22.bar", nil, msg, 0)
|
|
|
|
// Now remove first one for foo.bar and foo.baz.
|
|
removed, err := fs.RemoveMsg(1)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
removed, err = fs.RemoveMsg(2)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
|
|
// 92 blocks.
|
|
require_Equal(t, fs.numMsgBlocks(), 92)
|
|
fs.mu.RLock()
|
|
psi, ok := fs.psim.Find([]byte("foo.22.baz"))
|
|
total, fblk, lblk := psi.total, psi.fblk, psi.lblk
|
|
fs.mu.RUnlock()
|
|
require_True(t, ok)
|
|
require_Equal(t, total, 2)
|
|
require_Equal(t, fblk, 1)
|
|
require_Equal(t, lblk, 92)
|
|
|
|
fs.mu.RLock()
|
|
psi, ok = fs.psim.Find([]byte("foo.22.bar"))
|
|
total, fblk, lblk = psi.total, psi.fblk, psi.lblk
|
|
fs.mu.RUnlock()
|
|
require_True(t, ok)
|
|
require_Equal(t, total, 2)
|
|
require_Equal(t, fblk, 1)
|
|
require_Equal(t, lblk, 92)
|
|
|
|
// No make sure that a call to numFilterPending which will initially walk all blocks if starting from seq 1 updates psi.
|
|
var ss SimpleState
|
|
fs.mu.RLock()
|
|
err = fs.numFilteredPending("foo.22.*", &ss)
|
|
fs.mu.RUnlock()
|
|
require_NoError(t, err)
|
|
require_Equal(t, ss.Msgs, 4)
|
|
require_Equal(t, ss.First, 1003)
|
|
require_Equal(t, ss.Last, 1006)
|
|
|
|
// Check both psi were updated.
|
|
checkFor(t, time.Second, 100*time.Millisecond, func() error {
|
|
fs.mu.RLock()
|
|
psi, ok = fs.psim.Find([]byte("foo.22.baz"))
|
|
total, fblk, lblk = psi.total, psi.fblk, psi.lblk
|
|
fs.mu.RUnlock()
|
|
require_True(t, ok)
|
|
require_Equal(t, total, 2)
|
|
require_Equal(t, lblk, 92)
|
|
if fblk != 92 {
|
|
return fmt.Errorf("fblk should be 92, still %d", fblk)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
checkFor(t, time.Second, 100*time.Millisecond, func() error {
|
|
fs.mu.RLock()
|
|
psi, ok = fs.psim.Find([]byte("foo.22.bar"))
|
|
total, fblk, lblk = psi.total, psi.fblk, psi.lblk
|
|
fs.mu.RUnlock()
|
|
require_True(t, ok)
|
|
require_Equal(t, total, 2)
|
|
require_Equal(t, fblk, 92)
|
|
if fblk != 92 {
|
|
return fmt.Errorf("fblk should be 92, still %d", fblk)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Make sure if we only miss by one for fblk that we still update it.
|
|
func TestFileStoreFilteredPendingPSIMFirstBlockUpdateNextBlock(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 128},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("hello")
|
|
// Create 4 blocks, each block holds 2 msgs
|
|
for i := 0; i < 4; i++ {
|
|
fs.StoreMsg("foo.22.bar", nil, msg, 0)
|
|
fs.StoreMsg("foo.22.baz", nil, msg, 0)
|
|
}
|
|
require_Equal(t, fs.numMsgBlocks(), 4)
|
|
|
|
fetch := func(subj string) *psi {
|
|
t.Helper()
|
|
fs.mu.RLock()
|
|
var info psi
|
|
psi, ok := fs.psim.Find([]byte(subj))
|
|
if ok && psi != nil {
|
|
info = *psi
|
|
}
|
|
fs.mu.RUnlock()
|
|
require_True(t, ok)
|
|
return &info
|
|
}
|
|
|
|
psi := fetch("foo.22.bar")
|
|
require_Equal(t, psi.total, 4)
|
|
require_Equal(t, psi.fblk, 1)
|
|
require_Equal(t, psi.lblk, 4)
|
|
|
|
// Now remove first instance of "foo.22.bar"
|
|
removed, err := fs.RemoveMsg(1)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
|
|
// Call into numFilterePending(), we want to make sure it updates fblk.
|
|
var ss SimpleState
|
|
fs.mu.Lock()
|
|
err = fs.numFilteredPending("foo.22.bar", &ss)
|
|
fs.mu.Unlock()
|
|
require_NoError(t, err)
|
|
require_Equal(t, ss.Msgs, 3)
|
|
require_Equal(t, ss.First, 3)
|
|
require_Equal(t, ss.Last, 7)
|
|
|
|
// Now make sure that we properly updated the psim entry.
|
|
checkFor(t, time.Second, 100*time.Millisecond, func() error {
|
|
psi = fetch("foo.22.bar")
|
|
require_Equal(t, psi.total, 3)
|
|
require_Equal(t, psi.lblk, 4)
|
|
if psi.fblk != 2 {
|
|
return fmt.Errorf("fblk should be 2, still %d", psi.fblk)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Now make sure wildcard calls into also update blks.
|
|
// First remove first "foo.22.baz" which will remove first block.
|
|
removed, err = fs.RemoveMsg(2)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
// Make sure 3 blks left.
|
|
require_Equal(t, fs.numMsgBlocks(), 3)
|
|
|
|
psi = fetch("foo.22.baz")
|
|
require_Equal(t, psi.total, 3)
|
|
require_Equal(t, psi.fblk, 1)
|
|
require_Equal(t, psi.lblk, 4)
|
|
|
|
// Now call wildcard version of numFilteredPending to make sure it clears.
|
|
fs.mu.Lock()
|
|
err = fs.numFilteredPending("foo.*.baz", &ss)
|
|
fs.mu.Unlock()
|
|
require_NoError(t, err)
|
|
require_Equal(t, ss.Msgs, 3)
|
|
require_Equal(t, ss.First, 4)
|
|
require_Equal(t, ss.Last, 8)
|
|
|
|
checkFor(t, time.Second, 100*time.Millisecond, func() error {
|
|
psi = fetch("foo.22.baz")
|
|
require_Equal(t, psi.total, 3)
|
|
require_Equal(t, psi.lblk, 4)
|
|
if psi.fblk != 2 {
|
|
return fmt.Errorf("fblk should be 2, still %d", psi.fblk)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestFileStoreLargeSparseMsgsDoNotLoadAfterLast(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 128},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("hello")
|
|
// Create 2 blocks with each, each block holds 2 msgs
|
|
for i := 0; i < 2; i++ {
|
|
fs.StoreMsg("foo.22.bar", nil, msg, 0)
|
|
fs.StoreMsg("foo.22.baz", nil, msg, 0)
|
|
}
|
|
// Now create 8 more blocks with just baz. So no matches for these 8 blocks
|
|
// for "foo.22.bar".
|
|
for i := 0; i < 8; i++ {
|
|
fs.StoreMsg("foo.22.baz", nil, msg, 0)
|
|
fs.StoreMsg("foo.22.baz", nil, msg, 0)
|
|
}
|
|
require_Equal(t, fs.numMsgBlocks(), 10)
|
|
|
|
// Remove all blk cache and fss.
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.Lock()
|
|
mb.fss, mb.cache = nil, nil
|
|
mb.mu.Unlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
|
|
// "foo.22.bar" is at sequence 1 and 3.
|
|
// Make sure if we do a LoadNextMsg() starting at 4 that we do not load
|
|
// all the tail blocks.
|
|
_, _, err = fs.LoadNextMsg("foo.*.bar", true, 4, nil)
|
|
require_Error(t, err, ErrStoreEOF)
|
|
|
|
// Now make sure we did not load fss and cache.
|
|
var loaded int
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.RLock()
|
|
if mb.cache != nil || mb.fss != nil {
|
|
loaded++
|
|
}
|
|
mb.mu.RUnlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
// We will load first block for starting seq 4, but no others should have loaded.
|
|
require_Equal(t, loaded, 1)
|
|
}
|
|
|
|
func TestFileStoreCheckSkipFirstBlockBug(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 128},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("hello")
|
|
|
|
fs.StoreMsg("foo.BB.bar", nil, msg, 0)
|
|
fs.StoreMsg("foo.BB.bar", nil, msg, 0)
|
|
fs.StoreMsg("foo.AA.bar", nil, msg, 0)
|
|
for i := 0; i < 5; i++ {
|
|
fs.StoreMsg("foo.BB.bar", nil, msg, 0)
|
|
}
|
|
fs.StoreMsg("foo.AA.bar", nil, msg, 0)
|
|
fs.StoreMsg("foo.AA.bar", nil, msg, 0)
|
|
|
|
// Should have created 4 blocks.
|
|
// BB BB | AA BB | BB BB | BB BB | AA AA
|
|
require_Equal(t, fs.numMsgBlocks(), 5)
|
|
|
|
fs.RemoveMsg(3)
|
|
fs.RemoveMsg(4)
|
|
|
|
// Second block should be gone now.
|
|
// BB BB | -- -- | BB BB | BB BB | AA AA
|
|
require_Equal(t, fs.numMsgBlocks(), 4)
|
|
|
|
_, _, err = fs.LoadNextMsg("foo.AA.bar", false, 4, nil)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// https://github.com/nats-io/nats-server/issues/5702
|
|
func TestFileStoreTombstoneRbytes(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir(), BlockSize: 1024},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Block can hold 24 msgs.
|
|
// So will fill one block and half of the other
|
|
msg := []byte("hello")
|
|
for i := 0; i < 34; i++ {
|
|
fs.StoreMsg("foo.22", nil, msg, 0)
|
|
}
|
|
require_True(t, fs.numMsgBlocks() > 1)
|
|
// Now delete second half of first block which will place tombstones in second blk.
|
|
for seq := 11; seq <= 24; seq++ {
|
|
fs.RemoveMsg(uint64(seq))
|
|
}
|
|
// Now check that rbytes has been properly accounted for in second block.
|
|
fs.mu.RLock()
|
|
blk := fs.blks[1]
|
|
fs.mu.RUnlock()
|
|
|
|
blk.mu.RLock()
|
|
bytes, rbytes := blk.bytes, blk.rbytes
|
|
blk.mu.RUnlock()
|
|
require_True(t, rbytes > bytes)
|
|
}
|
|
|
|
func TestFileStoreMsgBlockShouldCompact(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// 127 fit into a block.
|
|
msg := bytes.Repeat([]byte("Z"), 64*1024)
|
|
for i := 0; i < 190; i++ {
|
|
fs.StoreMsg("foo.22", nil, msg, 0)
|
|
}
|
|
require_True(t, fs.numMsgBlocks() > 1)
|
|
// Now delete second half of first block which will place tombstones in second blk.
|
|
for seq := 64; seq <= 127; seq++ {
|
|
fs.RemoveMsg(uint64(seq))
|
|
}
|
|
fs.mu.RLock()
|
|
fblk := fs.blks[0]
|
|
sblk := fs.blks[1]
|
|
fs.mu.RUnlock()
|
|
|
|
fblk.mu.RLock()
|
|
bytes, rbytes := fblk.bytes, fblk.rbytes
|
|
shouldCompact := fblk.shouldCompactInline()
|
|
fblk.mu.RUnlock()
|
|
// Should have tripped compaction already.
|
|
require_Equal(t, bytes, rbytes)
|
|
require_False(t, shouldCompact)
|
|
|
|
sblk.mu.RLock()
|
|
shouldCompact = sblk.shouldCompactInline()
|
|
sblk.mu.RUnlock()
|
|
require_False(t, shouldCompact)
|
|
}
|
|
|
|
func TestFileStoreCheckSkipFirstBlockNotLoadOldBlocks(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 128},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("hello")
|
|
|
|
fs.StoreMsg("foo.BB.bar", nil, msg, 0)
|
|
fs.StoreMsg("foo.AA.bar", nil, msg, 0)
|
|
for i := 0; i < 6; i++ {
|
|
fs.StoreMsg("foo.BB.bar", nil, msg, 0)
|
|
}
|
|
fs.StoreMsg("foo.AA.bar", nil, msg, 0) // Sequence 9
|
|
fs.StoreMsg("foo.AA.bar", nil, msg, 0) // Sequence 10
|
|
|
|
for i := 0; i < 4; i++ {
|
|
fs.StoreMsg("foo.BB.bar", nil, msg, 0)
|
|
}
|
|
|
|
// Should have created 7 blocks.
|
|
// BB AA | BB BB | BB BB | BB BB | AA AA | BB BB | BB BB
|
|
require_Equal(t, fs.numMsgBlocks(), 7)
|
|
|
|
fs.RemoveMsg(1)
|
|
fs.RemoveMsg(2)
|
|
|
|
// First block should be gone now.
|
|
// -- -- | BB BB | BB BB | BB BB | AA AA | BB BB | BB BB
|
|
require_Equal(t, fs.numMsgBlocks(), 6)
|
|
|
|
// Remove all blk cache and fss.
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.Lock()
|
|
mb.fss, mb.cache = nil, nil
|
|
mb.mu.Unlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
|
|
// But this means that the psim still points fblk to block 1.
|
|
// So when we try to load AA from near the end (last AA sequence), it will not find anything and will then
|
|
// check if we can skip ahead, but in the process reload blocks 2, 3, 4 amd 5..
|
|
// This can trigger for an up to date consumer near the end of the stream that gets a new pull request that will pop it out of msgWait
|
|
// and it will call LoadNextMsg() like we do here with starting sequence of 11.
|
|
_, _, err = fs.LoadNextMsg("foo.AA.bar", false, 11, nil)
|
|
require_Error(t, err, ErrStoreEOF)
|
|
|
|
// Now make sure we did not load fss and cache.
|
|
var loaded int
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.RLock()
|
|
if mb.cache != nil || mb.fss != nil {
|
|
loaded++
|
|
}
|
|
mb.mu.RUnlock()
|
|
}
|
|
fs.mu.RUnlock()
|
|
// We will load last block for starting seq 9, but no others should have loaded.
|
|
require_Equal(t, loaded, 1)
|
|
}
|
|
|
|
func TestFileStoreSyncCompressOnlyIfDirty(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 256, SyncInterval: 250 * time.Millisecond},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("hello")
|
|
|
|
// 6 msgs per block.
|
|
// Fill 2 blocks.
|
|
for i := 0; i < 12; i++ {
|
|
fs.StoreMsg("foo.BB", nil, msg, 0)
|
|
}
|
|
// Create third block with just one message in it.
|
|
fs.StoreMsg("foo.BB", nil, msg, 0)
|
|
|
|
// Should have created 3 blocks.
|
|
require_Equal(t, fs.numMsgBlocks(), 3)
|
|
|
|
// Now delete a bunch that will will fill up 3 block with tombstones.
|
|
for _, seq := range []uint64{2, 3, 4, 5, 8, 9, 10, 11} {
|
|
_, err = fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
}
|
|
// Now make sure we add 4/5th block so syncBlocks will try to compact.
|
|
for i := 0; i < 6; i++ {
|
|
fs.StoreMsg("foo.BB", nil, msg, 0)
|
|
}
|
|
require_Equal(t, fs.numMsgBlocks(), 5)
|
|
|
|
// All should have compact set.
|
|
fs.mu.Lock()
|
|
// Only check first 3 blocks.
|
|
for i := 0; i < 3; i++ {
|
|
mb := fs.blks[i]
|
|
mb.mu.Lock()
|
|
shouldCompact := mb.shouldCompactSync()
|
|
mb.mu.Unlock()
|
|
if !shouldCompact {
|
|
fs.mu.Unlock()
|
|
t.Fatalf("Expected should compact to be true for %d, got false", mb.getIndex())
|
|
}
|
|
}
|
|
fs.mu.Unlock()
|
|
|
|
// Let sync run.
|
|
time.Sleep(300 * time.Millisecond)
|
|
|
|
// We want to make sure the last block, which is filled with tombstones and is not compactable, returns false now.
|
|
fs.mu.Lock()
|
|
for _, mb := range fs.blks {
|
|
mb.mu.Lock()
|
|
shouldCompact := mb.shouldCompactSync()
|
|
mb.mu.Unlock()
|
|
if shouldCompact {
|
|
fs.mu.Unlock()
|
|
t.Fatalf("Expected should compact to be false for %d, got true", mb.getIndex())
|
|
}
|
|
}
|
|
fs.mu.Unlock()
|
|
|
|
// Now remove some from block 3 and verify that compact is not suppressed.
|
|
_, err = fs.RemoveMsg(13)
|
|
require_NoError(t, err)
|
|
|
|
fs.mu.Lock()
|
|
mb := fs.blks[2] // block 3.
|
|
mb.mu.Lock()
|
|
noCompact := mb.noCompact
|
|
mb.mu.Unlock()
|
|
fs.mu.Unlock()
|
|
// Verify that since we deleted a message we should be considered for compaction again in syncBlocks().
|
|
require_False(t, noCompact)
|
|
}
|
|
|
|
// This test is for deleted interior message tracking after compaction from limits based deletes, meaning no tombstones.
|
|
// Bug was that dmap would not be properly be hydrated after the compact from rebuild. But we did so in populateGlobalInfo.
|
|
// So this is just to fix a bug in rebuildState tracking gaps after a compact.
|
|
func TestFileStoreDmapBlockRecoverAfterCompact(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 256},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage, MaxMsgsPer: 1})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("hello")
|
|
|
|
// 6 msgs per block.
|
|
// Fill the first block.
|
|
for i := 1; i <= 6; i++ {
|
|
fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
}
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
|
|
// Now create holes in the first block via the max msgs per subject of 1.
|
|
for i := 2; i < 6; i++ {
|
|
fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
}
|
|
require_Equal(t, fs.numMsgBlocks(), 2)
|
|
// Compact and rebuild the first blk. Do not have it call indexCacheBuf which will fix it up.
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.Lock()
|
|
if err = mb.compact(); err != nil {
|
|
mb.mu.Unlock()
|
|
require_NoError(t, err)
|
|
}
|
|
// Empty out dmap state.
|
|
mb.dmap.Empty()
|
|
ld, tombs, err := mb.rebuildStateLocked()
|
|
dmap := mb.dmap.Clone()
|
|
mb.mu.Unlock()
|
|
|
|
require_NoError(t, err)
|
|
require_Equal(t, ld, nil)
|
|
require_Equal(t, len(tombs), 0)
|
|
require_Equal(t, dmap.Size(), 4)
|
|
}
|
|
|
|
func TestFileStoreRestoreIndexWithMatchButLeftOverBlocks(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 256},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage, MaxMsgsPer: 1})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("hello")
|
|
|
|
// 6 msgs per block.
|
|
// Fill the first 2 blocks.
|
|
for i := 1; i <= 12; i++ {
|
|
fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
}
|
|
require_Equal(t, fs.numMsgBlocks(), 2)
|
|
|
|
// We will now stop which will create the index.db file which will
|
|
// match the last record exactly.
|
|
sfile := filepath.Join(sd, msgDir, streamStreamStateFile)
|
|
fs.Stop()
|
|
|
|
// Grab it since we will put it back.
|
|
buf, err := os.ReadFile(sfile)
|
|
require_NoError(t, err)
|
|
require_True(t, len(buf) > 0)
|
|
|
|
// Now do an additional block, but with the MaxMsgsPer this will remove the first block,
|
|
// but leave the second so on recovery will match the checksum for the last msg in second block.
|
|
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 256},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage, MaxMsgsPer: 1})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 1; i <= 6; i++ {
|
|
fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
}
|
|
|
|
// Grab correct state, we will use it to make sure we do the right thing.
|
|
var state StreamState
|
|
fs.FastState(&state)
|
|
|
|
require_Equal(t, state.Msgs, 12)
|
|
require_Equal(t, state.FirstSeq, 7)
|
|
require_Equal(t, state.LastSeq, 18)
|
|
// This will be block 2 and 3.
|
|
require_Equal(t, fs.numMsgBlocks(), 2)
|
|
|
|
fs.Stop()
|
|
// Put old stream state back.
|
|
require_NoError(t, os.WriteFile(sfile, buf, defaultFilePerms))
|
|
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: sd, BlockSize: 256},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage, MaxMsgsPer: 1})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.FastState(&state)
|
|
require_Equal(t, state.Msgs, 12)
|
|
require_Equal(t, state.FirstSeq, 7)
|
|
require_Equal(t, state.LastSeq, 18)
|
|
}
|
|
|
|
func TestFileStoreRestoreDeleteTombstonesExceedingMaxBlkSize(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 256
|
|
fs, err := newFileStoreWithCreated(
|
|
fcfg,
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage},
|
|
time.Now(),
|
|
prf(&fcfg),
|
|
nil,
|
|
)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
n, err := fs.PurgeEx(_EMPTY_, 1_000_000_000, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, n, 0)
|
|
|
|
msg := []byte("hello")
|
|
// 6 msgs per block with blk size 256.
|
|
for i := 1; i <= 10_000; i++ {
|
|
fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
}
|
|
// Now delete msgs which will write tombstones.
|
|
for seq := uint64(1_000_000_001); seq < 1_000_000_101; seq++ {
|
|
removed, err := fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
}
|
|
|
|
// Check last block and make sure the tombstones did not exceed blk size maximum.
|
|
// Check to make sure no blocks exceed blk size.
|
|
fs.mu.RLock()
|
|
blks := append([]*msgBlock(nil), fs.blks...)
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
|
|
var emptyBlks []*msgBlock
|
|
for _, mb := range blks {
|
|
mb.mu.RLock()
|
|
bytes, rbytes := mb.bytes, mb.rbytes
|
|
mb.mu.RUnlock()
|
|
require_True(t, bytes < 256)
|
|
require_True(t, rbytes < 256)
|
|
if bytes == 0 && mb != lmb {
|
|
emptyBlks = append(emptyBlks, mb)
|
|
}
|
|
}
|
|
// Check each block such that it signals it can be compacted but if we attempt compact here nothing should change.
|
|
for _, mb := range emptyBlks {
|
|
mb.mu.Lock()
|
|
mb.ensureRawBytesLoaded()
|
|
bytes, rbytes, shouldCompact := mb.bytes, mb.rbytes, mb.shouldCompactSync()
|
|
// Do the compact and make sure nothing changed.
|
|
err = mb.compact()
|
|
nbytes, nrbytes := mb.bytes, mb.rbytes
|
|
mb.mu.Unlock()
|
|
require_NoError(t, err)
|
|
require_True(t, shouldCompact)
|
|
require_Equal(t, bytes, nbytes)
|
|
require_Equal(t, rbytes, nrbytes)
|
|
}
|
|
|
|
// Now remove first msg which will invalidate the tombstones since they will be < first sequence.
|
|
removed, err := fs.RemoveMsg(1_000_000_000)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
|
|
// Now simulate a syncBlocks call and make sure it cleans up the tombstones that are no longer relevant.
|
|
fs.syncBlocks()
|
|
for _, mb := range emptyBlks {
|
|
mb.mu.Lock()
|
|
mb.ensureRawBytesLoaded()
|
|
index, bytes, rbytes := mb.index, mb.bytes, mb.rbytes
|
|
mb.mu.Unlock()
|
|
require_Equal(t, bytes, 0)
|
|
require_Equal(t, rbytes, 0)
|
|
// Also make sure we removed these blks all together.
|
|
fs.mu.RLock()
|
|
imb := fs.bim[index]
|
|
fs.mu.RUnlock()
|
|
require_True(t, imb == nil)
|
|
}
|
|
})
|
|
}
|
|
|
|
///////////////////////////////////////////////////////////////////////////
|
|
// Benchmarks
|
|
///////////////////////////////////////////////////////////////////////////
|
|
|
|
func Benchmark_FileStoreSelectMsgBlock(b *testing.B) {
|
|
// We use small block size to create lots of blocks for this test.
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir(), BlockSize: 128},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"*"}, Storage: FileStorage})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "A", bytes.Repeat([]byte("ABC"), 33) // ~100bytes
|
|
|
|
// Add in a bunch of blocks.
|
|
for i := 0; i < 1000; i++ {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
if fs.numMsgBlocks() < 1000 {
|
|
b.Fatalf("Expected at least 1000 blocks, got %d", fs.numMsgBlocks())
|
|
}
|
|
|
|
fs.mu.RLock()
|
|
defer fs.mu.RUnlock()
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
_, mb := fs.selectMsgBlockWithIndex(1)
|
|
if mb == nil {
|
|
b.Fatalf("Expected a non-nil mb")
|
|
}
|
|
}
|
|
b.StopTimer()
|
|
}
|
|
|
|
func Benchmark_FileStoreLoadNextMsgSameFilterAsStream(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
// Small om purpose.
|
|
msg := []byte("ok")
|
|
|
|
// Add in a bunch of msgs
|
|
for i := 0; i < 100_000; i++ {
|
|
subj := fmt.Sprintf("foo.%d", rand.Intn(1024))
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
var smv StoreMsg
|
|
for i := 0; i < b.N; i++ {
|
|
// Needs to start at ~1 to show slowdown.
|
|
_, _, err := fs.LoadNextMsg("foo.*", true, 10, &smv)
|
|
require_NoError(b, err)
|
|
}
|
|
}
|
|
|
|
func Benchmark_FileStoreLoadNextMsgLiteralSubject(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
// Small om purpose.
|
|
msg := []byte("ok")
|
|
|
|
// Add in a bunch of msgs
|
|
for i := 0; i < 100_000; i++ {
|
|
subj := fmt.Sprintf("foo.%d", rand.Intn(1024))
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
// This is the one we will try to match.
|
|
fs.StoreMsg("foo.2222", nil, msg, 0)
|
|
// So not last and we think we are done linear scan.
|
|
fs.StoreMsg("foo.3333", nil, msg, 0)
|
|
|
|
b.ResetTimer()
|
|
|
|
var smv StoreMsg
|
|
for i := 0; i < b.N; i++ {
|
|
_, _, err := fs.LoadNextMsg("foo.2222", false, 10, &smv)
|
|
require_NoError(b, err)
|
|
}
|
|
}
|
|
|
|
func Benchmark_FileStoreLoadNextMsgNoMsgsFirstSeq(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir(), BlockSize: 8192},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
// Small om purpose.
|
|
msg := []byte("ok")
|
|
|
|
// Add in a bunch of msgs
|
|
for i := 0; i < 1_000_000; i++ {
|
|
fs.StoreMsg("foo.bar", nil, msg, 0)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
var smv StoreMsg
|
|
for i := 0; i < b.N; i++ {
|
|
// This should error with EOF
|
|
_, _, err := fs.LoadNextMsg("foo.baz", false, 1, &smv)
|
|
if err != ErrStoreEOF {
|
|
b.Fatalf("Wrong error, expected EOF got %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func Benchmark_FileStoreLoadNextMsgNoMsgsNotFirstSeq(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir(), BlockSize: 8192},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
// Small om purpose.
|
|
msg := []byte("ok")
|
|
|
|
// Add in a bunch of msgs
|
|
for i := 0; i < 1_000_000; i++ {
|
|
fs.StoreMsg("foo.bar", nil, msg, 0)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
var smv StoreMsg
|
|
for i := 0; i < b.N; i++ {
|
|
// This should error with EOF
|
|
// Make sure the sequence is not first seq of 1.
|
|
_, _, err := fs.LoadNextMsg("foo.baz", false, 10, &smv)
|
|
if err != ErrStoreEOF {
|
|
b.Fatalf("Wrong error, expected EOF got %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func Benchmark_FileStoreLoadNextMsgVerySparseMsgsFirstSeq(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir(), BlockSize: 8192},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
// Small om purpose.
|
|
msg := []byte("ok")
|
|
|
|
// Add in a bunch of msgs
|
|
for i := 0; i < 1_000_000; i++ {
|
|
fs.StoreMsg("foo.bar", nil, msg, 0)
|
|
}
|
|
// Make last msg one that would match.
|
|
fs.StoreMsg("foo.baz", nil, msg, 0)
|
|
|
|
b.ResetTimer()
|
|
|
|
var smv StoreMsg
|
|
for i := 0; i < b.N; i++ {
|
|
_, _, err := fs.LoadNextMsg("foo.baz", false, 1, &smv)
|
|
require_NoError(b, err)
|
|
}
|
|
}
|
|
|
|
func Benchmark_FileStoreLoadNextMsgVerySparseMsgsNotFirstSeq(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir(), BlockSize: 8192},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
// Small om purpose.
|
|
msg := []byte("ok")
|
|
|
|
// Add in a bunch of msgs
|
|
for i := 0; i < 1_000_000; i++ {
|
|
fs.StoreMsg("foo.bar", nil, msg, 0)
|
|
}
|
|
// Make last msg one that would match.
|
|
fs.StoreMsg("foo.baz", nil, msg, 0)
|
|
|
|
b.ResetTimer()
|
|
|
|
var smv StoreMsg
|
|
for i := 0; i < b.N; i++ {
|
|
// Make sure not first seq.
|
|
_, _, err := fs.LoadNextMsg("foo.baz", false, 10, &smv)
|
|
require_NoError(b, err)
|
|
}
|
|
}
|
|
|
|
func Benchmark_FileStoreLoadNextMsgVerySparseMsgsInBetween(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir(), BlockSize: 8192},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
// Small om purpose.
|
|
msg := []byte("ok")
|
|
|
|
// Make first msg one that would match as well.
|
|
fs.StoreMsg("foo.baz", nil, msg, 0)
|
|
// Add in a bunch of msgs
|
|
for i := 0; i < 1_000_000; i++ {
|
|
fs.StoreMsg("foo.bar", nil, msg, 0)
|
|
}
|
|
// Make last msg one that would match as well.
|
|
fs.StoreMsg("foo.baz", nil, msg, 0)
|
|
|
|
b.ResetTimer()
|
|
|
|
var smv StoreMsg
|
|
for i := 0; i < b.N; i++ {
|
|
// Make sure not first seq.
|
|
_, _, err := fs.LoadNextMsg("foo.baz", false, 2, &smv)
|
|
require_NoError(b, err)
|
|
}
|
|
}
|
|
|
|
func Benchmark_FileStoreLoadNextMsgVerySparseMsgsInBetweenWithWildcard(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, Storage: FileStorage})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
// Small om purpose.
|
|
msg := []byte("ok")
|
|
|
|
// Make first msg one that would match as well.
|
|
fs.StoreMsg("foo.1.baz", nil, msg, 0)
|
|
// Add in a bunch of msgs.
|
|
// We need to make sure we have a range of subjects that could kick in a linear scan.
|
|
for i := 0; i < 1_000_000; i++ {
|
|
subj := fmt.Sprintf("foo.%d.bar", rand.Intn(100_000)+2)
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
// Make last msg one that would match as well.
|
|
fs.StoreMsg("foo.1.baz", nil, msg, 0)
|
|
|
|
b.ResetTimer()
|
|
|
|
var smv StoreMsg
|
|
for i := 0; i < b.N; i++ {
|
|
// Make sure not first seq.
|
|
_, _, err := fs.LoadNextMsg("foo.*.baz", true, 2, &smv)
|
|
require_NoError(b, err)
|
|
}
|
|
}
|
|
|
|
func Benchmark_FileStoreLoadNextManySubjectsWithWildcardNearLastBlock(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, Storage: FileStorage})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
// Small om purpose.
|
|
msg := []byte("ok")
|
|
|
|
// Make first msg one that would match as well.
|
|
fs.StoreMsg("foo.1.baz", nil, msg, 0)
|
|
// Add in a bunch of msgs.
|
|
// We need to make sure we have a range of subjects that could kick in a linear scan.
|
|
for i := 0; i < 1_000_000; i++ {
|
|
subj := fmt.Sprintf("foo.%d.bar", rand.Intn(100_000)+2)
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
// Make last msg one that would match as well.
|
|
fs.StoreMsg("foo.1.baz", nil, msg, 0)
|
|
|
|
b.ResetTimer()
|
|
|
|
var smv StoreMsg
|
|
for i := 0; i < b.N; i++ {
|
|
// Make sure not first seq.
|
|
_, _, err := fs.LoadNextMsg("foo.*.baz", true, 999_990, &smv)
|
|
require_NoError(b, err)
|
|
}
|
|
}
|
|
|
|
func Benchmark_FileStoreLoadNextMsgVerySparseMsgsLargeTail(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, Storage: FileStorage})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
// Small om purpose.
|
|
msg := []byte("ok")
|
|
|
|
// Make first msg one that would match as well.
|
|
fs.StoreMsg("foo.1.baz", nil, msg, 0)
|
|
// Add in a bunch of msgs.
|
|
// We need to make sure we have a range of subjects that could kick in a linear scan.
|
|
for i := 0; i < 1_000_000; i++ {
|
|
subj := fmt.Sprintf("foo.%d.bar", rand.Intn(64_000)+2)
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
b.ResetTimer()
|
|
|
|
var smv StoreMsg
|
|
for i := 0; i < b.N; i++ {
|
|
// Make sure not first seq.
|
|
_, _, err := fs.LoadNextMsg("foo.*.baz", true, 2, &smv)
|
|
require_Error(b, err, ErrStoreEOF)
|
|
}
|
|
}
|
|
|
|
func Benchmark_FileStoreCreateConsumerStores(b *testing.B) {
|
|
for _, syncAlways := range []bool{true, false} {
|
|
b.Run(fmt.Sprintf("%v", syncAlways), func(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir(), SyncAlways: syncAlways},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*.*"}, Storage: FileStorage})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
oconfig := ConsumerConfig{
|
|
DeliverSubject: "d",
|
|
FilterSubject: "foo",
|
|
AckPolicy: AckAll,
|
|
}
|
|
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
oname := fmt.Sprintf("obs22_%d", i)
|
|
ofs, err := fs.ConsumerStore(oname, time.Time{}, &oconfig)
|
|
require_NoError(b, err)
|
|
require_NoError(b, ofs.Stop())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFileStoreMaxMsgsPerSubjectOneStaleFblkAfterRestart(t *testing.T) {
|
|
sd := t.TempDir()
|
|
fcfg := FileStoreConfig{StoreDir: sd, BlockSize: 256}
|
|
scfg := StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage, MaxMsgsPer: 1}
|
|
|
|
fs, err := newFileStore(fcfg, scfg)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("hello")
|
|
|
|
// Store "foo.0" into the first block.
|
|
_, _, err = fs.StoreMsg("foo.0", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
|
|
// Fill the first block with other subjects until a second block is created.
|
|
for i := 1; fs.numMsgBlocks() < 2; i++ {
|
|
_, _, err = fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Store "foo.0" again. The message lands in the second block, and MaxMsgsPer=1 removes
|
|
// the copy in the first block. lblk should now point to the last block.
|
|
seq, _, err := fs.StoreMsg("foo.0", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
|
|
// Sanity check, while the in-memory lblk is correct, the last message is found.
|
|
var smv StoreMsg
|
|
sm, err := fs.LoadLastMsg("foo.0", &smv)
|
|
require_NoError(t, err)
|
|
require_Equal(t, sm.seq, seq)
|
|
|
|
// Stop writes the stream state file.
|
|
require_NoError(t, fs.Stop())
|
|
_, err = os.Stat(filepath.Join(sd, msgDir, streamStreamStateFile))
|
|
require_NoError(t, err)
|
|
|
|
// Restart, and recover from the stream state file. On 2.12.x recovery would set lblk to the
|
|
// stale fblk, both pointing at the first block.
|
|
fs, err = newFileStore(fcfg, scfg)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// The last message for "foo.0" must still be found after a restart.
|
|
sm, err = fs.LoadLastMsg("foo.0", &smv)
|
|
require_NoError(t, err)
|
|
require_Equal(t, sm.subj, "foo.0")
|
|
require_Equal(t, sm.seq, seq)
|
|
|
|
// Validate internal subject state.
|
|
fs.mu.RLock()
|
|
defer fs.mu.RUnlock()
|
|
info, ok := fs.psim.Find(stringToBytes("foo.0"))
|
|
require_True(t, ok)
|
|
require_Equal(t, info.total, 1)
|
|
require_Equal(t, info.lblk, 2)
|
|
require_Equal(t, info.fblk, 2) // Since it's MaxMsgsPer:1, this should be optimized to last block.
|
|
}
|
|
|
|
func Benchmark_FileStoreSubjectStateConsistencyOptimizationPerf(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: b.TempDir()},
|
|
StreamConfig{Name: "TEST", Subjects: []string{"foo.*"}, Storage: FileStorage, MaxMsgsPer: 1},
|
|
)
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
// Do R rounds of storing N messages.
|
|
// MaxMsgsPer=1, so every unique subject that's placed only exists in the stream once.
|
|
// If R=2, N=3 that means we'd place foo.0, foo.1, foo.2 in the first round, and the second
|
|
// round we'd place foo.2, foo.1, foo.0, etc. This is intentional so that without any
|
|
// optimizations we'd need to scan either 1 in the optimal case or N in the worst case.
|
|
// Which is way more expensive than always knowing what the sequences are and it being O(1).
|
|
r := max(2, b.N)
|
|
n := 40_000
|
|
b.ResetTimer()
|
|
for i := 0; i < r; i++ {
|
|
for j := 0; j < n; j++ {
|
|
d := j
|
|
if i%2 == 0 {
|
|
d = n - j - 1
|
|
}
|
|
subject := fmt.Sprintf("foo.%d", d)
|
|
_, _, err = fs.StoreMsg(subject, nil, nil, 0)
|
|
require_NoError(b, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func benchmarkFileStoreSyncDeletedFullBlocks(b *testing.B, msgSize int) {
|
|
fs, _ := newFileStore(
|
|
FileStoreConfig{
|
|
StoreDir: b.TempDir(),
|
|
BlockSize: defaultLargeBlockSize,
|
|
},
|
|
StreamConfig{
|
|
Name: "zzz",
|
|
Storage: FileStorage,
|
|
},
|
|
)
|
|
defer fs.Stop()
|
|
|
|
const numBlocks = 50
|
|
subj, msg := "foo", make([]byte, msgSize)
|
|
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
b.StopTimer()
|
|
for len(fs.blks) < numBlocks {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
dbs := DeleteBlocks{
|
|
&DeleteRange{First: fs.state.FirstSeq, Num: fs.state.Msgs}}
|
|
|
|
b.StartTimer()
|
|
fs.SyncDeleted(dbs)
|
|
}
|
|
}
|
|
|
|
func Benchmark_FileStoreSyncDeletedFullBlocks(b *testing.B) {
|
|
sizes := []int{32, 64, 128, 256, 512, 1024}
|
|
for _, msgSize := range sizes {
|
|
b.Run(fmt.Sprintf("MsgSize-%d", msgSize),
|
|
func(b *testing.B) {
|
|
benchmarkFileStoreSyncDeletedFullBlocks(b, msgSize)
|
|
})
|
|
}
|
|
}
|
|
|
|
func benchmarkFileStoreSyncDeletedPartialBlocks(b *testing.B, msgSize int) {
|
|
fs, _ := newFileStore(
|
|
FileStoreConfig{
|
|
StoreDir: b.TempDir(),
|
|
BlockSize: defaultLargeBlockSize,
|
|
},
|
|
StreamConfig{
|
|
Name: "zzz",
|
|
Storage: FileStorage,
|
|
},
|
|
)
|
|
defer fs.Stop()
|
|
|
|
const numBlocks = 100
|
|
subj, msg := "foo", make([]byte, msgSize)
|
|
|
|
b.ResetTimer()
|
|
for b.Loop() {
|
|
b.StopTimer()
|
|
if len(fs.blks) > 1 {
|
|
fs.removeMsgsInRange(fs.state.FirstSeq, fs.blks[1].last.seq, true)
|
|
}
|
|
for len(fs.blks) <= numBlocks {
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
first := fs.state.FirstSeq + 1
|
|
last := fs.blks[1].last.seq
|
|
dbs := DeleteBlocks{&DeleteRange{
|
|
First: first,
|
|
Num: last - first}}
|
|
|
|
b.StartTimer()
|
|
fs.SyncDeleted(dbs)
|
|
}
|
|
}
|
|
|
|
func Benchmark_FileStoreSyncDeletedPartialBlocks(b *testing.B) {
|
|
sizes := []int{16, 512}
|
|
for _, msgSize := range sizes {
|
|
b.Run(fmt.Sprintf("MsgSize-%d", msgSize),
|
|
func(b *testing.B) {
|
|
benchmarkFileStoreSyncDeletedPartialBlocks(b, msgSize)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFileStoreWriteFullStateDetectCorruptState(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("abc")
|
|
for i := 1; i <= 10; i++ {
|
|
_, _, err = fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Simulate a change in a message block not being reflected in the fs.
|
|
mb := fs.selectMsgBlock(2)
|
|
mb.mu.Lock()
|
|
mb.msgs--
|
|
mb.mu.Unlock()
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
require_Equal(t, ss.Msgs, 10)
|
|
|
|
// Make sure we detect the corrupt state and rebuild.
|
|
err = fs.writeFullState()
|
|
require_Error(t, err, errCorruptState)
|
|
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
require_Equal(t, ss.Msgs, 9)
|
|
}
|
|
|
|
func TestFileStoreRecoverFullStateDetectCorruptState(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("abc")
|
|
for i := 1; i <= 10; i++ {
|
|
_, _, err = fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
err = fs.writeFullState()
|
|
require_NoError(t, err)
|
|
|
|
sfile := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)
|
|
buf, err := os.ReadFile(sfile)
|
|
require_NoError(t, err)
|
|
// Update to an incorrect message count.
|
|
binary.PutUvarint(buf[2:], 0)
|
|
// Just append a corrected checksum to the end to make it pass the checks.
|
|
fs.hh.Reset()
|
|
fs.hh.Write(buf)
|
|
buf = fs.hh.Sum(buf)
|
|
err = os.WriteFile(sfile, buf, defaultFilePerms)
|
|
require_NoError(t, err)
|
|
|
|
err = fs.recoverFullState()
|
|
require_Error(t, err, errCorruptState)
|
|
}
|
|
|
|
func TestFileStoreResetConsumerToStreamState(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := []byte("abc")
|
|
for i := 1; i <= 30; i++ {
|
|
_, _, err = fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
err = fs.writeFullState()
|
|
require_NoError(t, err)
|
|
|
|
obs, err := fs.ConsumerStore("c1", time.Now(), &ConsumerConfig{
|
|
Durable: "c1",
|
|
FilterSubject: "foo.*",
|
|
AckPolicy: AckNone,
|
|
DeliverPolicy: DeliverAll,
|
|
})
|
|
|
|
require_NoError(t, err)
|
|
defer obs.Stop()
|
|
|
|
state := &ConsumerState{}
|
|
state.Delivered = SequencePair{Consumer: 5, Stream: 5}
|
|
state.AckFloor = SequencePair{Consumer: 5, Stream: 5}
|
|
|
|
// set to 5
|
|
err = obs.Update(state)
|
|
require_NoError(t, err)
|
|
|
|
currState, err := obs.State()
|
|
require_NoError(t, err)
|
|
|
|
fsState := fs.State()
|
|
require_Equal(t, fsState.LastSeq, uint64(30))
|
|
require_Equal(t, fsState.FirstSeq, uint64(1))
|
|
require_Equal(t, currState.AckFloor, state.AckFloor)
|
|
require_Equal(t, currState.Delivered, state.Delivered)
|
|
require_Equal(t, len(currState.Redelivered), len(state.Redelivered))
|
|
require_Equal(t, len(currState.Pending), len(state.Pending))
|
|
|
|
fs.mu.Lock()
|
|
fs.state.FirstSeq = 0
|
|
fs.state.LastSeq = 0
|
|
fs.mu.Unlock()
|
|
|
|
// set back to lower values
|
|
newState := &ConsumerState{
|
|
Delivered: SequencePair{Consumer: 1, Stream: 4},
|
|
AckFloor: SequencePair{Consumer: 1, Stream: 3},
|
|
}
|
|
|
|
// update should fail but force update should pass
|
|
err = obs.Update(newState)
|
|
require_Error(t, err, ErrStoreOldUpdate)
|
|
|
|
err = obs.ForceUpdate(newState)
|
|
require_NoError(t, err)
|
|
|
|
currState, err = obs.State()
|
|
require_NoError(t, err)
|
|
|
|
require_Equal(t, currState.AckFloor, newState.AckFloor)
|
|
require_Equal(t, currState.Delivered, newState.Delivered)
|
|
require_Equal(t, len(currState.Redelivered), len(newState.Redelivered))
|
|
require_Equal(t, len(currState.Pending), len(newState.Pending))
|
|
}
|
|
|
|
func TestFileStoreNumPendingMulti(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"ev.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
totalMsgs := 100_000
|
|
totalSubjects := 10_000
|
|
numFiltered := 5000
|
|
startSeq := uint64(5_000 + rand.Intn(90_000))
|
|
|
|
subjects := make([]string, 0, totalSubjects)
|
|
for i := 0; i < totalSubjects; i++ {
|
|
subjects = append(subjects, fmt.Sprintf("ev.%s", nuid.Next()))
|
|
}
|
|
|
|
// Put in 100k msgs with random subjects.
|
|
msg := bytes.Repeat([]byte("ZZZ"), 333)
|
|
for i := 0; i < totalMsgs; i++ {
|
|
_, _, err = fs.StoreMsg(subjects[rand.Intn(totalSubjects)], nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Now we want to do a calculate NumPendingMulti.
|
|
filters := gsl.NewSublist[struct{}]()
|
|
for filters.Count() < uint32(numFiltered) {
|
|
filter := subjects[rand.Intn(totalSubjects)]
|
|
if !filters.HasInterest(filter) {
|
|
filters.Insert(filter, struct{}{})
|
|
}
|
|
}
|
|
|
|
// Use new function.
|
|
total, _, err := fs.NumPendingMulti(startSeq, filters, false)
|
|
require_NoError(t, err)
|
|
|
|
// Check our results.
|
|
var checkTotal uint64
|
|
var smv StoreMsg
|
|
for seq := startSeq; seq <= uint64(totalMsgs); seq++ {
|
|
sm, err := fs.LoadMsg(seq, &smv)
|
|
require_NoError(t, err)
|
|
if filters.HasInterest(sm.subj) {
|
|
checkTotal++
|
|
}
|
|
}
|
|
require_Equal(t, total, checkTotal)
|
|
}
|
|
|
|
func TestFileStoreMessageTTL(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
ttl := int64(1) // 1 second
|
|
|
|
for i := 1; i <= 10; i++ {
|
|
_, _, err = fs.StoreMsg("test", nil, nil, ttl)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
require_Equal(t, ss.Msgs, 10)
|
|
|
|
time.Sleep(time.Second * 2)
|
|
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 11)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
require_Equal(t, ss.Msgs, 0)
|
|
}
|
|
|
|
func TestFileStoreMessageTTLRestart(t *testing.T) {
|
|
dir := t.TempDir()
|
|
|
|
t.Run("BeforeRestart", func(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
ttl := int64(1) // 1 second
|
|
|
|
for i := 1; i <= 10; i++ {
|
|
_, _, err = fs.StoreMsg("test", nil, nil, ttl)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
require_Equal(t, ss.Msgs, 10)
|
|
})
|
|
|
|
t.Run("AfterRestart", func(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
require_Equal(t, ss.Msgs, 10)
|
|
|
|
time.Sleep(time.Second * 2)
|
|
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 11)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
require_Equal(t, ss.Msgs, 0)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMessageTTLRecovered(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
dir := t.TempDir()
|
|
|
|
t.Run("BeforeRestart", func(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
ttl := int64(1) // 1 second
|
|
|
|
// Make the first message with no ttl.
|
|
// This exposes a bug we had in recovery.
|
|
_, _, err = fs.StoreMsg("test", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
for i := 0; i < 9; i++ {
|
|
// When the timed hash wheel state is deleted, the only way we can recover
|
|
// the TTL is to look at the original message header, therefore the TTL
|
|
// must be in the headers for this test to work.
|
|
hdr := fmt.Appendf(nil, "NATS/1.0\r\n%s: %d\r\n", JSMessageTTL, ttl)
|
|
_, _, err = fs.StoreMsg("test", hdr, nil, ttl)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
require_Equal(t, ss.Msgs, 10)
|
|
})
|
|
|
|
t.Run("AfterRestart", func(t *testing.T) {
|
|
// Delete the timed hash wheel state so that we are forced to do a linear scan
|
|
// of message blocks containing TTL'd messages.
|
|
fn := filepath.Join(dir, msgDir, ttlStreamStateFile)
|
|
require_NoError(t, os.RemoveAll(fn))
|
|
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
fs.mu.RLock()
|
|
mb := fs.blks[0]
|
|
fs.mu.RUnlock()
|
|
mb.mu.RLock()
|
|
ttls := mb.ttls
|
|
mb.mu.RUnlock()
|
|
|
|
require_Equal(t, ttls, 9)
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
require_Equal(t, ss.Msgs, 10)
|
|
|
|
time.Sleep(time.Second * 2)
|
|
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 10)
|
|
require_Equal(t, ss.Msgs, 1)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMessageTTLRecoveredSingleMessageWithoutStreamState(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
dir := t.TempDir()
|
|
|
|
t.Run("BeforeRestart", func(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
ttl := int64(1) // 1 second
|
|
hdr := fmt.Appendf(nil, "NATS/1.0\r\n%s: %d\r\n", JSMessageTTL, ttl)
|
|
_, _, err = fs.StoreMsg("test", hdr, nil, ttl)
|
|
require_NoError(t, err)
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 1)
|
|
require_Equal(t, ss.Msgs, 1)
|
|
})
|
|
|
|
t.Run("AfterRestart", func(t *testing.T) {
|
|
// Delete the stream state file so that we need to rebuild.
|
|
fn := filepath.Join(dir, msgDir, streamStreamStateFile)
|
|
require_NoError(t, os.RemoveAll(fn))
|
|
// Delete the timed hash wheel state so that we are forced to do a linear scan
|
|
// of message blocks containing TTL'd messages.
|
|
fn = filepath.Join(dir, msgDir, ttlStreamStateFile)
|
|
require_NoError(t, os.RemoveAll(fn))
|
|
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 1)
|
|
require_Equal(t, ss.Msgs, 1)
|
|
|
|
time.Sleep(time.Second * 2)
|
|
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 2)
|
|
require_Equal(t, ss.LastSeq, 1)
|
|
require_Equal(t, ss.Msgs, 0)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMessageTTLWriteTombstone(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
dir := t.TempDir()
|
|
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
ttl := int64(1)
|
|
|
|
// When the timed hash wheel state is deleted, the only way we can recover
|
|
// the TTL is to look at the original message header, therefore the TTL
|
|
// must be in the headers for this test to work.
|
|
hdr := fmt.Appendf(nil, "NATS/1.0\r\n%s: %d\r\n", JSMessageTTL, ttl)
|
|
_, _, err = fs.StoreMsg("test", hdr, nil, ttl)
|
|
require_NoError(t, err)
|
|
|
|
// Publish another message, but without TTL.
|
|
_, _, err = fs.StoreMsg("test", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.Msgs, 2)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 2)
|
|
|
|
checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
|
|
if fs.State().FirstSeq == 1 {
|
|
return errors.New("message not expired yet")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.Msgs, 1)
|
|
require_Equal(t, ss.FirstSeq, 2)
|
|
require_Equal(t, ss.LastSeq, 2)
|
|
|
|
fs.Stop()
|
|
|
|
// Delete the stream state file so that we need to rebuild.
|
|
// Should have written a tombstone so we can properly recover.
|
|
fn := filepath.Join(dir, msgDir, streamStreamStateFile)
|
|
require_NoError(t, os.RemoveAll(fn))
|
|
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.Msgs, 1)
|
|
require_Equal(t, ss.FirstSeq, 2)
|
|
require_Equal(t, ss.LastSeq, 2)
|
|
}
|
|
|
|
func TestFileStoreMessageTTLRecoveredOffByOne(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
dir := t.TempDir()
|
|
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
ts := time.Now().UnixNano()
|
|
ttl := int64(120) // 2 minutes
|
|
expires := time.Duration(ts) + (time.Second * time.Duration(ttl))
|
|
|
|
// When the timed hash wheel state is deleted, the only way we can recover
|
|
// the TTL is to look at the original message header, therefore the TTL
|
|
// must be in the headers for this test to work.
|
|
hdr := fmt.Appendf(nil, "NATS/1.0\r\n%s: %d\r\n", JSMessageTTL, ttl)
|
|
require_NoError(t, fs.StoreRawMsg("test", hdr, nil, 1, ts, ttl, false))
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.Msgs, 1)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 1)
|
|
|
|
fs.mu.Lock()
|
|
ttlc := fs.ttls.Count()
|
|
// Adding to the THW is idempotent, so we sneakily remove it here
|
|
// so we can check it doesn't get re-added during restart.
|
|
err = fs.ttls.Remove(1, int64(expires))
|
|
fs.mu.Unlock()
|
|
require_Equal(t, ttlc, 1)
|
|
require_NoError(t, err)
|
|
|
|
fs.Stop()
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.Msgs, 1)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 1)
|
|
|
|
fs.mu.Lock()
|
|
ttlc = fs.ttls.Count()
|
|
fs.mu.Unlock()
|
|
require_Equal(t, ttlc, 0)
|
|
}
|
|
|
|
func TestFileStoreDontSpamCompactWhenMostlyTombstones(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir(), BlockSize: defaultMediumBlockSize},
|
|
StreamConfig{Name: "TEST", Subjects: []string{"foo"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Store a bunch of messages, ensuring we get enough blocks.
|
|
msg := bytes.Repeat([]byte("X"), 100)
|
|
totalBlksAfterStore := 6
|
|
expectedLseq := uint64(31_536*(totalBlksAfterStore-1) + 2)
|
|
for seq := uint64(1); seq <= expectedLseq; seq++ {
|
|
_, _, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Confirms we have the required amount of blocks.
|
|
fs.mu.RLock()
|
|
lenBlks := len(fs.blks)
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
require_Len(t, lenBlks, totalBlksAfterStore)
|
|
|
|
// Ensure the last block contains the last two messages.
|
|
lmb.mu.RLock()
|
|
fseq, lseq := lmb.first.seq, lmb.last.seq
|
|
lmb.mu.RUnlock()
|
|
require_Equal(t, fseq, expectedLseq-1)
|
|
require_Equal(t, lseq, expectedLseq)
|
|
|
|
// Remove all messages before the last block, will fill up last block with tombstones.
|
|
for seq := uint64(1); seq < fseq; seq++ {
|
|
removed, err := fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
}
|
|
|
|
// The last block will be filled with so many tombstones that
|
|
// a new message block will be created to store the rest.
|
|
fs.mu.RLock()
|
|
lenBlks = len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Len(t, lenBlks, 2)
|
|
|
|
fs.mu.Lock()
|
|
defer fs.mu.Unlock()
|
|
|
|
fmb := fs.blks[0]
|
|
fmb.mu.Lock()
|
|
defer fmb.mu.Unlock()
|
|
|
|
// We don't call fs.RemoveMsg as that would call into compact.
|
|
// Instead, we mark as deleted and check compaction ourselves.
|
|
fmb.dmap.Insert(expectedLseq)
|
|
fmb.bytes /= 2
|
|
|
|
// This message block takes up ~4MB but contains only one ~100 bytes message, and the rest is all tombstones.
|
|
// We should allow trying to compact.
|
|
require_Equal(t, fmb.bytes, 133)
|
|
require_Equal(t, fmb.cbytes, 0)
|
|
require_True(t, fmb.shouldCompactInline())
|
|
|
|
// Compact will be successful, but since it doesn't clean up tombstones it will be ineffective.
|
|
require_NoError(t, fmb.compact())
|
|
|
|
// We should not allow compacting again as we're not removing tombstones inline.
|
|
// Otherwise, we would spam compaction.
|
|
require_False(t, fmb.shouldCompactInline())
|
|
|
|
// Just checking fmb.cbytes is tracking the previous value of fmb.bytes,
|
|
// so it can block compaction until enough data has been removed.
|
|
require_Equal(t, fmb.bytes, 133)
|
|
require_Equal(t, fmb.cbytes, fmb.bytes)
|
|
|
|
// Simulate having removed sufficient bytes and being allowed to compact again.
|
|
fmb.bytes /= 2
|
|
require_True(t, fmb.shouldCompactInline())
|
|
}
|
|
|
|
func TestFileStoreSubjectDeleteMarkers(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{
|
|
Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage,
|
|
MaxAge: time.Second, AllowMsgTTL: true,
|
|
SubjectDeleteMarkerTTL: time.Second,
|
|
},
|
|
)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Capture subject delete marker proposals.
|
|
ch := make(chan *inMsg, 1)
|
|
fs.rmcb = func(seq uint64) {
|
|
_, err := fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
}
|
|
fs.pmsgcb = func(im *inMsg) {
|
|
ch <- im
|
|
}
|
|
|
|
// Store three messages that will expire because of MaxAge.
|
|
for i := 0; i < 3; i++ {
|
|
_, _, err = fs.StoreMsg("test", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Wait for MaxAge to pass.
|
|
time.Sleep(time.Second + time.Millisecond*500)
|
|
|
|
// We should have placed a subject delete marker.
|
|
im := require_ChanRead(t, ch, time.Second*5)
|
|
require_Equal(t, bytesToString(getHeader(JSMarkerReason, im.hdr)), JSMarkerReasonMaxAge)
|
|
require_Equal(t, bytesToString(getHeader(JSMessageTTL, im.hdr)), "1s")
|
|
}
|
|
|
|
func TestFileStoreStoreRawMessageThrowsPermissionErrorIfFSModeReadOnly(t *testing.T) {
|
|
// Test fails in Buildkite environment. Skip it.
|
|
skipIfBuildkite(t)
|
|
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"ev.1"}, Storage: FileStorage}
|
|
fs, err := newFileStore(FileStoreConfig{StoreDir: t.TempDir(), BlockSize: 1024}, cfg)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
READONLY_MODE := os.FileMode(0o555)
|
|
ORIGINAL_FILE_MODE, err := os.Stat(fs.fcfg.StoreDir)
|
|
require_NoError(t, err)
|
|
require_NoError(t, changeDirectoryPermission(fs.fcfg.StoreDir, READONLY_MODE))
|
|
defer func() {
|
|
require_NoError(t, changeDirectoryPermission(fs.fcfg.StoreDir, ORIGINAL_FILE_MODE.Mode()))
|
|
}()
|
|
|
|
totalMsgs := 10000
|
|
msg := bytes.Repeat([]byte("Z"), 1024)
|
|
for i := 0; i < totalMsgs; i++ {
|
|
if _, _, err = fs.StoreMsg("ev.1", nil, msg, 0); err != nil {
|
|
break
|
|
}
|
|
}
|
|
require_Error(t, err, os.ErrPermission)
|
|
}
|
|
|
|
func TestFileStoreWriteFullStateThrowsPermissionErrorIfFSModeReadOnly(t *testing.T) {
|
|
// Test fails in Buildkite environment. Skip it.
|
|
skipIfBuildkite(t)
|
|
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"ev.1"}, Storage: FileStorage}
|
|
fs, err := newFileStore(FileStoreConfig{StoreDir: t.TempDir()}, cfg)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
READONLY_MODE := os.FileMode(0o555)
|
|
ORIGINAL_FILE_MODE, err := os.Stat(fs.fcfg.StoreDir)
|
|
require_NoError(t, err)
|
|
|
|
totalMsgs := 10000
|
|
msg := bytes.Repeat([]byte("Z"), 1024)
|
|
for i := 0; i < totalMsgs; i++ {
|
|
if _, _, err := fs.StoreMsg("ev.1", nil, msg, 0); err != nil {
|
|
break
|
|
}
|
|
}
|
|
|
|
require_NoError(t, changeDirectoryPermission(fs.fcfg.StoreDir, READONLY_MODE))
|
|
require_Error(t, fs.writeFullState(), os.ErrPermission)
|
|
require_NoError(t, changeDirectoryPermission(fs.fcfg.StoreDir, ORIGINAL_FILE_MODE.Mode()))
|
|
}
|
|
|
|
func changeDirectoryPermission(directory string, mode fs.FileMode) error {
|
|
err := filepath.Walk(directory, func(path string, info os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return fmt.Errorf("error accessing path %q: %w", path, err)
|
|
}
|
|
|
|
// Check if the path is a directory or file and set permissions accordingly
|
|
if info.IsDir() {
|
|
err = os.Chmod(path, mode)
|
|
if err != nil {
|
|
return fmt.Errorf("error changing directory permissions for %q: %w", path, err)
|
|
}
|
|
} else {
|
|
err = os.Chmod(path, mode)
|
|
if err != nil {
|
|
return fmt.Errorf("error changing file permissions for %q: %w", path, err)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
return err
|
|
}
|
|
|
|
func TestFileStoreLeftoverSkipMsgInDmap(t *testing.T) {
|
|
storeDir := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: storeDir},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test.*"}, Storage: FileStorage, MaxMsgsPer: 1},
|
|
)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
getLmbState := func(fs *fileStore) (uint64, uint64, int) {
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
lmb.mu.RLock()
|
|
fseq := atomic.LoadUint64(&lmb.first.seq)
|
|
lseq := atomic.LoadUint64(&lmb.last.seq)
|
|
dmaps := lmb.dmap.Size()
|
|
lmb.mu.RUnlock()
|
|
return fseq, lseq, dmaps
|
|
}
|
|
|
|
// Only skip a message.
|
|
fs.SkipMsg(0)
|
|
|
|
// Confirm state.
|
|
state := fs.State()
|
|
require_Equal(t, state.FirstSeq, 2)
|
|
require_Equal(t, state.LastSeq, 1)
|
|
require_Equal(t, state.NumDeleted, 0)
|
|
fseq, lseq, dmaps := getLmbState(fs)
|
|
require_Equal(t, fseq, 2)
|
|
require_Equal(t, lseq, 1)
|
|
require_Len(t, dmaps, 0)
|
|
|
|
// Stop without writing index.db so we recover based on just the blk file.
|
|
require_NoError(t, fs.stop(false, false))
|
|
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: storeDir},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test.*"}, Storage: FileStorage, MaxMsgsPer: 1},
|
|
)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Confirm the skipped message is not included in the deletes.
|
|
state = fs.State()
|
|
require_Equal(t, state.FirstSeq, 2)
|
|
require_Equal(t, state.LastSeq, 1)
|
|
require_Equal(t, state.NumDeleted, 0)
|
|
fseq, lseq, dmaps = getLmbState(fs)
|
|
require_Equal(t, fseq, 2)
|
|
require_Equal(t, lseq, 1)
|
|
require_Len(t, dmaps, 0)
|
|
}
|
|
|
|
func TestFileStoreRecoverOnlyBlkFiles(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
// Confirm state as baseline.
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 1)
|
|
require_Equal(t, before.FirstSeq, 1)
|
|
require_Equal(t, before.LastSeq, 1)
|
|
|
|
// Restart should equal state.
|
|
require_NoError(t, fs.Stop())
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of %+v, got %+v", before, state)
|
|
}
|
|
|
|
// Stream state should exist.
|
|
_, err = os.Stat(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
require_NoError(t, err)
|
|
|
|
// Stop and write some random files, but containing ".blk", should be ignored.
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.WriteFile(filepath.Join(fs.fcfg.StoreDir, msgDir, "10.blk.random"), nil, defaultFilePerms))
|
|
require_NoError(t, os.WriteFile(filepath.Join(fs.fcfg.StoreDir, msgDir, fmt.Sprintf("10.blk.%s", blkTmpSuffix)), nil, defaultFilePerms))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// The random files would previously result in stream state to be deleted.
|
|
_, err = os.Stat(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
require_NoError(t, err)
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of %+v, got %+v", before, state)
|
|
}
|
|
|
|
// Stop and remove stream state file.
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)))
|
|
|
|
// Recovering based on blocks should also ignore the random files.
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of %+v, got %+v", before, state)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreRecoverAfterRemoveOperation(t *testing.T) {
|
|
tests := []struct {
|
|
title string
|
|
action func(fs *fileStore)
|
|
validate func(state StreamState)
|
|
}{
|
|
{
|
|
title: "None",
|
|
action: func(fs *fileStore) {},
|
|
validate: func(state StreamState) {
|
|
require_Equal(t, state.Msgs, 4)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 4)
|
|
},
|
|
},
|
|
{
|
|
title: "RemoveMsg",
|
|
action: func(fs *fileStore) {
|
|
removed, err := fs.RemoveMsg(1)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
},
|
|
validate: func(state StreamState) {
|
|
require_Equal(t, state.Msgs, 3)
|
|
require_Equal(t, state.FirstSeq, 2)
|
|
require_Equal(t, state.LastSeq, 4)
|
|
},
|
|
},
|
|
{
|
|
title: "EraseMsg",
|
|
action: func(fs *fileStore) {
|
|
erased, err := fs.EraseMsg(1)
|
|
require_NoError(t, err)
|
|
require_True(t, erased)
|
|
},
|
|
validate: func(state StreamState) {
|
|
require_Equal(t, state.Msgs, 3)
|
|
require_Equal(t, state.FirstSeq, 2)
|
|
require_Equal(t, state.LastSeq, 4)
|
|
},
|
|
},
|
|
{
|
|
title: "Purge",
|
|
action: func(fs *fileStore) {
|
|
purged, err := fs.Purge()
|
|
require_NoError(t, err)
|
|
require_Equal(t, purged, 4)
|
|
},
|
|
validate: func(state StreamState) {
|
|
require_Equal(t, state.Msgs, 0)
|
|
require_Equal(t, state.FirstSeq, 5)
|
|
require_Equal(t, state.LastSeq, 4)
|
|
},
|
|
},
|
|
{
|
|
title: "PurgeEx-0",
|
|
action: func(fs *fileStore) {
|
|
purged, err := fs.PurgeEx("foo.0", 0, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, purged, 2)
|
|
},
|
|
validate: func(state StreamState) {
|
|
require_Equal(t, state.Msgs, 2)
|
|
require_Equal(t, state.FirstSeq, 2)
|
|
require_Equal(t, state.LastSeq, 4)
|
|
require_Equal(t, state.NumDeleted, 1)
|
|
},
|
|
},
|
|
{
|
|
title: "PurgeEx-1",
|
|
action: func(fs *fileStore) {
|
|
purged, err := fs.PurgeEx("foo.1", 0, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, purged, 2)
|
|
},
|
|
validate: func(state StreamState) {
|
|
require_Equal(t, state.Msgs, 2)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 4)
|
|
require_Equal(t, state.NumDeleted, 2)
|
|
},
|
|
},
|
|
{
|
|
title: "Compact",
|
|
action: func(fs *fileStore) {
|
|
purged, err := fs.Compact(3)
|
|
require_NoError(t, err)
|
|
require_Equal(t, purged, 2)
|
|
},
|
|
validate: func(state StreamState) {
|
|
require_Equal(t, state.Msgs, 2)
|
|
require_Equal(t, state.FirstSeq, 3)
|
|
require_Equal(t, state.LastSeq, 4)
|
|
},
|
|
},
|
|
{
|
|
title: "Truncate",
|
|
action: func(fs *fileStore) {
|
|
require_NoError(t, fs.Truncate(2))
|
|
},
|
|
validate: func(state StreamState) {
|
|
require_Equal(t, state.Msgs, 2)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 2)
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.title, func(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo.*"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 0; i < 4; i++ {
|
|
subject := fmt.Sprintf("foo.%d", i%2)
|
|
_, _, err = fs.StoreMsg(subject, nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
test.action(fs)
|
|
|
|
// Confirm state as baseline.
|
|
before := fs.State()
|
|
test.validate(before)
|
|
|
|
// Restart should equal state.
|
|
require_NoError(t, fs.Stop())
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of:\n%+v, got:\n%+v", before, state)
|
|
}
|
|
|
|
// Stop and remove stream state file.
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)))
|
|
|
|
// Recovering based on blocks should result in the same state.
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of:\n%+v, got:\n%+v", before, state)
|
|
}
|
|
|
|
// Rebuilding state must also result in the same state.
|
|
fs.rebuildState(nil)
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of:\n%+v, got:\n%+v", before, state)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFileStoreRecoverAfterCompact(t *testing.T) {
|
|
/*
|
|
fs.Compact may rewrite the .blk file if it's large enough. In which case we don't
|
|
need to write tombstones. But if the rewrite isn't done, tombstones must be placed
|
|
to ensure we can properly recover without the index.db file.
|
|
*/
|
|
for _, test := range []struct {
|
|
payloadSize int
|
|
usesTombstones bool
|
|
}{
|
|
{payloadSize: 1024, usesTombstones: true},
|
|
{payloadSize: 1024 * 1024, usesTombstones: false},
|
|
} {
|
|
t.Run(strconv.Itoa(test.payloadSize), func(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subject := "foo"
|
|
payload := make([]byte, test.payloadSize)
|
|
for i := 0; i < 4; i++ {
|
|
_, _, err = fs.StoreMsg(subject, nil, payload, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Confirm state before compacting.
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 4)
|
|
require_Equal(t, before.FirstSeq, 1)
|
|
require_Equal(t, before.LastSeq, 4)
|
|
require_Equal(t, before.Bytes, uint64(emptyRecordLen+len(subject)+test.payloadSize)*4)
|
|
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
|
|
// Underlying message block should report the same.
|
|
if fcfg.Cipher == NoCipher && fcfg.Compression == NoCompression {
|
|
lmb.mu.RLock()
|
|
bytes, rbytes := lmb.bytes, lmb.rbytes
|
|
lmb.mu.RUnlock()
|
|
size := uint64(emptyRecordLen+len(subject)+test.payloadSize) * 4
|
|
require_Equal(t, bytes, size)
|
|
require_Equal(t, rbytes, size)
|
|
}
|
|
|
|
// Now compact.
|
|
purged, err := fs.Compact(4)
|
|
require_NoError(t, err)
|
|
require_Equal(t, purged, 3)
|
|
|
|
// Confirm state after compacting.
|
|
// Bytes should reflect only having a single message left.
|
|
before = fs.State()
|
|
require_Equal(t, before.Msgs, 1)
|
|
require_Equal(t, before.FirstSeq, 4)
|
|
require_Equal(t, before.LastSeq, 4)
|
|
require_Equal(t, before.Bytes, uint64(emptyRecordLen+len(subject)+test.payloadSize))
|
|
|
|
// Underlying message block should report the same.
|
|
// Unless the compact didn't rewrite the block but used tombstones,
|
|
// in which case we expect the raw bytes to include them.
|
|
if fcfg.Cipher == NoCipher && fcfg.Compression == NoCompression {
|
|
lmb.mu.RLock()
|
|
bytes, rbytes := lmb.bytes, lmb.rbytes
|
|
lmb.mu.RUnlock()
|
|
size := uint64(emptyRecordLen + len(subject) + test.payloadSize)
|
|
require_Equal(t, bytes, size)
|
|
if test.usesTombstones {
|
|
// 4 messages, 3 tombstones
|
|
size = uint64(emptyRecordLen+len(subject)+test.payloadSize)*4 + uint64(emptyRecordLen)*3
|
|
}
|
|
require_Equal(t, rbytes, size)
|
|
}
|
|
|
|
// Restart should equal state.
|
|
require_NoError(t, fs.Stop())
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of:\n%+v, got:\n%+v", before, state)
|
|
}
|
|
|
|
// Stop and remove stream state file.
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)))
|
|
|
|
// Recovering based on blocks should result in the same state.
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of:\n%+v, got:\n%+v", before, state)
|
|
}
|
|
|
|
// Rebuilding state must also result in the same state.
|
|
fs.rebuildState(nil)
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of:\n%+v, got:\n%+v", before, state)
|
|
}
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFileStoreRecoverWithEmptyMessageBlock(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
// 4 messages with subject 'foo' and no payload.
|
|
fcfg.BlockSize = 33 * 4
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// First message block contains 4 messages.
|
|
for i := 0; i < 4; i++ {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
fs.mu.RLock()
|
|
lblks := len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Len(t, lblks, 1)
|
|
|
|
// Second (empty) message block only contains 2 tombstones.
|
|
for i := uint64(1); i <= 2; i++ {
|
|
removed, err := fs.RemoveMsg(i)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
}
|
|
|
|
fs.mu.RLock()
|
|
lblks = len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Len(t, lblks, 2)
|
|
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 2)
|
|
require_Equal(t, before.FirstSeq, 3)
|
|
require_Equal(t, before.LastSeq, 4)
|
|
|
|
// Restart should equal state.
|
|
require_NoError(t, fs.Stop())
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of:\n%+v, got:\n%+v", before, state)
|
|
}
|
|
|
|
// Stop and remove stream state file.
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)))
|
|
|
|
// Recovering based on blocks should result in the same state.
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of:\n%+v, got:\n%+v", before, state)
|
|
}
|
|
|
|
// Rebuilding state must also result in the same state.
|
|
fs.rebuildState(nil)
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of:\n%+v, got:\n%+v", before, state)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreRemoveMsgBlockFirst(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
dir := t.TempDir()
|
|
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("test", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.Msgs, 1)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 1)
|
|
|
|
fs.Stop()
|
|
|
|
for _, f := range []string{streamStreamStateFile, "1.blk"} {
|
|
fn := filepath.Join(dir, msgDir, f)
|
|
require_NoError(t, os.RemoveAll(fn))
|
|
}
|
|
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// If the block is removed first, we have nothing to recover. So starting out empty would be expected.
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.Msgs, 0)
|
|
require_Equal(t, ss.FirstSeq, 0)
|
|
require_Equal(t, ss.LastSeq, 0)
|
|
}
|
|
|
|
func TestFileStoreRemoveMsgBlockLast(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
dir := t.TempDir()
|
|
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("test", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.Msgs, 1)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 1)
|
|
|
|
// Copy first block so we can put it back later.
|
|
ofn := filepath.Join(dir, msgDir, "1.blk")
|
|
nfn := filepath.Join(dir, msgDir, "1.blk.cp")
|
|
require_NoError(t, os.Rename(ofn, nfn))
|
|
|
|
// Removing the last message will result in '2.blk' to be created, and '1.blk' to be removed.
|
|
_, err = fs.RemoveMsg(1)
|
|
require_NoError(t, err)
|
|
_, err = os.Stat(filepath.Join(dir, msgDir, "2.blk"))
|
|
require_NoError(t, err)
|
|
_, err = os.Stat(ofn)
|
|
require_True(t, os.IsNotExist(err))
|
|
|
|
fs.Stop()
|
|
|
|
// Remove index.db so we need to recover based on blocks.
|
|
fn := filepath.Join(dir, msgDir, streamStreamStateFile)
|
|
require_NoError(t, os.RemoveAll(fn))
|
|
|
|
// Put back '1.blk' file to simulate being hard killed right
|
|
// after creating '2.blk' but before cleaning up '1.blk'.
|
|
require_NoError(t, os.Rename(nfn, ofn))
|
|
_, err = os.Stat(ofn)
|
|
require_False(t, os.IsNotExist(err))
|
|
|
|
fs, err = newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"test"}, Storage: FileStorage, AllowMsgTTL: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Should recognize correct state, and remove '1.blk'.
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.Msgs, 0)
|
|
require_Equal(t, ss.FirstSeq, 2)
|
|
require_Equal(t, ss.LastSeq, 1)
|
|
_, err = os.Stat(ofn)
|
|
require_True(t, os.IsNotExist(err))
|
|
}
|
|
|
|
func TestFileStoreAllLastSeqs(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"*.*"}, MaxMsgsPer: 50, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subjs := []string{"foo.foo", "foo.bar", "foo.baz", "bar.foo", "bar.bar", "bar.baz"}
|
|
msg := []byte("abc")
|
|
|
|
for i := 0; i < 100_000; i++ {
|
|
subj := subjs[rand.Intn(len(subjs))]
|
|
fs.StoreMsg(subj, nil, msg, 0)
|
|
}
|
|
|
|
expected := make([]uint64, 0, len(subjs))
|
|
var smv StoreMsg
|
|
for _, subj := range subjs {
|
|
sm, err := fs.LoadLastMsg(subj, &smv)
|
|
require_NoError(t, err)
|
|
expected = append(expected, sm.seq)
|
|
}
|
|
slices.Sort(expected)
|
|
|
|
seqs, err := fs.AllLastSeqs()
|
|
require_NoError(t, err)
|
|
require_True(t, reflect.DeepEqual(seqs, expected))
|
|
}
|
|
|
|
func TestFileStoreRecoverDoesNotResetStreamState(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"ev.1"}, Storage: FileStorage, MaxAge: 2 * time.Second, Retention: WorkQueuePolicy}
|
|
created := time.Now()
|
|
fcfg.BlockSize = 1024
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
subj, msg := "foo", []byte("Hello World")
|
|
toStore := 500
|
|
for i := 0; i < toStore; i++ {
|
|
_, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
fs.mu.RLock()
|
|
blks := len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_True(t, blks > 1)
|
|
|
|
// Simulate a consumer consuming all messages, but this test
|
|
// expires all messages from the stream instead.
|
|
time.Sleep(2500 * time.Millisecond)
|
|
|
|
// Capture the state before stopping the store.
|
|
fs.mu.RLock()
|
|
blks = len(fs.blks)
|
|
var mfn string
|
|
if blks > 0 {
|
|
mfn = fs.blks[0].mfn
|
|
}
|
|
fs.mu.RUnlock()
|
|
|
|
// Stream state should exist after shutting down.
|
|
require_NoError(t, fs.Stop())
|
|
_, err = os.Stat(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
require_NoError(t, err)
|
|
|
|
// A single block should have remained, but remove it so we need to restore from the index file.
|
|
// The first/last sequences should be preserved and restored from the index.
|
|
require_Len(t, blks, 1)
|
|
require_NoError(t, os.Remove(mfn))
|
|
|
|
fs, err = newFileStoreWithCreated(fs.fcfg, cfg, time.Now(), prf(&fs.fcfg), nil)
|
|
require_NoError(t, err)
|
|
fs.mu.RLock()
|
|
defer fs.mu.RUnlock()
|
|
require_True(t, fs.state.FirstSeq|fs.state.LastSeq != 0)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreAccessTimeSpinUp(t *testing.T) {
|
|
// In case running lots of tests.
|
|
time.Sleep(time.Second)
|
|
ngr := runtime.NumGoroutine()
|
|
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: t.TempDir()},
|
|
StreamConfig{Name: "zzz", Subjects: []string{"*.*"}, Storage: FileStorage})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
at := ats.AccessTime()
|
|
require_True(t, at != 0)
|
|
|
|
// Now check we also cleanup.
|
|
fs.Stop()
|
|
time.Sleep(2 * ats.TickInterval)
|
|
ngra := runtime.NumGoroutine()
|
|
require_Equal(t, ngr, ngra)
|
|
}
|
|
|
|
func TestFileStoreUpdateConfigTTLState(t *testing.T) {
|
|
cfg := StreamConfig{
|
|
Name: "zzz",
|
|
Subjects: []string{">"},
|
|
Storage: FileStorage,
|
|
}
|
|
fs, err := newFileStore(FileStoreConfig{StoreDir: t.TempDir()}, cfg)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
require_Equal(t, fs.ttls, nil)
|
|
|
|
cfg.AllowMsgTTL = true
|
|
require_NoError(t, fs.UpdateConfig(&cfg))
|
|
require_NotEqual(t, fs.ttls, nil)
|
|
|
|
cfg.AllowMsgTTL = false
|
|
require_NoError(t, fs.UpdateConfig(&cfg))
|
|
require_Equal(t, fs.ttls, nil)
|
|
}
|
|
|
|
func TestFileStoreSubjectForSeq(t *testing.T) {
|
|
cfg := StreamConfig{
|
|
Name: "foo",
|
|
Subjects: []string{"foo.>"},
|
|
Storage: FileStorage,
|
|
}
|
|
fs, err := newFileStore(FileStoreConfig{StoreDir: t.TempDir()}, cfg)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
seq, _, err := fs.StoreMsg("foo.bar", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, seq, 1)
|
|
|
|
_, err = fs.SubjectForSeq(0)
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
|
|
subj, err := fs.SubjectForSeq(1)
|
|
require_NoError(t, err)
|
|
require_Equal(t, subj, "foo.bar")
|
|
|
|
_, err = fs.SubjectForSeq(2)
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
}
|
|
|
|
func BenchmarkFileStoreSubjectAccesses(b *testing.B) {
|
|
fs, err := newFileStore(FileStoreConfig{StoreDir: b.TempDir()}, StreamConfig{
|
|
Name: "foo",
|
|
Subjects: []string{"foo.>"},
|
|
Storage: FileStorage,
|
|
})
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
seq, _, err := fs.StoreMsg("foo.bar", nil, []byte{1, 2, 3, 4, 5}, 0)
|
|
require_NoError(b, err)
|
|
require_Equal(b, seq, 1)
|
|
|
|
b.Run("SubjectForSeq", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for range b.N {
|
|
subj, err := fs.SubjectForSeq(1)
|
|
require_NoError(b, err)
|
|
require_Equal(b, subj, "foo.bar")
|
|
}
|
|
})
|
|
|
|
b.Run("LoadMsg", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for range b.N {
|
|
// smv is deliberately inside the loop here because that's
|
|
// effectively what is happening with needAck.
|
|
var smv StoreMsg
|
|
sm, err := fs.LoadMsg(1, &smv)
|
|
require_NoError(b, err)
|
|
require_Equal(b, sm.subj, "foo.bar")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreFirstMatchingMultiExpiry(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo.>"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("foo.foo", nil, []byte("A"), 0)
|
|
require_NoError(t, err)
|
|
|
|
_, _, err = fs.StoreMsg("foo.foo", nil, []byte("B"), 0)
|
|
require_NoError(t, err)
|
|
|
|
_, _, err = fs.StoreMsg("foo.foo", nil, []byte("C"), 0)
|
|
require_NoError(t, err)
|
|
|
|
fs.mu.RLock()
|
|
mb := fs.lmb
|
|
mb.tryExpireCacheLocked()
|
|
fs.mu.RUnlock()
|
|
|
|
sl := gsl.NewSublist[struct{}]()
|
|
sl.Insert("foo.foo", struct{}{})
|
|
|
|
_, didLoad, err := mb.firstMatchingMulti(sl, 1, nil)
|
|
require_NoError(t, err)
|
|
require_False(t, didLoad)
|
|
|
|
_, didLoad, err = mb.firstMatchingMulti(sl, 2, nil)
|
|
require_NoError(t, err)
|
|
require_False(t, didLoad)
|
|
|
|
_, didLoad, err = mb.firstMatchingMulti(sl, 3, nil)
|
|
require_NoError(t, err)
|
|
require_True(t, didLoad) // last message, should expire
|
|
})
|
|
}
|
|
|
|
func TestFileStoreNoPanicOnRecoverTTLWithCorruptBlocks(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage, AllowMsgTTL: true}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
hdr := genHeader(nil, JSMessageTTL, "1")
|
|
for i := range 3 {
|
|
if i > 0 {
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
}
|
|
_, _, err = fs.StoreMsg("foo", hdr, []byte("A"), 1)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
fs.mu.Lock()
|
|
if blks := len(fs.blks); blks != 3 {
|
|
fs.mu.Unlock()
|
|
t.Fatalf("Expected 3 blocks, got %d", blks)
|
|
}
|
|
|
|
// Manually corrupt the blocks by removing the second and changing the
|
|
// sequence range for the last to that of the first.
|
|
fmb := fs.blks[0]
|
|
smb := fs.blks[1]
|
|
lmb := fs.lmb
|
|
fseq, lseq := atomic.LoadUint64(&fmb.first.seq), atomic.LoadUint64(&fmb.last.seq)
|
|
smb.mu.Lock()
|
|
fs.removeMsgBlock(smb)
|
|
smb.mu.Unlock()
|
|
fs.mu.Unlock()
|
|
atomic.StoreUint64(&lmb.first.seq, fseq)
|
|
atomic.StoreUint64(&lmb.last.seq, lseq)
|
|
|
|
require_NoError(t, fs.recoverTTLState())
|
|
})
|
|
}
|
|
|
|
func TestFileStoreAsyncTruncate(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 8192
|
|
fcfg.AsyncFlush = true
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
require_NotNil(t, lmb)
|
|
|
|
// Wait for flusher to be ready.
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
lmb.mu.RLock()
|
|
defer lmb.mu.RUnlock()
|
|
if !lmb.flusher {
|
|
return errors.New("flusher not active")
|
|
}
|
|
return nil
|
|
})
|
|
// Now shutdown flusher and wait for it to be closed.
|
|
lmb.mu.Lock()
|
|
if lmb.qch != nil {
|
|
close(lmb.qch)
|
|
lmb.qch = nil
|
|
}
|
|
lmb.mu.Unlock()
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
lmb.mu.RLock()
|
|
defer lmb.mu.RUnlock()
|
|
if lmb.flusher {
|
|
return errors.New("flusher still active")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Write some messages, none of them will have been flushed asynchronously.
|
|
subj, msg := "foo", make([]byte, 100)
|
|
for i := uint64(1); i <= 2; i++ {
|
|
seq, _, err := fs.StoreMsg(subj, nil, msg, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, seq, i)
|
|
}
|
|
// Truncate needs to flush if the data was not yet flushed asynchronously.
|
|
require_NoError(t, fs.Truncate(1))
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, 1)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 1)
|
|
|
|
fs.mu.RLock()
|
|
for _, mb := range fs.blks {
|
|
if mb.pendingWriteSize() > 0 {
|
|
fs.mu.RUnlock()
|
|
t.Fatalf("Message block %d still has pending writes", mb.index)
|
|
}
|
|
}
|
|
fs.mu.RUnlock()
|
|
})
|
|
}
|
|
|
|
func TestFileStoreAsyncFlushOnSkipMsgs(t *testing.T) {
|
|
for _, noFlushLoop := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("NoFlushLoop=%v", noFlushLoop), func(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 8192
|
|
fcfg.AsyncFlush = true
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.mu.RLock()
|
|
fmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
require_NotNil(t, fmb)
|
|
|
|
if noFlushLoop {
|
|
// Wait for flusher to be ready.
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
fmb.mu.RLock()
|
|
defer fmb.mu.RUnlock()
|
|
if !fmb.flusher {
|
|
return errors.New("flusher not active")
|
|
}
|
|
return nil
|
|
})
|
|
// Now shutdown flusher and wait for it to be closed.
|
|
fmb.mu.Lock()
|
|
if fmb.qch != nil {
|
|
close(fmb.qch)
|
|
fmb.qch = nil
|
|
}
|
|
fmb.mu.Unlock()
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
fmb.mu.RLock()
|
|
defer fmb.mu.RUnlock()
|
|
if fmb.flusher {
|
|
return errors.New("flusher still active")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Confirm no pending writes.
|
|
require_Equal(t, fmb.pendingWriteSize(), 0)
|
|
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
if noFlushLoop {
|
|
// Confirm above write is pending.
|
|
require_Equal(t, fmb.pendingWriteSize(), 33)
|
|
}
|
|
|
|
require_NoError(t, fs.SkipMsgs(2, 100_000))
|
|
fs.mu.RLock()
|
|
if blks := len(fs.blks); blks != 2 {
|
|
fs.mu.RUnlock()
|
|
t.Fatalf("Expected 2 blocks, got %d", blks)
|
|
}
|
|
lmb := fs.blks[1]
|
|
fs.mu.RUnlock()
|
|
|
|
// Should have immediately flushed the previous block.
|
|
require_Equal(t, fmb.pendingWriteSize(), 0)
|
|
|
|
// Should eventually flush the last block.
|
|
checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
|
|
if p := lmb.pendingWriteSize(); p > 0 {
|
|
return fmt.Errorf("expected no pending writes, got %d", p)
|
|
}
|
|
return nil
|
|
})
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFileStoreCompressionAfterTruncate(t *testing.T) {
|
|
tests := []struct {
|
|
title string
|
|
action func(fs *fileStore, seq uint64)
|
|
}{
|
|
{
|
|
title: "RemoveMsg",
|
|
action: func(fs *fileStore, seq uint64) {
|
|
removed, err := fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
},
|
|
},
|
|
{
|
|
title: "EraseMsg",
|
|
action: func(fs *fileStore, seq uint64) {
|
|
erased, err := fs.EraseMsg(seq)
|
|
require_NoError(t, err)
|
|
require_True(t, erased)
|
|
},
|
|
},
|
|
{
|
|
title: "Tombstone",
|
|
action: func(fs *fileStore, seq uint64) {
|
|
removed, err := fs.removeMsg(seq, false, false, true)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
for _, recompress := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("%s/Recompress=%v", test.title, recompress), func(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for range 2 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
checkCompressed := func(mb *msgBlock) (bool, error) {
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
buf, err := mb.loadBlock(nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if err = mb.encryptOrDecryptIfNeeded(buf); err != nil {
|
|
return false, err
|
|
}
|
|
var meta CompressionInfo
|
|
if n, err := meta.UnmarshalMetadata(buf); err != nil {
|
|
return false, err
|
|
} else if n == 0 {
|
|
return false, nil
|
|
} else {
|
|
return meta.Algorithm != NoCompression, nil
|
|
}
|
|
}
|
|
|
|
smb := fs.getFirstBlock()
|
|
require_NotNil(t, smb)
|
|
compressed, err := checkCompressed(smb)
|
|
require_NoError(t, err)
|
|
require_False(t, compressed)
|
|
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
|
|
if fcfg.Compression != NoCompression {
|
|
checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
|
|
if compressed, err = checkCompressed(smb); err != nil {
|
|
return err
|
|
} else if !compressed {
|
|
return errors.New("block not compressed yet")
|
|
}
|
|
return nil
|
|
})
|
|
} else {
|
|
compressed, err = checkCompressed(smb)
|
|
require_NoError(t, err)
|
|
require_False(t, compressed)
|
|
}
|
|
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
test.action(fs, 2)
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, 2)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 3)
|
|
require_Equal(t, state.NumDeleted, 1)
|
|
|
|
require_NoError(t, fs.Truncate(2))
|
|
state = fs.State()
|
|
require_Equal(t, state.Msgs, 1)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 2)
|
|
require_Equal(t, state.NumDeleted, 1)
|
|
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
if smb == lmb {
|
|
compressed, err = checkCompressed(smb)
|
|
require_NoError(t, err)
|
|
require_False(t, compressed)
|
|
} else {
|
|
compressed, err = checkCompressed(lmb)
|
|
require_NoError(t, err)
|
|
require_False(t, compressed)
|
|
|
|
if fcfg.Compression != NoCompression {
|
|
checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
|
|
if compressed, err = checkCompressed(smb); err != nil {
|
|
return err
|
|
} else if !compressed {
|
|
return errors.New("block not compressed yet")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
|
|
require_NoError(t, fs.forceWriteFullState())
|
|
|
|
seq, _, err := fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, seq, 3)
|
|
require_NoError(t, fs.forceWriteFullState())
|
|
|
|
smb.mu.Lock()
|
|
smb.clearCacheAndOffset()
|
|
smb.mu.Unlock()
|
|
|
|
require_NoError(t, smb.loadMsgsWithLock())
|
|
compressed, err = checkCompressed(smb)
|
|
require_NoError(t, err)
|
|
if smb == lmb {
|
|
require_False(t, compressed)
|
|
} else {
|
|
require_Equal(t, compressed, fcfg.Compression != NoCompression)
|
|
}
|
|
|
|
compressed, err = checkCompressed(lmb)
|
|
require_NoError(t, err)
|
|
require_False(t, compressed)
|
|
})
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFileStoreTruncateRemovedBlock(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := range 3 {
|
|
if i > 0 {
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
}
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
fs.mu.RLock()
|
|
blks := len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Len(t, blks, 3)
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, 3)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 3)
|
|
require_Equal(t, state.NumDeleted, 0)
|
|
|
|
removed, err := fs.RemoveMsg(2)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
|
|
fs.mu.RLock()
|
|
blks = len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Len(t, blks, 2)
|
|
|
|
fs.mu.RLock()
|
|
blks = len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Len(t, blks, 2)
|
|
|
|
state = fs.State()
|
|
require_Equal(t, state.Msgs, 2)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 3)
|
|
require_Equal(t, state.NumDeleted, 1)
|
|
|
|
require_NoError(t, fs.Truncate(2))
|
|
state = fs.State()
|
|
require_Equal(t, state.Msgs, 1)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 2)
|
|
require_Equal(t, state.NumDeleted, 1)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreAtomicEraseMsg(t *testing.T) {
|
|
for _, lmb := range []bool{true, false} {
|
|
t.Run(fmt.Sprintf("lmb=%v", lmb), func(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for range 3 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
checkCompressed := func(mb *msgBlock) (bool, error) {
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
buf, err := mb.loadBlock(nil)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if err := mb.checkAndLoadEncryption(); err != nil {
|
|
return false, err
|
|
}
|
|
if err = mb.encryptOrDecryptIfNeeded(buf); err != nil {
|
|
return false, err
|
|
}
|
|
var meta CompressionInfo
|
|
if n, err := meta.UnmarshalMetadata(buf); err != nil {
|
|
return false, err
|
|
} else if n == 0 {
|
|
return false, nil
|
|
} else {
|
|
return meta.Algorithm != NoCompression, nil
|
|
}
|
|
}
|
|
|
|
if !lmb {
|
|
smb := fs.getFirstBlock()
|
|
require_NotNil(t, smb)
|
|
|
|
mb, err := fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
compressed, err := checkCompressed(mb)
|
|
require_NoError(t, err)
|
|
require_False(t, compressed)
|
|
|
|
if fcfg.Compression != NoCompression {
|
|
checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
|
|
if compressed, err = checkCompressed(smb); err != nil {
|
|
return err
|
|
} else if !compressed {
|
|
return errors.New("block not compressed yet")
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
}
|
|
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 3)
|
|
require_Equal(t, before.FirstSeq, 1)
|
|
require_Equal(t, before.LastSeq, 3)
|
|
require_Equal(t, before.NumDeleted, 0)
|
|
|
|
removed, err := fs.EraseMsg(2)
|
|
require_NoError(t, err)
|
|
require_True(t, removed)
|
|
|
|
before = fs.State()
|
|
require_Equal(t, before.Msgs, 2)
|
|
require_Equal(t, before.FirstSeq, 1)
|
|
require_Equal(t, before.LastSeq, 3)
|
|
require_Equal(t, before.NumDeleted, 1)
|
|
|
|
seq, _, err := fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, seq, 4)
|
|
before = fs.State()
|
|
|
|
validateCompressed := func() {
|
|
t.Helper()
|
|
fs.mu.Lock()
|
|
defer fs.mu.Unlock()
|
|
for _, mb := range fs.blks {
|
|
compressed, err := checkCompressed(mb)
|
|
require_NoError(t, err)
|
|
if mb == fs.lmb {
|
|
require_False(t, compressed)
|
|
} else {
|
|
require_Equal(t, compressed, fs.fcfg.Compression != NoCompression)
|
|
}
|
|
}
|
|
}
|
|
validateCompressed()
|
|
|
|
// Restart should equal before.
|
|
require_NoError(t, fs.Stop())
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected before of:\n%+v, got:\n%+v", before, state)
|
|
}
|
|
validateCompressed()
|
|
|
|
// Stop and remove stream before file.
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)))
|
|
|
|
// Recovering based on blocks should result in the same before.
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected before of:\n%+v, got:\n%+v", before, state)
|
|
}
|
|
validateCompressed()
|
|
|
|
// Rebuilding before must also result in the same before.
|
|
fs.rebuildState(nil)
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected before of:\n%+v, got:\n%+v", before, state)
|
|
}
|
|
validateCompressed()
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFileStoreRemoveBlockWithStaleStreamState(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := range 3 {
|
|
if i > 0 {
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
}
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Get middle block.
|
|
fs.mu.RLock()
|
|
require_Len(t, len(fs.blks), 3)
|
|
midfn := fs.blks[1].mfn
|
|
fs.mu.RUnlock()
|
|
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Remove(midfn))
|
|
|
|
// Restart.
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := range 3 {
|
|
seq := uint64(i + 1)
|
|
_, err = fs.LoadMsg(seq, nil)
|
|
if seq == 2 {
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
} else {
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMessageSchedule(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
dir := t.TempDir()
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "TEST", Subjects: []string{"foo.*"}, Storage: FileStorage, AllowMsgSchedules: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Capture message schedule proposals.
|
|
ch := make(chan *inMsg, 1)
|
|
fs.pmsgcb = func(im *inMsg) {
|
|
ch <- im
|
|
}
|
|
|
|
// Store a single message schedule.
|
|
schedule := time.Now().Add(time.Second).Format(time.RFC3339Nano)
|
|
hdr := genHeader(nil, JSSchedulePattern, fmt.Sprintf("@at %s", schedule))
|
|
hdr = genHeader(hdr, JSScheduleTarget, "foo.target")
|
|
_, _, err = fs.StoreMsg("foo.schedule", hdr, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
// We should have published a scheduled message.
|
|
im := require_ChanRead(t, ch, time.Second*5)
|
|
require_Equal(t, im.subj, "foo.target")
|
|
require_Equal(t, bytesToString(getHeader(JSScheduler, im.hdr)), "foo.schedule")
|
|
require_Equal(t, bytesToString(getHeader(JSScheduleNext, im.hdr)), JSScheduleNextPurge)
|
|
}
|
|
|
|
func TestFileStoreMessageScheduleRecovered(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
dir := t.TempDir()
|
|
t.Run("BeforeRestart", func(t *testing.T) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "TEST", Subjects: []string{"foo.*"}, Storage: FileStorage, AllowMsgSchedules: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
schedule := time.Now().Add(time.Second).Format(time.RFC3339Nano)
|
|
hdr := genHeader(nil, JSSchedulePattern, fmt.Sprintf("@at %s", schedule))
|
|
hdr = genHeader(hdr, JSScheduleTarget, "foo.target")
|
|
_, _, err = fs.StoreMsg("foo.schedule", hdr, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 1)
|
|
require_Equal(t, ss.Msgs, 1)
|
|
})
|
|
|
|
t.Run("AfterRestart", func(t *testing.T) {
|
|
// Delete the message scheduling state so that we are forced to do a linear scan
|
|
// of message blocks containing message schedules.
|
|
fn := filepath.Join(dir, msgDir, msgSchedulingStreamStateFile)
|
|
require_NoError(t, os.Remove(fn))
|
|
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{StoreDir: dir, srv: s},
|
|
StreamConfig{Name: "TEST", Subjects: []string{"foo.*"}, Storage: FileStorage, AllowMsgSchedules: true})
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
require_Equal(t, fs.numMsgBlocks(), 1)
|
|
fs.mu.RLock()
|
|
mb := fs.blks[0]
|
|
fs.mu.RUnlock()
|
|
mb.mu.RLock()
|
|
schedules := mb.schedules
|
|
mb.mu.RUnlock()
|
|
|
|
require_Equal(t, schedules, 1)
|
|
|
|
var ss StreamState
|
|
fs.FastState(&ss)
|
|
require_Equal(t, ss.FirstSeq, 1)
|
|
require_Equal(t, ss.LastSeq, 1)
|
|
require_Equal(t, ss.Msgs, 1)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMessageScheduleEncodeDecode(t *testing.T) {
|
|
ms := newMsgScheduling(func() {})
|
|
now := time.Now()
|
|
|
|
// Add many sequences.
|
|
numSequences := 100_000
|
|
for seq := 0; seq < numSequences; seq++ {
|
|
ts := now.Add(time.Duration(seq) * time.Second).UnixNano()
|
|
subj := fmt.Sprintf("foo.%d", seq)
|
|
ms.add(uint64(seq), subj, ts)
|
|
}
|
|
|
|
b := ms.encode(12345)
|
|
require_True(t, len(b) > 17) // Bigger than just the header
|
|
|
|
nms := newMsgScheduling(func() {})
|
|
stamp, err := nms.decode(b)
|
|
require_NoError(t, err)
|
|
require_Equal(t, stamp, 12345)
|
|
require_Equal(t, ms.ttls.GetNextExpiration(math.MaxInt64), nms.ttls.GetNextExpiration(math.MaxInt64))
|
|
|
|
require_Len(t, len(ms.seqToSubj), len(nms.seqToSubj))
|
|
for seq, subj := range ms.seqToSubj {
|
|
require_Equal(t, subj, nms.seqToSubj[seq])
|
|
}
|
|
|
|
require_Len(t, len(ms.schedules), len(nms.schedules))
|
|
for subj, sched := range ms.schedules {
|
|
nsched := nms.schedules[subj]
|
|
require_NotNil(t, nsched)
|
|
require_Equal(t, sched.ts, nsched.ts)
|
|
require_Equal(t, sched.seq, nsched.seq)
|
|
}
|
|
}
|
|
|
|
func TestFileStoreCorruptedNonOrderedSequences(t *testing.T) {
|
|
for _, test := range []struct {
|
|
title string
|
|
seqs []uint64
|
|
msgs uint64
|
|
deleted int
|
|
}{
|
|
{title: "Unordered", seqs: []uint64{1, 3, 2, 4}, msgs: 3, deleted: 1},
|
|
{title: "Duplicated", seqs: []uint64{1, 2, 2, 3}, msgs: 3, deleted: 0},
|
|
} {
|
|
t.Run(test.title, func(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for _, seq := range test.seqs {
|
|
_, err = fs.writeMsgRecord(seq, 0, _EMPTY_, nil, nil)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
|
|
// The filestore will not yet know that something was corrupt.
|
|
lmb.mu.RLock()
|
|
defer lmb.mu.RUnlock()
|
|
require_Equal(t, lmb.msgs, 4)
|
|
require_Equal(t, lmb.dmap.Size(), 0)
|
|
|
|
// Need to reset, otherwise the rebuild will be incorrect.
|
|
atomic.StoreUint64(&lmb.first.seq, 0)
|
|
|
|
// Upon rebuild it should realize and correct.
|
|
_, _, err = lmb.rebuildStateLocked()
|
|
require_NoError(t, err)
|
|
require_Equal(t, lmb.msgs, test.msgs)
|
|
require_Equal(t, lmb.dmap.Size(), test.deleted)
|
|
|
|
// Indexing should also realize and correct.
|
|
require_True(t, lmb.cacheNotLoaded())
|
|
buf, err := lmb.loadBlock(nil)
|
|
require_NoError(t, err)
|
|
require_NoError(t, lmb.encryptOrDecryptIfNeeded(buf))
|
|
buf, err = lmb.decompressIfNeeded(buf)
|
|
require_NoError(t, err)
|
|
require_NoError(t, lmb.indexCacheBuf(buf))
|
|
require_True(t, lmb.cacheAlreadyLoaded())
|
|
})
|
|
})
|
|
}
|
|
}
|
|
|
|
func BenchmarkFileStoreGetSeqFromTime(b *testing.B) {
|
|
fs, err := newFileStore(
|
|
FileStoreConfig{
|
|
StoreDir: b.TempDir(),
|
|
BlockSize: 16,
|
|
},
|
|
StreamConfig{
|
|
Name: "foo",
|
|
Subjects: []string{"foo.>"},
|
|
Storage: FileStorage,
|
|
},
|
|
)
|
|
require_NoError(b, err)
|
|
defer fs.Stop()
|
|
|
|
for range 4096 {
|
|
_, _, err := fs.StoreMsg("foo.bar", nil, []byte{1, 2, 3, 4, 5}, 0)
|
|
require_NoError(b, err)
|
|
}
|
|
|
|
fs.mu.RLock()
|
|
fs.blks[0].mu.RLock()
|
|
fs.lmb.mu.RLock()
|
|
start := time.Unix(0, fs.blks[0].first.ts)
|
|
middle := time.Unix(0, fs.blks[0].first.ts+(fs.lmb.last.ts-fs.blks[0].first.ts)/2)
|
|
end := time.Unix(0, fs.lmb.last.ts)
|
|
fs.blks[0].mu.RUnlock()
|
|
fs.lmb.mu.RUnlock()
|
|
fs.mu.RUnlock()
|
|
|
|
b.Run("Start", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for range b.N {
|
|
fs.GetSeqFromTime(start)
|
|
}
|
|
})
|
|
|
|
b.Run("Middle", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for range b.N {
|
|
fs.GetSeqFromTime(middle)
|
|
}
|
|
})
|
|
|
|
b.Run("End", func(b *testing.B) {
|
|
b.ReportAllocs()
|
|
for range b.N {
|
|
fs.GetSeqFromTime(end)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreCacheLookupOnEmptyBlock(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
|
|
// First make sure that we haven't got a strong reference to the cache.
|
|
require_NotNil(t, lmb)
|
|
lmb.finishedWithCache()
|
|
require_True(t, lmb.cache == nil)
|
|
|
|
// Specifically we want ErrStoreMsgNotFound, not errNoCache.
|
|
_, err = lmb.cacheLookup(atomic.LoadUint64(&lmb.first.seq), nil)
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
|
|
// Now make sure that we didn't strengthen the reference. This proves
|
|
// that we short-circuited properly.
|
|
require_True(t, lmb.cache == nil)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreEraseMsgDoesNotLoseTombstones(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
secret := []byte("secret!")
|
|
// The first message will remain throughout.
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
// The second message wil be removed, so a tombstone will be placed.
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
// The third message is secret and will be erased.
|
|
_, _, err = fs.StoreMsg("foo", nil, secret, 0)
|
|
require_NoError(t, err)
|
|
|
|
// Removing the second message places a tombstone.
|
|
_, err = fs.RemoveMsg(2)
|
|
require_NoError(t, err)
|
|
|
|
// A fourth message gets placed after the tombstone.
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
// Now we erase the third message.
|
|
// This erases this message and should not lose the tombstone that comes after it.
|
|
_, err = fs.EraseMsg(3)
|
|
require_NoError(t, err)
|
|
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 2)
|
|
require_Equal(t, before.FirstSeq, 1)
|
|
require_Equal(t, before.LastSeq, 4)
|
|
require_True(t, slices.Equal(before.Deleted, []uint64{2, 3}))
|
|
|
|
_, err = fs.LoadMsg(2, nil)
|
|
require_Error(t, err, errDeletedMsg)
|
|
_, err = fs.LoadMsg(3, nil)
|
|
require_Error(t, err, errDeletedMsg)
|
|
|
|
// Make sure we can recover properly with no index.db present.
|
|
fs.Stop()
|
|
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state\n of %+v, \ngot %+v without index.db state", before, state)
|
|
}
|
|
|
|
_, err = fs.LoadMsg(2, nil)
|
|
require_Error(t, err, errDeletedMsg)
|
|
_, err = fs.LoadMsg(3, nil)
|
|
require_Error(t, err, errDeletedMsg)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreEraseMsgDoesNotLoseTombstonesInEmptyBlock(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// The first message will remain throughout.
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
// The second message wil be removed, so a tombstone will be placed.
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
mb, err := fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
|
|
secret := []byte("secret!")
|
|
// The third message is secret and will be erased.
|
|
_, _, err = fs.StoreMsg("foo", nil, secret, 0)
|
|
require_NoError(t, err)
|
|
|
|
// Removing the second message places a tombstone.
|
|
_, err = fs.RemoveMsg(2)
|
|
require_NoError(t, err)
|
|
|
|
// Now we erase the third message.
|
|
// This erases this message and should not lose the tombstone that comes after it.
|
|
// It should do the erase, even if the block would be empty afterward as it could contain tombstones.
|
|
_, err = fs.EraseMsg(3)
|
|
require_NoError(t, err)
|
|
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 1)
|
|
require_Equal(t, before.FirstSeq, 1)
|
|
require_Equal(t, before.LastSeq, 3)
|
|
require_True(t, slices.Equal(before.Deleted, []uint64{2, 3}))
|
|
|
|
_, err = fs.LoadMsg(2, nil)
|
|
require_Error(t, err, errDeletedMsg)
|
|
_, err = fs.LoadMsg(3, nil)
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
|
|
// The message should be erased.
|
|
buf, err := mb.loadBlock(nil)
|
|
require_NoError(t, err)
|
|
require_False(t, bytes.Contains(buf, secret))
|
|
|
|
// Make sure we can recover properly with no index.db present.
|
|
fs.Stop()
|
|
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state\n of %+v, \ngot %+v without index.db state", before, state)
|
|
}
|
|
|
|
_, err = fs.LoadMsg(2, nil)
|
|
require_Error(t, err, errDeletedMsg)
|
|
_, err = fs.LoadMsg(3, nil)
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreTombstonesNoFirstSeqRollback(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 10 * 33 // 10 messages per block.
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := 0; i < 20; i++ {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 20)
|
|
require_Equal(t, before.FirstSeq, 1)
|
|
require_Equal(t, before.LastSeq, 20)
|
|
|
|
// Expect 2 blocks with messages.
|
|
fs.mu.RLock()
|
|
lblks := len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Equal(t, lblks, 2)
|
|
|
|
// Write some tombstones for all messages, these will be in multiple blocks.
|
|
for seq := uint64(1); seq <= 20; seq++ {
|
|
_, err = fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
before = fs.State()
|
|
require_Equal(t, before.Msgs, 0)
|
|
require_Equal(t, before.FirstSeq, 21)
|
|
require_Equal(t, before.LastSeq, 20)
|
|
|
|
// Expect 1 block purely with tombstones.
|
|
fs.mu.RLock()
|
|
lblks = len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Equal(t, lblks, 1)
|
|
|
|
// Make sure we can recover properly with no index.db present.
|
|
fs.Stop()
|
|
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state\n of %+v, \ngot %+v without index.db state", before, state)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreTombstonesSelectNextFirstCleanup(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 10 * 33 // 10 messages per block.
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Write a bunch of messages in multiple blocks.
|
|
for i := 0; i < 50; i++ {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
for seq := uint64(2); seq <= 49; seq++ {
|
|
_, err = fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
for i := 0; i < 50; i++ {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
for seq := uint64(50); seq <= 100; seq++ {
|
|
_, err = fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 1)
|
|
require_Equal(t, before.FirstSeq, 1)
|
|
require_Equal(t, before.LastSeq, 100)
|
|
|
|
_, err = fs.RemoveMsg(1)
|
|
require_NoError(t, err)
|
|
|
|
before = fs.State()
|
|
require_Equal(t, before.Msgs, 0)
|
|
require_Equal(t, before.FirstSeq, 101)
|
|
require_Equal(t, before.LastSeq, 100)
|
|
|
|
// Make sure we can recover properly with no index.db present.
|
|
fs.Stop()
|
|
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state\n of %+v, \ngot %+v without index.db state", before, state)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreTombstonesSelectNextFirstCleanupOnRecovery(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 10 * 33 // 10 messages per block.
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Write a bunch of messages in multiple blocks.
|
|
for i := 0; i < 50; i++ {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
for seq := uint64(2); seq <= 49; seq++ {
|
|
_, err = fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
for i := 0; i < 50; i++ {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
for seq := uint64(50); seq <= 100; seq++ {
|
|
_, err = fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 1)
|
|
require_Equal(t, before.FirstSeq, 1)
|
|
require_Equal(t, before.LastSeq, 100)
|
|
|
|
// Explicitly write tombstone instead of calling fs.RemoveMsg,
|
|
// so we need to recover from a hard kill.
|
|
require_NoError(t, fs.writeTombstone(1, 0))
|
|
before = StreamState{FirstSeq: 101, FirstTime: time.Time{}, LastSeq: 100, LastTime: before.LastTime}
|
|
|
|
// Make sure we can recover properly with no index.db present.
|
|
fs.Stop()
|
|
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state\n of %+v, \ngot %+v without index.db state", before, state)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreDetectDeleteGapWithLastSkipMsg(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
// Skip a message at a sequence such that a gap is created.
|
|
// The gap should be detected later on as deleted messages.
|
|
require_NoError(t, fs.SkipMsgs(2, 3))
|
|
|
|
// We should have 3 deletes, one is the skip msg, the other two is the gap.
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 1)
|
|
require_Equal(t, before.FirstSeq, 1)
|
|
require_Equal(t, before.LastSeq, 4)
|
|
require_Equal(t, before.NumDeleted, 3)
|
|
|
|
// Make sure we can recover properly with no index.db present.
|
|
fs.Stop()
|
|
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of %+v, got %+v", before, state)
|
|
}
|
|
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.RLock()
|
|
defer mb.mu.RUnlock()
|
|
require_Equal(t, atomic.LoadUint64(&mb.first.seq), 1)
|
|
require_Equal(t, atomic.LoadUint64(&mb.last.seq), 4)
|
|
require_Len(t, mb.dmap.Size(), 3)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreDetectDeleteGapWithOnlySkipMsg(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Skip messages with a gap.
|
|
_, err = fs.SkipMsg(1)
|
|
require_NoError(t, err)
|
|
require_NoError(t, fs.SkipMsgs(2, 3))
|
|
|
|
// We should have no deletes, as the SkipMsgs only move the sequences up.
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 0)
|
|
require_Equal(t, before.FirstSeq, 5)
|
|
require_Equal(t, before.LastSeq, 4)
|
|
require_Equal(t, before.NumDeleted, 0)
|
|
|
|
// Make sure we can recover properly with no index.db present.
|
|
fs.Stop()
|
|
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state of %+v, got %+v", before, state)
|
|
}
|
|
|
|
// The block should not register the deletes between the two SkipMsgs.
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.RLock()
|
|
defer mb.mu.RUnlock()
|
|
require_Equal(t, atomic.LoadUint64(&mb.first.seq), 5)
|
|
require_Equal(t, atomic.LoadUint64(&mb.last.seq), 4)
|
|
require_Len(t, mb.dmap.Size(), 0)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreEraseMsgErr(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.Lock()
|
|
if mb.cache == nil {
|
|
mb.mu.Unlock()
|
|
t.Fatal("Expected cache to be initialized")
|
|
}
|
|
// Set to a bogus value such that the file rename fails while performing the message erase.
|
|
mb.mfn = _EMPTY_
|
|
mb.mu.Unlock()
|
|
fs.EraseMsg(2)
|
|
|
|
// Cleanup ".tmp" file if it was created due to the purposefully invalid file name above.
|
|
_, err = os.Stat(blkTmpSuffix)
|
|
if err == nil {
|
|
require_NoError(t, os.Remove(blkTmpSuffix))
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStorePurgeMsgBlock(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 10 * 33
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for range 20 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
fs.mu.RLock()
|
|
blks := len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Equal(t, blks, 2)
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 20)
|
|
require_Equal(t, state.Msgs, 20)
|
|
require_Equal(t, state.Bytes, 20*33)
|
|
|
|
// Purging the block should both remove the block and do the accounting.
|
|
fmb := fs.getFirstBlock()
|
|
fs.mu.Lock()
|
|
fs.purgeMsgBlock(fmb)
|
|
blks = len(fs.blks)
|
|
fs.mu.Unlock()
|
|
|
|
require_Equal(t, blks, 1)
|
|
state = fs.State()
|
|
require_Equal(t, state.FirstSeq, 11)
|
|
require_Equal(t, state.LastSeq, 20)
|
|
require_Equal(t, state.Msgs, 10)
|
|
require_Equal(t, state.Bytes, 10*33)
|
|
})
|
|
}
|
|
|
|
func TestFileStorePurgeMsgBlockUpdatesSubjects(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.BlockSize = 10 * 33
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for range 20 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
fst := fs.SubjectsTotals("foo")
|
|
require_Equal(t, fst["foo"], uint64(20))
|
|
|
|
fmb := fs.getFirstBlock()
|
|
fs.mu.Lock()
|
|
fs.purgeMsgBlock(fmb)
|
|
fs.mu.Unlock()
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, uint64(10))
|
|
require_Equal(t, state.FirstSeq, uint64(11))
|
|
|
|
fst = fs.SubjectsTotals("foo")
|
|
require_Equal(t, fst["foo"], uint64(10))
|
|
})
|
|
}
|
|
|
|
func TestFileStorePurgeMsgBlockRemovesSchedules(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
schedule := time.Now().Add(time.Hour).Format(time.RFC3339Nano)
|
|
hdr := genHeader(nil, JSSchedulePattern, fmt.Sprintf("@at %s", schedule))
|
|
hdr = genHeader(hdr, JSScheduleTarget, "foo.target.0")
|
|
msgSize := fileStoreMsgSize("foo.sched.0", hdr, []byte("x"))
|
|
|
|
// Force two blocks of 5 messages each.
|
|
fcfg.BlockSize = uint64(msgSize * 5)
|
|
cfg := StreamConfig{
|
|
Name: "zzz",
|
|
Subjects: []string{"foo.*"},
|
|
Storage: FileStorage,
|
|
AllowMsgSchedules: true,
|
|
}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := range 10 {
|
|
subj := fmt.Sprintf("foo.sched.%d", i)
|
|
target := fmt.Sprintf("foo.target.%d", i)
|
|
hdr := genHeader(nil, JSSchedulePattern, fmt.Sprintf("@at %s", schedule))
|
|
hdr = genHeader(hdr, JSScheduleTarget, target)
|
|
_, _, err = fs.StoreMsg(subj, hdr, []byte("x"), 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
fs.mu.RLock()
|
|
blks := len(fs.blks)
|
|
sts, msgs := len(fs.scheduling.seqToSubj), int(fs.state.Msgs)
|
|
fs.mu.RUnlock()
|
|
require_True(t, blks >= 2)
|
|
require_Equal(t, sts, msgs)
|
|
|
|
fmb := fs.getFirstBlock()
|
|
fs.mu.Lock()
|
|
fs.purgeMsgBlock(fmb)
|
|
fs.mu.Unlock()
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, uint64(5))
|
|
|
|
fs.mu.Lock()
|
|
defer fs.mu.Unlock()
|
|
require_Equal(t, len(fs.scheduling.seqToSubj), int(state.Msgs))
|
|
for seq := uint64(1); seq < state.FirstSeq; seq++ {
|
|
if _, ok := fs.scheduling.seqToSubj[seq]; ok {
|
|
t.Fatalf("expected schedule for seq %d to be removed", seq)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStorePurgeMsgBlockAccounting(t *testing.T) {
|
|
test := func(t *testing.T, update func(cfg *nats.StreamConfig)) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
|
|
cfg := &nats.StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"foo"},
|
|
Storage: nats.FileStorage,
|
|
}
|
|
_, err := js.AddStream(cfg)
|
|
require_NoError(t, err)
|
|
|
|
subj, data := "foo", make([]byte, 1024*1024)
|
|
for range 10 {
|
|
_, err = js.Publish(subj, data)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
gacc := s.globalAccount()
|
|
mset, err := gacc.lookupStream("TEST")
|
|
require_NoError(t, err)
|
|
state := mset.state()
|
|
stats := gacc.JetStreamUsage()
|
|
require_Equal(t, state.Bytes, stats.JetStreamTier.Store)
|
|
|
|
update(cfg)
|
|
_, err = js.UpdateStream(cfg)
|
|
require_NoError(t, err)
|
|
|
|
state = mset.state()
|
|
stats = gacc.JetStreamUsage()
|
|
require_Equal(t, state.Bytes, fileStoreMsgSizeRaw(len(subj), 0, len(data)))
|
|
require_Equal(t, state.Bytes, stats.JetStreamTier.Store)
|
|
}
|
|
|
|
t.Run("MaxMsgs", func(t *testing.T) {
|
|
test(t, func(cfg *nats.StreamConfig) {
|
|
cfg.MaxMsgs = 1
|
|
})
|
|
})
|
|
t.Run("MaxBytes", func(t *testing.T) {
|
|
test(t, func(cfg *nats.StreamConfig) {
|
|
cfg.MaxBytes = int64(fileStoreMsgSizeRaw(3, 0, 1024*1024))
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestFileStoreMissingDeletesAfterCompact(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Generate a block with 6 messages and then delete the first and last, as well as a larger gap in the middle.
|
|
for range 6 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
_, err = fs.RemoveMsg(1)
|
|
require_NoError(t, err)
|
|
_, err = fs.RemoveMsg(3)
|
|
require_NoError(t, err)
|
|
_, err = fs.RemoveMsg(4)
|
|
require_NoError(t, err)
|
|
_, err = fs.RemoveMsg(6)
|
|
require_NoError(t, err)
|
|
|
|
// We'll compact the deletes later, but shouldn't be lmb.
|
|
fmb := fs.getFirstBlock()
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
|
|
// Confirm the block's state.
|
|
fmb.mu.Lock()
|
|
defer fmb.mu.Unlock()
|
|
require_Equal(t, atomic.LoadUint64(&fmb.first.seq), 2)
|
|
require_Equal(t, atomic.LoadUint64(&fmb.last.seq), 6)
|
|
require_Equal(t, fmb.msgs, 2)
|
|
require_Len(t, fmb.dmap.Size(), 3)
|
|
require_True(t, fmb.dmap.Exists(3))
|
|
require_True(t, fmb.dmap.Exists(4))
|
|
require_True(t, fmb.dmap.Exists(6))
|
|
|
|
// Now compact and reload and the block should still have the correct deletes.
|
|
require_NoError(t, fmb.compact())
|
|
fmb.clearCache()
|
|
fmb.dmap.Empty()
|
|
require_NoError(t, fmb.loadMsgsWithLock())
|
|
require_Equal(t, atomic.LoadUint64(&fmb.first.seq), 2)
|
|
require_Equal(t, atomic.LoadUint64(&fmb.last.seq), 5)
|
|
require_Equal(t, fmb.msgs, 2)
|
|
require_Len(t, fmb.dmap.Size(), 2)
|
|
require_True(t, fmb.dmap.Exists(3))
|
|
require_True(t, fmb.dmap.Exists(4))
|
|
|
|
// Rebuilding should have the state remain the same.
|
|
_, _, err = fmb.rebuildStateLocked()
|
|
require_NoError(t, err)
|
|
require_Equal(t, atomic.LoadUint64(&fmb.first.seq), 2)
|
|
require_Equal(t, atomic.LoadUint64(&fmb.last.seq), 5)
|
|
require_Equal(t, fmb.msgs, 2)
|
|
|
|
// Delete at sequence 5 such that the block can be compacted to a single message.
|
|
fmb.mu.Unlock()
|
|
_, err = fs.RemoveMsg(5)
|
|
fmb.mu.Lock()
|
|
require_NoError(t, err)
|
|
require_NoError(t, fmb.compact())
|
|
fmb.clearCache()
|
|
fmb.dmap.Empty()
|
|
require_NoError(t, fmb.loadMsgsWithLock())
|
|
require_Equal(t, atomic.LoadUint64(&fmb.first.seq), 2)
|
|
require_Equal(t, atomic.LoadUint64(&fmb.last.seq), 2)
|
|
require_Equal(t, fmb.msgs, 1)
|
|
require_Len(t, fmb.dmap.Size(), 0)
|
|
|
|
// Rebuilding should have the state remain the same.
|
|
_, _, err = fmb.rebuildStateLocked()
|
|
require_NoError(t, err)
|
|
require_Equal(t, atomic.LoadUint64(&fmb.first.seq), 2)
|
|
require_Equal(t, atomic.LoadUint64(&fmb.last.seq), 2)
|
|
require_Equal(t, fmb.msgs, 1)
|
|
require_Len(t, fmb.dmap.Size(), 0)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreIdxAccountingForSkipMsgs(t *testing.T) {
|
|
test := func(t *testing.T, skipMany bool) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
if skipMany {
|
|
require_NoError(t, fs.SkipMsgs(2, 10))
|
|
} else {
|
|
for i := range 10 {
|
|
_, err = fs.SkipMsg(uint64(i + 2))
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
fmb := fs.getFirstBlock()
|
|
fmb.mu.Lock()
|
|
defer fmb.mu.Unlock()
|
|
|
|
for i := range 12 {
|
|
seq := uint64(i + 1)
|
|
_, err = fmb.cacheLookupNoCopy(seq, nil)
|
|
if seq >= 2 && seq <= 11 {
|
|
require_Error(t, err, errDeletedMsg)
|
|
} else {
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
|
|
cache := fmb.cache
|
|
require_NotNil(t, cache)
|
|
require_Len(t, len(cache.idx), 12)
|
|
})
|
|
}
|
|
|
|
t.Run("SkipMsg", func(t *testing.T) { test(t, false) })
|
|
t.Run("SkipMsgs", func(t *testing.T) { test(t, true) })
|
|
}
|
|
|
|
func TestFileStoreEmptyBlockContainsPriorTombstones(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// 1.blk
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
// 2.blk
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
_, err = fs.RemoveMsg(2)
|
|
require_NoError(t, err)
|
|
_, err = fs.RemoveMsg(3) // Will create a new lmb with this as tombstone.
|
|
require_NoError(t, err)
|
|
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 1)
|
|
require_Equal(t, before.FirstSeq, 1)
|
|
require_Equal(t, before.LastSeq, 3)
|
|
require_True(t, slices.Equal(before.Deleted, []uint64{2, 3}))
|
|
|
|
fs.mu.RLock()
|
|
lblks := len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Equal(t, lblks, 3)
|
|
|
|
_, err = fs.LoadMsg(2, nil)
|
|
require_Error(t, err, errDeletedMsg)
|
|
_, err = fs.LoadMsg(3, nil)
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
|
|
// Make sure we can recover properly with no index.db present.
|
|
fs.Stop()
|
|
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state\n of %+v, \ngot %+v without index.db state", before, state)
|
|
}
|
|
|
|
fs.mu.RLock()
|
|
lblks = len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Equal(t, lblks, 3)
|
|
|
|
_, err = fs.LoadMsg(2, nil)
|
|
require_Error(t, err, errDeletedMsg)
|
|
_, err = fs.LoadMsg(3, nil)
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
|
|
// 3.blk
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
fs.mu.RLock()
|
|
lblks = len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Equal(t, lblks, 3)
|
|
|
|
// Removing the first message moves the first seq up.
|
|
// Should also remove blocks without any messages and (invalidated) tombstones.
|
|
_, err = fs.RemoveMsg(1)
|
|
require_NoError(t, err)
|
|
|
|
fs.mu.RLock()
|
|
lblks = len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Equal(t, lblks, 1)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreCompactTombstonesBelowFirstSeq(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Subjects: []string{"foo"}, Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// 1.blk
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
// 2.blk
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
_, err = fs.RemoveMsg(3)
|
|
require_NoError(t, err)
|
|
_, err = fs.RemoveMsg(2)
|
|
require_NoError(t, err)
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, 2)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 4)
|
|
require_True(t, slices.Equal(state.Deleted, []uint64{2, 3}))
|
|
|
|
fs.mu.RLock()
|
|
lblks := len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
require_Equal(t, lblks, 2)
|
|
|
|
// Block should report the two prior tombstones.
|
|
fs.mu.Lock()
|
|
lmb := fs.lmb
|
|
lmb.mu.Lock()
|
|
priorTombs := lmb.numPriorTombsLocked()
|
|
lmb.mu.Unlock()
|
|
fs.mu.Unlock()
|
|
require_Equal(t, priorTombs, 2)
|
|
|
|
// The first sequence moves up as a result of the removal.
|
|
_, err = fs.RemoveMsg(1)
|
|
require_NoError(t, err)
|
|
|
|
// Block should now report no prior tombstones, since they are now invalid.
|
|
fs.mu.Lock()
|
|
lmb.mu.Lock()
|
|
priorTombs = lmb.numPriorTombsLocked()
|
|
lmb.mu.Unlock()
|
|
fs.mu.Unlock()
|
|
require_Equal(t, priorTombs, 0)
|
|
|
|
// Make sure we have a new last block such that we can compact.
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
|
|
lmb.mu.RLock()
|
|
rbytes := lmb.rbytes
|
|
shouldCompact := lmb.shouldCompactSync()
|
|
lmb.mu.RUnlock()
|
|
require_True(t, shouldCompact)
|
|
fs.syncBlocks()
|
|
|
|
lmb.mu.RLock()
|
|
defer lmb.mu.RUnlock()
|
|
require_NotEqual(t, lmb.rbytes, rbytes)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreSyncBlocksFlushesAndSyncsMessages(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fcfg.AsyncFlush = true
|
|
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
require_NotNil(t, lmb)
|
|
|
|
// Wait for flusher to be ready.
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
lmb.mu.RLock()
|
|
defer lmb.mu.RUnlock()
|
|
if !lmb.flusher {
|
|
return errors.New("flusher not active")
|
|
}
|
|
return nil
|
|
})
|
|
// Now shutdown flusher and wait for it to be closed.
|
|
lmb.mu.Lock()
|
|
if lmb.qch != nil {
|
|
close(lmb.qch)
|
|
lmb.qch = nil
|
|
}
|
|
lmb.mu.Unlock()
|
|
checkFor(t, 2*time.Second, 200*time.Millisecond, func() error {
|
|
lmb.mu.RLock()
|
|
defer lmb.mu.RUnlock()
|
|
if lmb.flusher {
|
|
return errors.New("flusher still active")
|
|
}
|
|
return nil
|
|
})
|
|
|
|
seq, _, err := fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
require_Equal(t, seq, 1)
|
|
|
|
// Update the last write timestamp to be in the past.
|
|
lmb.mu.Lock()
|
|
lwts := lmb.lwts
|
|
lmb.lwts = 0
|
|
lmb.mu.Unlock()
|
|
require_NotEqual(t, lwts, 0)
|
|
|
|
// Syncing should write out the data.
|
|
fs.syncBlocks()
|
|
|
|
// Manually reset, sync should have written the data.
|
|
lmb.clearCacheAndOffset()
|
|
|
|
sm, err := fs.LoadMsg(1, nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, sm.seq, 1)
|
|
require_Equal(t, sm.subj, "foo")
|
|
})
|
|
}
|
|
|
|
func TestJetStreamFileStoreSubjectsRemovedAfterSecureErase(t *testing.T) {
|
|
s := RunBasicJetStreamServer(t)
|
|
defer s.Shutdown()
|
|
|
|
nc, js := jsClientConnect(t, s)
|
|
defer nc.Close()
|
|
|
|
_, err := js.AddStream(&nats.StreamConfig{
|
|
Name: "TEST",
|
|
Subjects: []string{"test.*"},
|
|
Storage: nats.FileStorage,
|
|
})
|
|
require_NoError(t, err)
|
|
|
|
_, err = js.Publish("test.1", []byte("msg1"))
|
|
require_NoError(t, err)
|
|
_, err = js.Publish("test.2", []byte("msg2"))
|
|
require_NoError(t, err)
|
|
_, err = js.Publish("test.3", []byte("msg3"))
|
|
require_NoError(t, err)
|
|
|
|
si, err := js.StreamInfo("TEST", &nats.StreamInfoRequest{SubjectsFilter: ">"})
|
|
require_NoError(t, err)
|
|
require_Equal(t, si.State.NumSubjects, 3)
|
|
require_Len(t, len(si.State.Subjects), 3)
|
|
|
|
// The bug happened here: the underlying eraseMsg() call in removeMsg() would
|
|
// corrupt the sm.subj from the shallow cache lookup. We would then pass the
|
|
// corrupted subject into removeSeqPerSubject() & removePerSubject(), resulting
|
|
// in them being no-ops. This is now fixed.
|
|
require_NoError(t, js.SecureDeleteMsg("TEST", 1))
|
|
|
|
si, err = js.StreamInfo("TEST", &nats.StreamInfoRequest{SubjectsFilter: ">"})
|
|
require_NoError(t, err)
|
|
require_Equal(t, si.State.NumSubjects, 2)
|
|
require_Len(t, len(si.State.Subjects), 2)
|
|
|
|
_, exists := si.State.Subjects["test.1"]
|
|
require_False(t, exists)
|
|
require_Equal(t, si.State.Subjects["test.2"], uint64(1))
|
|
require_Equal(t, si.State.Subjects["test.3"], uint64(1))
|
|
}
|
|
|
|
func TestFileStorePreserveLastSeqAfterCompact(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Storage: FileStorage}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
_, err = fs.Compact(2)
|
|
require_NoError(t, err)
|
|
|
|
before := fs.State()
|
|
require_Equal(t, before.Msgs, 0)
|
|
require_Equal(t, before.FirstSeq, 2)
|
|
require_Equal(t, before.LastSeq, 1)
|
|
|
|
fs.Stop()
|
|
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
t.Fatalf("Expected state\n of %+v, \ngot %+v without index.db state", before, state)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestFileStoreSkipMsgAndCompactRequiresAppend(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Store one very long message.
|
|
msg := make([]byte, 256*1024)
|
|
_, _, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
|
|
// Skip and compact to that sequence, the block will be preserved and tombstones would be written.
|
|
_, err = fs.SkipMsg(2)
|
|
require_NoError(t, err)
|
|
_, err = fs.Compact(2)
|
|
require_NoError(t, err)
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, 0)
|
|
require_Equal(t, state.FirstSeq, 3)
|
|
require_Equal(t, state.LastSeq, 2)
|
|
|
|
// Skipping again would result in the bug. We tried to write to the start of the file,
|
|
// but the old message data would still be there. Instead, now add the SkipMsg to the end.
|
|
_, err = fs.SkipMsg(3)
|
|
require_NoError(t, err)
|
|
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
mb.clearCacheAndOffset()
|
|
require_NoError(t, mb.loadMsgsWithLock())
|
|
})
|
|
}
|
|
|
|
func TestFileStoreCompactRewritesFileWithSwap(t *testing.T) {
|
|
fcfg := FileStoreConfig{Cipher: NoCipher, Compression: NoCompression, StoreDir: t.TempDir()}
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := make([]byte, 256*1024)
|
|
for range 20 {
|
|
_, _, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Compact should realize the block's data was largely removed, the file should be rewritten
|
|
_, err = fs.Compact(20)
|
|
require_NoError(t, err)
|
|
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, 1)
|
|
require_Equal(t, state.FirstSeq, 20)
|
|
require_Equal(t, state.LastSeq, 20)
|
|
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
mb.clearCacheAndOffset()
|
|
require_NoError(t, mb.loadMsgsWithLock())
|
|
|
|
mbcache := mb.cache
|
|
require_NotNil(t, mbcache)
|
|
require_Len(t, len(mbcache.idx), 1)
|
|
require_Equal(t, mbcache.idx[0], 0)
|
|
}
|
|
|
|
func TestFileStoreIndexCacheBufIdxMismatch(t *testing.T) {
|
|
const (
|
|
KindTruncateFull = iota
|
|
KindTruncatePartial
|
|
KindCompactHead
|
|
KindCompactTail
|
|
KindCompact
|
|
)
|
|
test := func(t *testing.T, kind int) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for range 5 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
|
|
var deleted int
|
|
compactHead := kind == KindCompactHead || kind == KindCompact
|
|
compactTail := kind == KindCompactTail || kind == KindCompact
|
|
if compactHead || compactTail {
|
|
require_Equal(t, atomic.LoadUint64(&mb.first.seq), 1)
|
|
require_Equal(t, atomic.LoadUint64(&mb.last.seq), 5)
|
|
if compactHead {
|
|
atomic.StoreUint64(&mb.first.seq, 2)
|
|
deleted++
|
|
}
|
|
if compactTail {
|
|
mb.dmap.Insert(5)
|
|
deleted++
|
|
}
|
|
require_NoError(t, mb.compactWithFloor(0, nil))
|
|
|
|
// Revert the state, upon reload we need to recognize this.
|
|
atomic.StoreUint64(&mb.first.seq, 1)
|
|
atomic.StoreUint64(&mb.last.seq, 5)
|
|
}
|
|
|
|
mb.clearCacheAndOffset()
|
|
switch kind {
|
|
case KindTruncateFull:
|
|
// Truncate to be empty.
|
|
require_NoError(t, os.Truncate(mb.mfn, 0))
|
|
|
|
// When loading messages, we should realize our in-memory state doesn't match what's on disk.
|
|
// The block needs to be rebuilt and re-indexed.
|
|
require_NoError(t, mb.loadMsgsWithLock())
|
|
require_Equal(t, mb.msgs, 0)
|
|
require_Equal(t, atomic.LoadUint64(&mb.first.seq), 6)
|
|
require_Equal(t, atomic.LoadUint64(&mb.last.seq), 5)
|
|
for i := range 5 {
|
|
seq := uint64(i + 1)
|
|
_, err = mb.cacheLookup(seq, nil)
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
}
|
|
case KindTruncatePartial:
|
|
// Truncate to half of the original file.
|
|
stat, err := os.Stat(mb.mfn)
|
|
require_NoError(t, err)
|
|
require_NoError(t, os.Truncate(mb.mfn, stat.Size()/2))
|
|
|
|
// When loading messages, we should realize our in-memory state doesn't match what's on disk.
|
|
// The block needs to be rebuilt and re-indexed.
|
|
require_NoError(t, mb.loadMsgsWithLock())
|
|
require_Equal(t, mb.msgs, 2)
|
|
require_Equal(t, atomic.LoadUint64(&mb.first.seq), 1)
|
|
require_Equal(t, atomic.LoadUint64(&mb.last.seq), 2)
|
|
for i := range 5 {
|
|
seq := uint64(i + 1)
|
|
_, err = mb.cacheLookup(seq, nil)
|
|
if seq <= 2 {
|
|
require_NoError(t, err)
|
|
} else {
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
}
|
|
}
|
|
case KindCompact, KindCompactHead, KindCompactTail:
|
|
// Since we've reverted the state after compacting, our in-memory state doesn't match what's on disk.
|
|
// The block needs to be rebuilt and re-indexed.
|
|
require_NoError(t, mb.loadMsgsWithLock())
|
|
require_Equal(t, mb.msgs, uint64(5-deleted))
|
|
if compactHead {
|
|
require_Equal(t, atomic.LoadUint64(&mb.first.seq), 2)
|
|
} else {
|
|
require_Equal(t, atomic.LoadUint64(&mb.first.seq), 1)
|
|
}
|
|
if compactTail {
|
|
require_Equal(t, atomic.LoadUint64(&mb.last.seq), 4)
|
|
} else {
|
|
require_Equal(t, atomic.LoadUint64(&mb.last.seq), 5)
|
|
}
|
|
for i := range 5 {
|
|
seq := uint64(i + 1)
|
|
_, err = mb.cacheLookup(seq, nil)
|
|
if seq == 1 && compactHead {
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
} else if seq == 5 && compactTail {
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
} else {
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
t.Run("TruncateFull", func(t *testing.T) { test(t, KindTruncateFull) })
|
|
t.Run("TruncatePartial", func(t *testing.T) { test(t, KindTruncatePartial) })
|
|
t.Run("CompactHead", func(t *testing.T) { test(t, KindCompactHead) })
|
|
t.Run("CompactTail", func(t *testing.T) { test(t, KindCompactTail) })
|
|
t.Run("Compact", func(t *testing.T) { test(t, KindCompact) })
|
|
}
|
|
|
|
func TestFileStoreIndexCacheBufTombstoneMismatch(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for range 3 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
_, err = fs.RemoveMsg(2)
|
|
require_NoError(t, err)
|
|
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
|
|
// Not only clear the cache, also clear the dmap.
|
|
// indexCacheBuf should still properly index.
|
|
mb.clearCacheAndOffset()
|
|
mb.dmap.Empty()
|
|
require_NoError(t, mb.loadMsgsWithLock())
|
|
|
|
mbcache := mb.cache
|
|
require_NotNil(t, mbcache)
|
|
require_Len(t, len(mbcache.idx), 3)
|
|
require_Equal(t, mbcache.idx[0], 0)
|
|
require_Equal(t, mbcache.idx[1], 33)
|
|
require_Equal(t, mbcache.idx[2], 66)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreIndexCacheBufTombstoneMismatchAfterCompact(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for range 3 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
_, err = fs.RemoveMsg(2)
|
|
require_NoError(t, err)
|
|
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
|
|
require_NoError(t, mb.compactWithFloor(0, nil))
|
|
|
|
// Not only clear the cache, also clear the dmap.
|
|
mb.clearCacheAndOffset()
|
|
mb.dmap.Empty()
|
|
require_NoError(t, mb.loadMsgsWithLock())
|
|
|
|
require_Equal(t, mb.dmap.Size(), 1)
|
|
require_True(t, mb.dmap.Exists(2))
|
|
mbcache := mb.cache
|
|
require_NotNil(t, mbcache)
|
|
require_Len(t, len(mbcache.idx), 3)
|
|
require_Equal(t, mbcache.idx[0], 0)
|
|
// Since the block was compacted, this will be both dbit and exist in the dmap which is checked above.
|
|
require_Equal(t, mbcache.idx[1], dbit)
|
|
// Due to the compact, this message will be one entry earlier in the file than it was before.
|
|
require_Equal(t, mbcache.idx[2], 33)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreIndexCacheBufEraseMsgMismatch(t *testing.T) {
|
|
fcfg := FileStoreConfig{Cipher: NoCipher, Compression: NoCompression, StoreDir: t.TempDir()}
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for range 3 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
_, err = fs.EraseMsg(2)
|
|
require_NoError(t, err)
|
|
|
|
// Revert the state in the filestore itself, as well as the block.
|
|
// This simulates recovery based on a stale stream state file that should be recognized during runtime.
|
|
before := fs.State()
|
|
fs.mu.Lock()
|
|
msgs := fs.state.Msgs
|
|
fs.state.Msgs = 3
|
|
fs.mu.Unlock()
|
|
require_Equal(t, msgs, 2)
|
|
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
|
|
// Not only clear the cache, also clear the dmap.
|
|
// indexCacheBuf should still properly index.
|
|
mb.clearCacheAndOffset()
|
|
mb.dmap.Empty()
|
|
require_NoError(t, mb.loadMsgsWithLock())
|
|
|
|
mbcache := mb.cache
|
|
require_NotNil(t, mbcache)
|
|
require_Len(t, len(mbcache.idx), 3)
|
|
require_Equal(t, mbcache.idx[0], 0)
|
|
require_Equal(t, mbcache.idx[1], 33)
|
|
require_Equal(t, mbcache.idx[2], 66)
|
|
|
|
// This looks backwards but is intentional. The above asserts unlock based on the defer if they fail,
|
|
// so need to do the reverse here while we inspect the filestore as a whole.
|
|
mb.mu.Unlock()
|
|
defer mb.mu.Lock()
|
|
|
|
checkFor(t, 2*time.Second, 100*time.Millisecond, func() error {
|
|
if state := fs.State(); !reflect.DeepEqual(state, before) {
|
|
return fmt.Errorf("expected state\n of %+v,\n got %+v", before, state)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func TestFileStoreCompactRestoresLastSeq(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
var tss []int64
|
|
for range 4 {
|
|
_, ts, err := fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
tss = append(tss, ts)
|
|
}
|
|
|
|
checkMbState := func(fseq, lseq, msgs uint64) {
|
|
t.Helper()
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
|
|
require_Equal(t, atomic.LoadUint64(&mb.first.seq), fseq)
|
|
require_Equal(t, atomic.LoadUint64(&mb.last.seq), lseq)
|
|
require_Equal(t, mb.first.ts, tss[fseq-1])
|
|
require_Equal(t, mb.last.ts, tss[lseq-1])
|
|
require_Equal(t, mb.msgs, msgs)
|
|
deletes := lseq - fseq + 1 - msgs
|
|
require_Equal(t, mb.dmap.Size(), int(deletes))
|
|
}
|
|
checkMbState(1, 4, 4)
|
|
|
|
_, err = fs.RemoveMsg(1)
|
|
require_NoError(t, err)
|
|
checkMbState(2, 4, 3)
|
|
|
|
_, err = fs.RemoveMsg(4)
|
|
require_NoError(t, err)
|
|
checkMbState(2, 4, 2)
|
|
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.Lock()
|
|
err = mb.compactWithFloor(0, nil)
|
|
mb.mu.Unlock()
|
|
require_NoError(t, err)
|
|
checkMbState(2, 3, 2)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreCompactFullyResetsFirstAndLastSeq(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
var tss []int64
|
|
for range 2 {
|
|
_, ts, err := fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
tss = append(tss, ts)
|
|
}
|
|
|
|
checkMbState := func(fseq, lseq, msgs uint64) {
|
|
t.Helper()
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.Lock()
|
|
defer mb.mu.Unlock()
|
|
|
|
require_Equal(t, atomic.LoadUint64(&mb.first.seq), fseq)
|
|
require_Equal(t, mb.first.ts, tss[fseq-1])
|
|
if lseq == 0 {
|
|
require_Equal(t, atomic.LoadUint64(&mb.last.seq), fseq-1)
|
|
require_Equal(t, mb.last.ts, 0)
|
|
} else {
|
|
require_Equal(t, atomic.LoadUint64(&mb.last.seq), lseq)
|
|
require_Equal(t, mb.last.ts, tss[lseq-1])
|
|
}
|
|
require_Equal(t, mb.msgs, msgs)
|
|
deletes := lseq - fseq + 1 - msgs
|
|
require_Equal(t, mb.dmap.Size(), int(deletes))
|
|
}
|
|
checkMbState(1, 2, 2)
|
|
|
|
mb := fs.getFirstBlock()
|
|
// Load the cache before compacting so that the compact itself needs to (re)load it.
|
|
require_NoError(t, mb.loadMsgs())
|
|
mb.mu.Lock()
|
|
// Manually 'delete' the whole block contents such that a call to rebuildState and indexCacheBuf
|
|
// can't fix this. A call to compact needs to end with correctly resetting the first and last seq.
|
|
mb.dmap.Insert(1)
|
|
mb.dmap.Insert(2)
|
|
mb.msgs = 0
|
|
err = mb.compactWithFloor(0, nil)
|
|
mb.mu.Unlock()
|
|
require_NoError(t, err)
|
|
checkMbState(1, 0, 0)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreDoesntRebuildSubjectStateWithNoTrack(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
// This implicitly was calling resetGlobalPerSubjectInfo and
|
|
// populating "foo" back into the psim.
|
|
require_NoError(t, fs.Truncate(1))
|
|
|
|
fs.mu.Lock()
|
|
defer fs.mu.Unlock()
|
|
require_Equal(t, fs.psim.Size(), 0)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreLoadNextMsgSkipAhead(t *testing.T) {
|
|
test := func(t *testing.T, headStart, singleSubjPerBlock bool) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Storage: FileStorage, Subjects: []string{">"}}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := range 10 {
|
|
subj := "a"
|
|
if !singleSubjPerBlock && i%2 == 0 {
|
|
subj = "c"
|
|
}
|
|
_, _, err = fs.StoreMsg(subj, nil, nil, 0)
|
|
require_NoError(t, err)
|
|
if (i+1)%2 == 0 {
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
_, _, err = fs.StoreMsg("b", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
fs.mu.Lock()
|
|
fs.lockAllMsgBlocks()
|
|
for _, mb := range fs.blks {
|
|
mb.lsts = 0
|
|
}
|
|
fs.unlockAllMsgBlocks()
|
|
fs.mu.Unlock()
|
|
|
|
var start uint64
|
|
if !headStart {
|
|
start = 3
|
|
}
|
|
|
|
// Check we properly skip over blocks that aren't relevant for our filter subject.
|
|
// If we're starting with start=0, we should skip to the correct block immediately.
|
|
// Otherwise, we should only need to access one block and then skip to the correct one.
|
|
sm, nseq, err := fs.LoadNextMsg("b", false, start, nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, sm.seq, 11)
|
|
require_Equal(t, nseq, 11)
|
|
|
|
fs.mu.Lock()
|
|
defer fs.mu.Unlock()
|
|
fs.lockAllMsgBlocks()
|
|
defer fs.unlockAllMsgBlocks()
|
|
for _, mb := range fs.blks {
|
|
t.Logf("mb %d: lsts=%d", mb.index, mb.lsts)
|
|
if mb.index == 6 || (mb.index == 2 && !headStart) {
|
|
require_NotEqual(t, mb.lsts, 0)
|
|
} else {
|
|
require_Equal(t, mb.lsts, 0)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, singleSubjPerBlock := range []bool{true, false} {
|
|
title := "Single"
|
|
if !singleSubjPerBlock {
|
|
title = "Multi"
|
|
}
|
|
t.Run(fmt.Sprintf("%s/Head", title), func(t *testing.T) { test(t, true, singleSubjPerBlock) })
|
|
t.Run(fmt.Sprintf("%s/Interior", title), func(t *testing.T) { test(t, false, singleSubjPerBlock) })
|
|
}
|
|
}
|
|
|
|
func TestFileStoreLoadNextMsgMultiSkipAhead(t *testing.T) {
|
|
test := func(t *testing.T, headStart, singleSubjPerBlock bool) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Storage: FileStorage, Subjects: []string{">"}}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := range 10 {
|
|
subj := "a"
|
|
if !singleSubjPerBlock && i%2 == 0 {
|
|
subj = "c"
|
|
}
|
|
_, _, err = fs.StoreMsg(subj, nil, nil, 0)
|
|
require_NoError(t, err)
|
|
if (i+1)%2 == 0 {
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
_, _, err = fs.StoreMsg("b", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
|
|
fs.mu.Lock()
|
|
fs.lockAllMsgBlocks()
|
|
for _, mb := range fs.blks {
|
|
mb.lsts = 0
|
|
}
|
|
fs.unlockAllMsgBlocks()
|
|
fs.mu.Unlock()
|
|
|
|
var start uint64
|
|
if !headStart {
|
|
start = 3
|
|
}
|
|
|
|
// Check we properly skip over blocks that aren't relevant for our filter subjects.
|
|
// If we're starting with start=0, we should skip to the correct block immediately.
|
|
// Otherwise, we should only need to access one block and then skip to the correct one.
|
|
filters := gsl.NewSublist[struct{}]()
|
|
require_NoError(t, filters.Insert("b", struct{}{}))
|
|
require_NoError(t, filters.Insert("d", struct{}{}))
|
|
sm, nseq, err := fs.LoadNextMsgMulti(filters, start, nil)
|
|
require_NoError(t, err)
|
|
require_Equal(t, sm.seq, 11)
|
|
require_Equal(t, nseq, 11)
|
|
|
|
fs.mu.Lock()
|
|
defer fs.mu.Unlock()
|
|
fs.lockAllMsgBlocks()
|
|
defer fs.unlockAllMsgBlocks()
|
|
for _, mb := range fs.blks {
|
|
t.Logf("mb %d: lsts=%d", mb.index, mb.lsts)
|
|
if mb.index == 6 || (mb.index == 2 && !headStart) {
|
|
require_NotEqual(t, mb.lsts, 0)
|
|
} else {
|
|
require_Equal(t, mb.lsts, 0)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
for _, singleSubjPerBlock := range []bool{true, false} {
|
|
title := "Single"
|
|
if !singleSubjPerBlock {
|
|
title = "Multi"
|
|
}
|
|
t.Run(fmt.Sprintf("%s/Head", title), func(t *testing.T) { test(t, true, singleSubjPerBlock) })
|
|
t.Run(fmt.Sprintf("%s/Interior", title), func(t *testing.T) { test(t, false, singleSubjPerBlock) })
|
|
}
|
|
}
|
|
|
|
func TestFileStoreDeleteRangeTwoGaps(t *testing.T) {
|
|
fcfg := FileStoreConfig{
|
|
Cipher: NoCipher,
|
|
Compression: NoCompression,
|
|
StoreDir: t.TempDir(),
|
|
BlockSize: 256,
|
|
}
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
checkDeleteBlocks := func(exp DeleteBlocks) {
|
|
t.Helper()
|
|
dBlocks := fs.deleteBlocks()
|
|
require_Equal(t, len(exp), len(dBlocks))
|
|
|
|
for i, found := range dBlocks {
|
|
ef, el, en := exp[i].State()
|
|
ff, fl, fn := found.State()
|
|
|
|
require_Equal(t, reflect.TypeOf(exp[i]), reflect.TypeOf(found))
|
|
require_Equal(t, ef, ff)
|
|
require_Equal(t, el, fl)
|
|
require_Equal(t, en, fn)
|
|
}
|
|
}
|
|
|
|
msg := make([]byte, 256)
|
|
for range 20 {
|
|
_, _, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Initially, we have sequences [1,20].
|
|
// Each message fills one entire block
|
|
require_Equal(t, len(fs.blks), 20)
|
|
|
|
// Make two gaps by removing sequences 10 and 15
|
|
// [1,9] [11,14] [16,20]
|
|
fs.RemoveMsg(10)
|
|
fs.RemoveMsg(15)
|
|
|
|
// If bug is present: deleteBlocks extends the first gap
|
|
// to the second, even though non-deleted blocks are in
|
|
// between the two gaps. Expect two distinct gaps.
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 10, Num: 1},
|
|
&DeleteRange{First: 15, Num: 1},
|
|
})
|
|
}
|
|
|
|
func TestFileStoreDeleteBlocksWithSingleMessageBlocks(t *testing.T) {
|
|
fcfg := FileStoreConfig{
|
|
Cipher: NoCipher,
|
|
Compression: NoCompression,
|
|
StoreDir: t.TempDir(),
|
|
BlockSize: 256,
|
|
}
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
checkDeleteBlocks := func(exp DeleteBlocks) {
|
|
dBlocks := fs.deleteBlocks()
|
|
require_Equal(t, len(exp), len(dBlocks))
|
|
|
|
for i, found := range dBlocks {
|
|
ef, el, en := exp[i].State()
|
|
ff, fl, fn := found.State()
|
|
|
|
require_Equal(t, reflect.TypeOf(exp[i]), reflect.TypeOf(found))
|
|
require_Equal(t, ef, ff)
|
|
require_Equal(t, el, fl)
|
|
require_Equal(t, en, fn)
|
|
}
|
|
}
|
|
|
|
msg := make([]byte, 256)
|
|
for range 20 {
|
|
_, _, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Initially, we have sequences [1,20].
|
|
// Each message fills one entire block
|
|
require_Equal(t, len(fs.blks), 20)
|
|
|
|
// Removing sequence 10 leaves a gap
|
|
// [1,9] [11,20]
|
|
fs.RemoveMsg(10)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 10, Num: 1},
|
|
})
|
|
|
|
// Extend the gap by removing 11
|
|
// [1,9] [12,20]
|
|
fs.RemoveMsg(11)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 10, Num: 2},
|
|
})
|
|
|
|
// Make another gap by removing 5
|
|
// [1,4] [6,9] [12,20]
|
|
fs.RemoveMsg(5)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 5, Num: 1},
|
|
&DeleteRange{First: 10, Num: 2},
|
|
})
|
|
|
|
// Remove blocks in the middle
|
|
// [1,4] [12,20]
|
|
fs.RemoveMsg(6)
|
|
fs.RemoveMsg(7)
|
|
fs.RemoveMsg(8)
|
|
fs.RemoveMsg(9)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 5, Num: 7},
|
|
})
|
|
|
|
// Make a couple more gaps by removing 3 and 16,17,18
|
|
// [1,2] [4,4] [12,15] [19,20]
|
|
fs.RemoveMsg(3)
|
|
fs.RemoveMsg(16)
|
|
fs.RemoveMsg(17)
|
|
fs.RemoveMsg(18)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 3, Num: 1},
|
|
&DeleteRange{First: 5, Num: 7},
|
|
&DeleteRange{First: 16, Num: 3},
|
|
})
|
|
|
|
// Removing the first sequence make no difference in gaps
|
|
// [2,2] [4,4] [12,15] [19,20]
|
|
fs.RemoveMsg(1)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 3, Num: 1},
|
|
&DeleteRange{First: 5, Num: 7},
|
|
&DeleteRange{First: 16, Num: 3},
|
|
})
|
|
|
|
// Removing the last sequence creates a gap
|
|
// [2,2] [4,4] [12,15] [19,19] (empty block starting at 21)
|
|
fs.RemoveMsg(20)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 3, Num: 1},
|
|
&DeleteRange{First: 5, Num: 7},
|
|
&DeleteRange{First: 16, Num: 3},
|
|
&DeleteRange{First: 20, Num: 1},
|
|
})
|
|
|
|
// Extend the last gap by removing 19
|
|
// [2,2] [4,4] [12,15] (empty block starting at 21)
|
|
fs.RemoveMsg(19)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 3, Num: 1},
|
|
&DeleteRange{First: 5, Num: 7},
|
|
&DeleteRange{First: 16, Num: 5},
|
|
})
|
|
}
|
|
|
|
func makeSequenceSet(seqs []uint64) *avl.SequenceSet {
|
|
var set avl.SequenceSet
|
|
for _, seq := range seqs {
|
|
set.Insert(seq)
|
|
}
|
|
return &set
|
|
}
|
|
|
|
func TestFileStoreDeleteBlocks(t *testing.T) {
|
|
fcfg := FileStoreConfig{
|
|
Cipher: NoCipher,
|
|
Compression: NoCompression,
|
|
StoreDir: t.TempDir(),
|
|
BlockSize: 256,
|
|
}
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
checkDeleteBlocks := func(exp DeleteBlocks) {
|
|
dBlocks := fs.deleteBlocks()
|
|
require_Equal(t, len(exp), len(dBlocks))
|
|
|
|
for i, found := range dBlocks {
|
|
ef, el, en := exp[i].State()
|
|
ff, fl, fn := found.State()
|
|
|
|
require_Equal(t, reflect.TypeOf(exp[i]), reflect.TypeOf(found))
|
|
require_Equal(t, ef, ff)
|
|
require_Equal(t, el, fl)
|
|
require_Equal(t, en, fn)
|
|
}
|
|
}
|
|
|
|
msg := make([]byte, 20)
|
|
for range 20 {
|
|
_, _, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// We have 5 blocks, with 4 messages each.
|
|
// block 1: [1,4]
|
|
// block 2: [5,8]
|
|
// block 3: [9,12]
|
|
// block 4: [13,16]
|
|
// block 5: [17,20]
|
|
require_Equal(t, len(fs.blks), 5)
|
|
|
|
// Removing sequence 9, creates a gap between blocks 2 and 3
|
|
// block 1: [1,4]
|
|
// block 2: [5,8]
|
|
// block 3: [10,12]
|
|
// block 4: [13,16]
|
|
// block 5: [17,20]
|
|
fs.RemoveMsg(9)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 9, Num: 1},
|
|
})
|
|
|
|
// Extend the gap by removing 10
|
|
// block 1: [1,4]
|
|
// block 2: [5,8]
|
|
// block 3: [11,12]
|
|
// block 4: [13,16]
|
|
// block 5: [17,20]
|
|
fs.RemoveMsg(10)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 9, Num: 2},
|
|
})
|
|
|
|
// Extend the gap by removing 8, creates an interior delete
|
|
// block 1: [1,4]
|
|
// block 2: [5,7]
|
|
// block 3: [11,12]
|
|
// block 4: [13,16]
|
|
// block 5: [17,20]
|
|
fs.RemoveMsg(8)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
makeSequenceSet([]uint64{8}),
|
|
&DeleteRange{First: 9, Num: 2},
|
|
})
|
|
|
|
// Make another gap by removing 17 and 18
|
|
// block 1: [1,4]
|
|
// block 2: [5,7]
|
|
// block 3: [11,12]
|
|
// block 4: [13,16]
|
|
// block 5: [19,20]
|
|
fs.RemoveMsg(18)
|
|
fs.RemoveMsg(17)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
makeSequenceSet([]uint64{8}),
|
|
&DeleteRange{First: 9, Num: 2},
|
|
&DeleteRange{First: 17, Num: 2},
|
|
})
|
|
|
|
// Remove messages 5, 6, and 7 to create a fully deleted block
|
|
// block 1: [1,4]
|
|
// block 2: [11,12]
|
|
// block 3: [13,16]
|
|
// block 4: [19,20]
|
|
fs.RemoveMsg(6)
|
|
fs.RemoveMsg(5)
|
|
fs.RemoveMsg(7)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 5, Num: 6},
|
|
&DeleteRange{First: 17, Num: 2},
|
|
})
|
|
|
|
// Remove the last message
|
|
// block 1: [1,4]
|
|
// block 2: [11,12]
|
|
// block 3: [13,16]
|
|
// block 4: [19,19]
|
|
fs.RemoveMsg(20)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 5, Num: 6},
|
|
&DeleteRange{First: 17, Num: 2},
|
|
makeSequenceSet([]uint64{20}),
|
|
})
|
|
|
|
// Remove the last message
|
|
// block 1: [1,4]
|
|
// block 2: [11,12]
|
|
// block 3: [13,16]
|
|
fs.RemoveMsg(19)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 5, Num: 6},
|
|
&DeleteRange{First: 17, Num: 4},
|
|
})
|
|
|
|
// Remove block in the middle
|
|
// block 1: [1,4]
|
|
// block 2: [13,16]
|
|
fs.RemoveMsg(11)
|
|
fs.RemoveMsg(12)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 5, Num: 8},
|
|
&DeleteRange{First: 17, Num: 4},
|
|
})
|
|
|
|
// Remove the first block
|
|
// block 1: [13,16]
|
|
fs.RemoveMsg(1)
|
|
fs.RemoveMsg(2)
|
|
fs.RemoveMsg(3)
|
|
fs.RemoveMsg(4)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 17, Num: 4},
|
|
})
|
|
}
|
|
|
|
func TestFileStoreDeleteBlocksWithManyEmptyBlocks(t *testing.T) {
|
|
fcfg := FileStoreConfig{
|
|
Cipher: NoCipher,
|
|
Compression: NoCompression,
|
|
StoreDir: t.TempDir(),
|
|
BlockSize: 33 * 6,
|
|
}
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
checkDeleteBlocks := func(exp DeleteBlocks) {
|
|
t.Helper()
|
|
dBlocks := fs.deleteBlocks()
|
|
require_Equal(t, len(exp), len(dBlocks))
|
|
|
|
for i, found := range dBlocks {
|
|
ef, el, en := exp[i].State()
|
|
ff, fl, fn := found.State()
|
|
|
|
require_Equal(t, reflect.TypeOf(exp[i]), reflect.TypeOf(found))
|
|
require_Equal(t, ef, ff)
|
|
require_Equal(t, el, fl)
|
|
require_Equal(t, en, fn)
|
|
}
|
|
}
|
|
|
|
// 1.blk, 6 msgs
|
|
for range 6 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
// *.blk, tombstone for 1.blk + 2 msgs
|
|
// every next block adds 2 tombstones for the previous block
|
|
for i := range 5 {
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
|
|
// Every iteration will remove one message from 1.blk
|
|
_, err = fs.RemoveMsg(uint64(i + 2))
|
|
require_NoError(t, err)
|
|
|
|
// Every block after the first removes two messages from the previous block.
|
|
if i > 0 {
|
|
for j := range 2 {
|
|
_, err = fs.RemoveMsg(uint64(7 + (i-1)*2 + j))
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
// Every block except for the last stores two new messages for the next block to delete.
|
|
if i < 4 {
|
|
for range 2 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
makeSequenceSet([]uint64{2, 3, 4, 5, 6}),
|
|
&DeleteRange{First: 7, Num: 8},
|
|
})
|
|
|
|
fs.mu.Lock()
|
|
fs.lockAllMsgBlocks()
|
|
for _, mb := range fs.blks {
|
|
mb.compact()
|
|
}
|
|
fs.unlockAllMsgBlocks()
|
|
fs.mu.Unlock()
|
|
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 2, Num: 13},
|
|
})
|
|
}
|
|
|
|
func TestFileStoreTrailingSkipMsgsFromStreamStateFile(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
cfg := StreamConfig{Name: "zzz", Storage: FileStorage, Subjects: []string{">"}}
|
|
created := time.Now()
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for range 10 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
fs.mu.RLock()
|
|
lmb := fs.lmb
|
|
fs.mu.RUnlock()
|
|
lmb.mu.RLock()
|
|
mfn := lmb.mfn
|
|
lmb.mu.RUnlock()
|
|
require_NotEqual(t, mfn, _EMPTY_)
|
|
|
|
// Stop the store and truncate the block file.
|
|
// Stopping should write out our stream state file, letting us "remember" the highest last sequence.
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Truncate(mfn, 33*5))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// We should recover with the right state, the last sequence coming from the stream state file,
|
|
// and the messages from the recovered blocks.
|
|
state := fs.State()
|
|
require_Equal(t, state.Msgs, 5)
|
|
require_Equal(t, state.FirstSeq, 1)
|
|
require_Equal(t, state.LastSeq, 10)
|
|
|
|
// Go through all messages, they should properly report deletes.
|
|
for seq := uint64(1); seq <= 10; seq++ {
|
|
_, err = fs.LoadMsg(seq, nil)
|
|
if seq <= 5 {
|
|
require_NoError(t, err)
|
|
} else {
|
|
require_Error(t, err, ErrStoreMsgNotFound)
|
|
}
|
|
}
|
|
|
|
// Also check a new block was created to represent this.
|
|
fs.mu.RLock()
|
|
lmb = fs.lmb
|
|
lblks := len(fs.blks)
|
|
fs.mu.RUnlock()
|
|
lmb.mu.RLock()
|
|
ldmap := lmb.dmap.Size()
|
|
lmb.mu.RUnlock()
|
|
require_Len(t, lblks, 2)
|
|
require_Len(t, ldmap, 0)
|
|
require_Equal(t, atomic.LoadUint64(&lmb.first.seq), 11)
|
|
require_Equal(t, atomic.LoadUint64(&lmb.last.seq), 10)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreSelectMsgBlockBinarySearch(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
created := time.Now()
|
|
cfg := StreamConfig{Name: "zzz", Storage: FileStorage, Subjects: []string{">"}}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
for i := range 33 {
|
|
if i > 0 {
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
}
|
|
for range 2 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
if i == 15 {
|
|
for _, seq := range []uint64{2, 5} {
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
_, err = fs.RemoveMsg(seq)
|
|
require_NoError(t, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
test := func() {
|
|
// Select a block containing a message we deleted.
|
|
i, mb := fs.selectMsgBlockWithIndex(2)
|
|
require_Equal(t, i, 0)
|
|
require_Equal(t, mb.index, 1)
|
|
|
|
// Once more for the other deleted message.
|
|
i, mb = fs.selectMsgBlockWithIndex(5)
|
|
require_Equal(t, i, 2)
|
|
require_Equal(t, mb.index, 3)
|
|
|
|
// Select the first block/message.
|
|
i, mb = fs.selectMsgBlockWithIndex(1)
|
|
require_Equal(t, i, 0)
|
|
require_Equal(t, mb.index, 1)
|
|
|
|
// Select a block before the delete block, but after the deleted sequences.
|
|
i, mb = fs.selectMsgBlockWithIndex(10)
|
|
require_Equal(t, i, 4)
|
|
require_Equal(t, mb.index, 5)
|
|
|
|
// Select a block after the delete block AND after the deleted sequences.
|
|
i, mb = fs.selectMsgBlockWithIndex(64)
|
|
require_Equal(t, i, 33)
|
|
require_Equal(t, mb.index, 34)
|
|
}
|
|
test()
|
|
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
test()
|
|
})
|
|
}
|
|
|
|
func TestFileStoreRemoveMsgsInRange(t *testing.T) {
|
|
fcfg := FileStoreConfig{Cipher: NoCipher, Compression: NoCompression, StoreDir: t.TempDir(), BlockSize: 256}
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := make([]byte, 256)
|
|
for range 20 {
|
|
_, _, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
fs.mu.Lock()
|
|
defer fs.mu.Unlock()
|
|
|
|
checkDeleteBlocks := func(exp DeleteBlocks) {
|
|
dBlocks := fs.deleteBlocks()
|
|
require_Equal(t, len(exp), len(dBlocks))
|
|
|
|
for i, found := range dBlocks {
|
|
ef, el, en := exp[i].State()
|
|
ff, fl, fn := found.State()
|
|
|
|
require_Equal(t, reflect.TypeOf(exp[i]), reflect.TypeOf(found))
|
|
require_Equal(t, ef, ff)
|
|
require_Equal(t, el, fl)
|
|
require_Equal(t, en, fn)
|
|
}
|
|
}
|
|
|
|
require_Equal(t, len(fs.blks), 20)
|
|
|
|
// Remove range [1,1]
|
|
fs.removeMsgsInRange(1, 1, true)
|
|
require_Equal(t, len(fs.blks), 19)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].first.seq), 2)
|
|
|
|
// Removing range [1,1] again is a noop
|
|
// We are left with blocks [2,20]
|
|
fs.removeMsgsInRange(1, 1, true)
|
|
require_Equal(t, len(fs.blks), 19)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].first.seq), 2)
|
|
|
|
// Remove the range [1,2] should remove block with sequence 2
|
|
// We are left with blocks [3,20]
|
|
fs.removeMsgsInRange(1, 2, true)
|
|
require_Equal(t, len(fs.blks), 18)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].first.seq), 3)
|
|
|
|
// Remove the first two blocks [3,4]
|
|
// We are left with blocks [5,20]
|
|
fs.removeMsgsInRange(3, 4, true)
|
|
require_Equal(t, len(fs.blks), 16)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].first.seq), 5)
|
|
|
|
// Remove range [9, 13]
|
|
// We are left with [5,8] [14,20]
|
|
fs.removeMsgsInRange(9, 13, true)
|
|
require_Equal(t, len(fs.blks), 11)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 9, Num: 5},
|
|
})
|
|
|
|
// Make the gap larger by removing range [8 8]
|
|
// We are left with [5,7] [14, 20]
|
|
fs.removeMsgsInRange(8, 8, true)
|
|
require_Equal(t, len(fs.blks), 10)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 8, Num: 6},
|
|
})
|
|
|
|
// Make another gap by removing range [17, 17]
|
|
// We are left with [5,7] [14,16] [18,20]
|
|
fs.removeMsgsInRange(17, 17, true)
|
|
require_Equal(t, len(fs.blks), 9)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 8, Num: 6},
|
|
&DeleteRange{First: 17, Num: 1},
|
|
})
|
|
|
|
// Remove the last block
|
|
// We are left with [5,7] [14,16] [18,19] (empty block 21-20)
|
|
fs.removeMsgsInRange(20, 20, true)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 8, Num: 6},
|
|
&DeleteRange{First: 17, Num: 1},
|
|
&DeleteRange{First: 20, Num: 1},
|
|
})
|
|
|
|
// Make a big gap removing range [7, 18]
|
|
// We are left with [5,6] [19]
|
|
fs.removeMsgsInRange(7, 18, true)
|
|
require_Equal(t, len(fs.blks), 4)
|
|
checkDeleteBlocks(DeleteBlocks{
|
|
&DeleteRange{First: 7, Num: 12},
|
|
&DeleteRange{First: 20, Num: 1},
|
|
})
|
|
|
|
// Remove everything
|
|
// We are left with an empty block
|
|
fs.removeMsgsInRange(1, 20, true)
|
|
require_Equal(t, len(fs.blks), 1)
|
|
require_Equal(t, fs.blks[0].msgs, 0)
|
|
}
|
|
|
|
func TestFileStoreRemoveMsgsInRangePartialBlocks(t *testing.T) {
|
|
fcfg := FileStoreConfig{
|
|
Cipher: NoCipher,
|
|
Compression: NoCompression,
|
|
StoreDir: t.TempDir(),
|
|
BlockSize: 256,
|
|
}
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
msg := make([]byte, 16)
|
|
for range 20 {
|
|
_, _, err = fs.StoreMsg("foo", nil, msg, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
fs.mu.Lock()
|
|
defer fs.mu.Unlock()
|
|
|
|
// Initially:
|
|
// block 0 [ 1, 5]
|
|
// block 1 [ 6,10]
|
|
// block 2 [11,15]
|
|
// block 3 [16,20]
|
|
require_Equal(t, len(fs.blks), 4)
|
|
|
|
// Remove [5,8] leaves:
|
|
// block 0 [ 1, 4]
|
|
// block 1 [ 9,10]
|
|
// block 2 [11,15]
|
|
// block 3 [16,20]
|
|
fs.removeMsgsInRange(5, 6, true)
|
|
fs.blks[0].compact()
|
|
|
|
require_Equal(t, len(fs.blks), 4)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].first.seq), 1)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].last.seq), 4)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[1].first.seq), 7)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[1].last.seq), 10)
|
|
|
|
// Remove [5,8] leaves:
|
|
// block 0 [ 1, 3]
|
|
// block 1 [12,15]
|
|
// block 2 [16,20]
|
|
// empty block 21 20
|
|
fs.removeMsgsInRange(4, 11, true)
|
|
fs.blks[0].compact()
|
|
|
|
require_Equal(t, len(fs.blks), 3)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].first.seq), 1)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].last.seq), 3)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[1].first.seq), 12)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[1].last.seq), 15)
|
|
|
|
// Remove [13,14] leaves:
|
|
// block 0 [ 1, 3]
|
|
// block 1 12 and 15
|
|
// block 2 [16,20]
|
|
fs.removeMsgsInRange(13, 14, true)
|
|
fs.blks[1].compact()
|
|
|
|
require_Equal(t, len(fs.blks), 3)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].first.seq), 1)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].last.seq), 3)
|
|
require_Equal(t, fs.blks[1].msgs, 2)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[1].first.seq), 12)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[1].last.seq), 15)
|
|
|
|
// Remove [13,20] leaves:
|
|
// block 0 [ 1, 3]
|
|
// block 1 12
|
|
// empty block 21 20
|
|
fs.removeMsgsInRange(13, 20, true)
|
|
fs.blks[1].compact()
|
|
|
|
require_Equal(t, len(fs.blks), 3)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].first.seq), 1)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].last.seq), 3)
|
|
require_Equal(t, fs.blks[1].msgs, 1)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[1].first.seq), 12)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[1].last.seq), 12)
|
|
|
|
// Remove [10,20] leaves:
|
|
// block 0 [ 1, 3]
|
|
// empty block 21 20
|
|
fs.removeMsgsInRange(10, 20, true)
|
|
require_Equal(t, len(fs.blks), 2)
|
|
|
|
// Remove everything
|
|
// empty block 21 20
|
|
fs.removeMsgsInRange(1, 30, true)
|
|
|
|
require_Equal(t, len(fs.blks), 1)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].first.seq), 21)
|
|
require_Equal(t, atomic.LoadUint64(&fs.blks[0].last.seq), 20)
|
|
}
|
|
|
|
func TestFileStoreRemoveMsgsInRangeWithTombstones(t *testing.T) {
|
|
fcfg := FileStoreConfig{
|
|
Cipher: NoCipher,
|
|
Compression: NoCompression,
|
|
StoreDir: t.TempDir(),
|
|
BlockSize: 256,
|
|
}
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
// Block 1: [1,4]
|
|
for range 4 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Block 2: [5,8]
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
for range 4 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
// Block 3 [9,12] - tombs {4,3,2,10}
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
for range 4 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
fs.RemoveMsg(4)
|
|
fs.RemoveMsg(3)
|
|
fs.RemoveMsg(2)
|
|
fs.RemoveMsg(10)
|
|
|
|
// Block 4 [13, 16] - tombs {14,15}
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
for range 4 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
fs.RemoveMsg(14)
|
|
fs.RemoveMsg(15)
|
|
|
|
// Block 5 [17,17]
|
|
_, err = fs.newMsgBlockForWrite()
|
|
require_NoError(t, err)
|
|
for range 4 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
}
|
|
|
|
checkBlock := func(mb *msgBlock, fseq, lseq uint64, tombs []uint64) {
|
|
t.Helper()
|
|
require_Equal(t, mb.first.seq, fseq)
|
|
require_Equal(t, mb.last.seq, lseq)
|
|
mbTombs := make([]uint64, len(mb.tombs()))
|
|
for i, id := range mb.tombs() {
|
|
mbTombs[i] = id.seq
|
|
}
|
|
require_True(t, slices.Equal(tombs, mbTombs))
|
|
}
|
|
|
|
require_Equal(t, len(fs.blks), 5)
|
|
checkBlock(fs.blks[0], 1, 4, nil)
|
|
checkBlock(fs.blks[1], 5, 8, nil)
|
|
checkBlock(fs.blks[2], 9, 12, []uint64{4, 3, 2, 10})
|
|
checkBlock(fs.blks[3], 13, 16, []uint64{14, 15})
|
|
checkBlock(fs.blks[4], 17, 20, nil)
|
|
|
|
// for i, mb := range fs.blks {
|
|
// mbFirstSeq := atomic.LoadUint64(&mb.first.seq)
|
|
// mbLastSeq := atomic.LoadUint64(&mb.last.seq)
|
|
// t.Log(i, mb.msgs, mbFirstSeq, mbLastSeq, mb.tombs())
|
|
// }
|
|
|
|
// Remove sequences 4-17
|
|
// Block 1: removing the tail, noop because already deleted
|
|
// Block 2: has no prior tombs, should be purged entirely
|
|
// Block 3: has prior tombs, should be preserved
|
|
// Block 4: has "internal" tombs, can be purged entirely
|
|
// Block 5: updates mb first
|
|
fs.removeMsgsInRange(4, 17, false)
|
|
|
|
checkBlock(fs.blks[0], 1, 4, nil)
|
|
checkBlock(fs.blks[1], 13, 12, []uint64{4, 3, 2, 10})
|
|
checkBlock(fs.blks[2], 18, 20, []uint64{9, 11, 17})
|
|
|
|
// Remove everything (past the last sequence)
|
|
fs.removeMsgsInRange(1, 100, false)
|
|
require_Equal(t, len(fs.blks), 1)
|
|
checkBlock(fs.blks[0], 21, 20, []uint64{20})
|
|
|
|
// Attempt to remove the empty block
|
|
fs.removeMsgsInRange(1, 100, false)
|
|
require_Equal(t, len(fs.blks), 1)
|
|
checkBlock(fs.blks[0], 21, 20, []uint64{20})
|
|
}
|
|
|
|
func TestFileStoreCorrectChecksumAfterTruncate(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
var fchkCmp []byte
|
|
var fchk [8]byte
|
|
for i := range 5 {
|
|
_, _, err = fs.StoreMsg("foo", nil, nil, 0)
|
|
require_NoError(t, err)
|
|
if i == 2 {
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.RLock()
|
|
fchkCmp = mb.lastChecksum()
|
|
copy(fchk[:], mb.lchk[:])
|
|
mb.mu.RUnlock()
|
|
}
|
|
}
|
|
require_True(t, bytes.Equal(fchkCmp, fchk[:]))
|
|
|
|
// After truncating we should return to the checksum we stored above.
|
|
require_NoError(t, fs.Truncate(3))
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.RLock()
|
|
lchkCmp := mb.lastChecksum()
|
|
var lchk [8]byte
|
|
copy(lchk[:], mb.lchk[:])
|
|
mb.mu.RUnlock()
|
|
require_True(t, bytes.Equal(lchkCmp, lchk[:]))
|
|
require_True(t, bytes.Equal(fchkCmp, lchkCmp))
|
|
})
|
|
}
|
|
|
|
func TestFileStoreRecoverTTLAndScheduleStateAndCounters(t *testing.T) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
created := time.Now()
|
|
cfg := StreamConfig{Name: "zzz", Storage: FileStorage, Subjects: []string{">"}, AllowMsgTTL: true, AllowMsgSchedules: true}
|
|
fs, err := newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
ttl := int64(5)
|
|
sched := time.Now().Add(5 * time.Second)
|
|
hdr := genHeader(nil, JSMessageTTL, strconv.FormatInt(ttl, 10))
|
|
hdr = genHeader(hdr, JSSchedulePattern, fmt.Sprintf("@at %s", sched.Format(time.RFC3339)))
|
|
_, _, err = fs.StoreMsg("foo", hdr, nil, ttl)
|
|
require_NoError(t, err)
|
|
|
|
mb := fs.getFirstBlock()
|
|
mb.mu.RLock()
|
|
ttls := mb.ttls
|
|
schedules := mb.schedules
|
|
mb.mu.RUnlock()
|
|
require_Equal(t, ttls, 1)
|
|
require_Equal(t, schedules, 1)
|
|
|
|
require_NoError(t, fs.Stop())
|
|
require_NoError(t, os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)))
|
|
|
|
fs, err = newFileStoreWithCreated(fcfg, cfg, created, prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
mb = fs.getFirstBlock()
|
|
mb.mu.RLock()
|
|
ttls = mb.ttls
|
|
schedules = mb.schedules
|
|
mb.mu.RUnlock()
|
|
require_Equal(t, ttls, 1)
|
|
require_Equal(t, schedules, 1)
|
|
})
|
|
}
|
|
|
|
func TestFileStoreCorruptionSetsHbitWithoutHeaders(t *testing.T) {
|
|
const (
|
|
KindMsgFromBuf = iota
|
|
KindIndexCacheBuf
|
|
KindRebuildState
|
|
)
|
|
test := func(t *testing.T, kind int) {
|
|
testFileStoreAllPermutations(t, func(t *testing.T, fcfg FileStoreConfig) {
|
|
fs, err := newFileStoreWithCreated(fcfg, StreamConfig{Name: "zzz", Storage: FileStorage, Subjects: []string{">"}}, time.Now(), prf(&fcfg), nil)
|
|
require_NoError(t, err)
|
|
defer fs.Stop()
|
|
|
|
mb := fs.getFirstBlock()
|
|
fs.mu.Lock()
|
|
mb.mu.Lock()
|
|
err = mb.writeMsgRecordLocked(emptyRecordLen|hbit, 1, _EMPTY_, nil, nil, 0, false, false)
|
|
cache := mb.ecache.Value()
|
|
mb.mu.Unlock()
|
|
fs.mu.Unlock()
|
|
require_NoError(t, err)
|
|
require_NotNil(t, cache)
|
|
|
|
switch kind {
|
|
case KindMsgFromBuf:
|
|
mb.mu.Lock()
|
|
_, err = mb.msgFromBufNoCopy(cache.buf, nil, mb.hh)
|
|
mb.mu.Unlock()
|
|
require_True(t, strings.Contains(err.Error(), "sanity check failed"))
|
|
case KindIndexCacheBuf:
|
|
mb.mu.Lock()
|
|
err = mb.indexCacheBuf(cache.buf)
|
|
mb.mu.Unlock()
|
|
require_Error(t, err, errCorruptState)
|
|
case KindRebuildState:
|
|
require_NoError(t, mb.flushPendingMsgs())
|
|
_, _, err = mb.rebuildState()
|
|
require_True(t, err != nil)
|
|
require_True(t, strings.Contains(err.Error(), "sanity check failed"))
|
|
default:
|
|
t.Fatalf("unknown kind %d", kind)
|
|
}
|
|
})
|
|
}
|
|
t.Run("msgFromBuf", func(t *testing.T) { test(t, KindMsgFromBuf) })
|
|
t.Run("indexCacheBuf", func(t *testing.T) { test(t, KindIndexCacheBuf) })
|
|
t.Run("rebuildState", func(t *testing.T) { test(t, KindRebuildState) })
|
|
}
|