[FIXED] Filestore only stores last block when MaxMsgsPerSubject 1 (#8254)
Commit https://github.com/nats-io/nats-server/commit/f8656fe8ea5472e7be24435017e5c7b4a27a174a removed the `info.fblk = info.lblk` which was done by default when hitting `info.total == 1`. This is correct, since if you'd remove the last message in the last block, you wouldn't be able to find the first message in the first block. In 2.14 this fix was included with https://github.com/nats-io/nats-server/commit/08275d90cfb844a148571e913ff7d3fc62e16a9e, which ensures both `fblk` and `lblk` are stored in the `index.db` file. However, since this fix was not included since 2.12.7, this had a side-effect that the stale `fblk` was always included in `index.db`, without it being changed to reflect latest writes (which the previous `info.fblk = info.lblk` was in essence hiding). That would result in not being able to find certain messages if not being written to frequently, after a server restart that recovers from this stale index. This PR returns that check, but only does so if `MaxMsgsPerSubject: 1`, since only then can we be sure to do that optimization. Also fix a bug where an update from `MaxMsgsPerSubject: -1` to `1` was not enforced during the update.
This commit is contained in:
+11
-7
@@ -743,7 +743,7 @@ func (fs *fileStore) UpdateConfig(cfg *StreamConfig) error {
|
||||
fs.ageChkTime = 0
|
||||
}
|
||||
|
||||
if fs.cfg.MaxMsgsPer > 0 && (old_cfg.MaxMsgsPer == 0 || fs.cfg.MaxMsgsPer < old_cfg.MaxMsgsPer) {
|
||||
if fs.cfg.MaxMsgsPer > 0 && (old_cfg.MaxMsgsPer <= 0 || fs.cfg.MaxMsgsPer < old_cfg.MaxMsgsPer) {
|
||||
if err := fs.enforceMsgPerSubjectLimit(true); err != nil {
|
||||
fs.mu.Unlock()
|
||||
return err
|
||||
@@ -4930,15 +4930,17 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t
|
||||
}
|
||||
|
||||
// Adjust top level tracking of per subject msg counts.
|
||||
var info *psi
|
||||
var ok bool
|
||||
if len(subj) > 0 && fs.psim != nil {
|
||||
index := fs.lmb.index
|
||||
if info, ok := fs.psim.Find(stringToBytes(subj)); ok {
|
||||
if info, ok = fs.psim.Find(stringToBytes(subj)); ok {
|
||||
info.total++
|
||||
if index > info.lblk {
|
||||
info.lblk = index
|
||||
}
|
||||
} else {
|
||||
fs.psim.Insert(stringToBytes(subj), psi{total: 1, fblk: index, lblk: index})
|
||||
info, _ = fs.psim.Insert(stringToBytes(subj), psi{total: 1, fblk: index, lblk: index})
|
||||
fs.tsl += len(subj)
|
||||
}
|
||||
}
|
||||
@@ -4993,6 +4995,10 @@ func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, t
|
||||
}
|
||||
}
|
||||
}
|
||||
// If we only ever store one/last message for a subject, can correct the first block to where we've just written.
|
||||
if info != nil && info.total == 1 && mmp == 1 {
|
||||
info.fblk = info.lblk
|
||||
}
|
||||
|
||||
// Limits checks and enforcement.
|
||||
// If they do any deletions they will update the
|
||||
@@ -9005,10 +9011,8 @@ func (fs *fileStore) loadLastLocked(subj string, sm *StoreMsg) (lsm *StoreMsg, e
|
||||
if ss.lastNeedsUpdate {
|
||||
// mb is already loaded into the cache so should be fast-ish.
|
||||
if err = mb.recalculateForSubj(subj, ss); err != nil {
|
||||
if err != nil {
|
||||
mb.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
mb.mu.Unlock()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
l = ss.Last
|
||||
|
||||
@@ -8583,6 +8583,65 @@ func Benchmark_FileStoreCreateConsumerStores(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
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()},
|
||||
|
||||
+1
-1
@@ -123,7 +123,7 @@ func (ms *memStore) UpdateConfig(cfg *StreamConfig) error {
|
||||
maxp := ms.maxp
|
||||
ms.maxp = cfg.MaxMsgsPer
|
||||
// If the value is smaller, or was unset before, we need to enforce that.
|
||||
if ms.maxp > 0 && (maxp == 0 || ms.maxp < maxp) {
|
||||
if ms.maxp > 0 && (maxp <= 0 || ms.maxp < maxp) {
|
||||
lm := uint64(ms.maxp)
|
||||
ms.fss.IterFast(func(subj []byte, ss *SimpleState) bool {
|
||||
if ss.Msgs > lm {
|
||||
|
||||
@@ -447,6 +447,84 @@ func TestStoreMaxMsgsPerUpdateBug(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
func TestStoreMaxMsgsPerUpdateToOneRemoveNewest(t *testing.T) {
|
||||
config := func() StreamConfig {
|
||||
return StreamConfig{Name: "TEST", Subjects: []string{"foo.*"}, MaxMsgsPer: -1}
|
||||
}
|
||||
testAllStoreAllPermutations(
|
||||
t, false, config(),
|
||||
func(t *testing.T, fs StreamStore) {
|
||||
// Store the first copy of "foo.0".
|
||||
_, _, err := fs.StoreMsg("foo.0", nil, nil, 0)
|
||||
require_NoError(t, err)
|
||||
|
||||
// Store filler subjects with enough data that the filestore rolls over into a
|
||||
// second message block, so both copies of "foo.0" live in different blocks.
|
||||
msg := make([]byte, 1024*1024)
|
||||
fillBlock := func(expectedBlocks int) {
|
||||
for i := 1; i <= 9; i++ {
|
||||
_, _, err := fs.StoreMsg(fmt.Sprintf("foo.%d", i), nil, msg, 0)
|
||||
require_NoError(t, err)
|
||||
}
|
||||
if f, ok := fs.(*fileStore); ok {
|
||||
require_True(t, f.numMsgBlocks() >= expectedBlocks)
|
||||
}
|
||||
}
|
||||
fillBlock(2)
|
||||
|
||||
// Store a second copy of "foo.0".
|
||||
nseq, _, err := fs.StoreMsg("foo.0", nil, nil, 0)
|
||||
require_NoError(t, err)
|
||||
|
||||
// And a third, in its own block, which we'll remove as well.
|
||||
fillBlock(3)
|
||||
lseq, _, err := fs.StoreMsg("foo.0", nil, nil, 0)
|
||||
require_NoError(t, err)
|
||||
removed, err := fs.RemoveMsg(lseq)
|
||||
require_NoError(t, err)
|
||||
require_True(t, removed)
|
||||
|
||||
// Update max messages per-subject from -1 (unlimited) to 1.
|
||||
// This transition does not run per-subject limit enforcement, so both copies remain.
|
||||
cfg := config()
|
||||
if _, ok := fs.(*fileStore); ok {
|
||||
cfg.Storage = FileStorage
|
||||
} else {
|
||||
cfg.Storage = MemoryStorage
|
||||
}
|
||||
cfg.MaxMsgsPer = 1
|
||||
require_NoError(t, fs.UpdateConfig(&cfg))
|
||||
|
||||
ss := fs.SubjectsState("foo.0")["foo.0"]
|
||||
require_Equal(t, ss.Msgs, 1)
|
||||
require_Equal(t, ss.First, nseq)
|
||||
require_Equal(t, ss.Last, nseq)
|
||||
|
||||
var smv StoreMsg
|
||||
sm, err := fs.LoadLastMsg("foo.0", &smv)
|
||||
require_NoError(t, err)
|
||||
require_Equal(t, sm.seq, nseq)
|
||||
|
||||
sm, _, err = fs.LoadNextMsg("foo.0", false, 0, &smv)
|
||||
require_NoError(t, err)
|
||||
require_Equal(t, sm.seq, nseq)
|
||||
|
||||
// The older copy must also still be found after a restart.
|
||||
if f, ok := fs.(*fileStore); ok {
|
||||
fcfg := f.fcfg
|
||||
require_NoError(t, f.Stop())
|
||||
f, err = newFileStore(fcfg, cfg)
|
||||
require_NoError(t, err)
|
||||
defer f.Stop()
|
||||
|
||||
sm, err = f.LoadLastMsg("foo.0", &smv)
|
||||
require_NoError(t, err)
|
||||
require_Equal(t, sm.seq, nseq)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
func TestStoreCompactCleansUpDmap(t *testing.T) {
|
||||
config := func() StreamConfig {
|
||||
return StreamConfig{Name: "TEST", Subjects: []string{"foo"}, MaxMsgsPer: 0}
|
||||
|
||||
Reference in New Issue
Block a user