Files
mesh-controller/vendor/github.com/nats-io/nats-server/v2/server/filestore.go
T

12016 lines
311 KiB
Go

// Copyright 2019-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package server
import (
"archive/tar"
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"math"
mrand "math/rand"
"net"
"os"
"path/filepath"
"runtime"
"slices"
"sort"
"strings"
"sync"
"sync/atomic"
"time"
"github.com/klauspost/compress/s2"
"github.com/minio/highwayhash"
"github.com/nats-io/nats-server/v2/server/ats"
"github.com/nats-io/nats-server/v2/server/avl"
"github.com/nats-io/nats-server/v2/server/gsl"
"github.com/nats-io/nats-server/v2/server/stree"
"github.com/nats-io/nats-server/v2/server/thw"
"golang.org/x/crypto/chacha20"
"golang.org/x/crypto/chacha20poly1305"
)
type FileStoreConfig struct {
// Where the parent directory for all storage will be located.
StoreDir string
// BlockSize is the file block size. This also represents the maximum overhead size.
BlockSize uint64
// CacheExpire is how long with no activity until we expire the cache.
CacheExpire time.Duration
// SubjectStateExpire is how long with no activity until we expire a msg block's subject state.
SubjectStateExpire time.Duration
// SyncInterval is how often we sync to disk in the background.
SyncInterval time.Duration
// SyncAlways is when the stream should sync all data writes.
SyncAlways bool
// AsyncFlush allows async flush to batch write operations.
AsyncFlush bool
// Cipher is the cipher to use when encrypting.
Cipher StoreCipher
// Compression is the algorithm to use when compressing.
Compression StoreCompression
// Internal reference to our server.
srv *Server
}
// FileStreamInfo allows us to remember created time.
type FileStreamInfo struct {
Created time.Time
StreamConfig
}
type StoreCipher int
const (
ChaCha StoreCipher = iota
AES
NoCipher
)
func (cipher StoreCipher) String() string {
switch cipher {
case ChaCha:
return "ChaCha20-Poly1305"
case AES:
return "AES-GCM"
case NoCipher:
return "None"
default:
return "Unknown StoreCipher"
}
}
type StoreCompression uint8
const (
NoCompression StoreCompression = iota
S2Compression
)
func (alg StoreCompression) String() string {
switch alg {
case NoCompression:
return "None"
case S2Compression:
return "S2"
default:
return "Unknown StoreCompression"
}
}
func (alg StoreCompression) MarshalJSON() ([]byte, error) {
var str string
switch alg {
case S2Compression:
str = "s2"
case NoCompression:
str = "none"
default:
return nil, fmt.Errorf("unknown compression algorithm")
}
return json.Marshal(str)
}
func (alg *StoreCompression) UnmarshalJSON(b []byte) error {
var str string
if err := json.Unmarshal(b, &str); err != nil {
return err
}
switch str {
case "s2":
*alg = S2Compression
case "none":
*alg = NoCompression
default:
return fmt.Errorf("unknown compression algorithm")
}
return nil
}
// File ConsumerInfo is used for creating consumer stores.
type FileConsumerInfo struct {
Created time.Time
Name string
ConsumerConfig
}
// Default file and directory permissions.
const (
defaultDirPerms = os.FileMode(0700)
defaultFilePerms = os.FileMode(0600)
)
type psi struct {
total uint64
fblk uint32
lblk uint32
}
type fileStore struct {
srv *Server
mu sync.RWMutex
state StreamState
tombs []uint64
ld *LostStreamData
scb StorageUpdateHandler
rmcb StorageRemoveMsgHandler
sdmcb SubjectDeleteMarkerUpdateHandler
ageChk *time.Timer // Timer to expire messages.
ageChkRun bool // Whether message expiration is currently running.
ageChkTime int64 // When the message expiration is scheduled to run.
syncTmr *time.Timer
cfg FileStreamInfo
fcfg FileStoreConfig
prf keyGen
oldprf keyGen
aek cipher.AEAD
lmb *msgBlock
blks []*msgBlock
bim map[uint32]*msgBlock
psim *stree.SubjectTree[psi]
tsl int
adml int
hh *highwayhash.Digest64
qch chan struct{}
fsld chan struct{}
cmu sync.RWMutex
cfs []ConsumerStore
sips int
dirty int
closing bool
closed atomic.Bool // Atomic to reduce contention on ConsumerStores.
fip bool
receivedAny bool
firstMoved bool
ttls *thw.HashWheel
sdm *SDMMeta
lpex time.Time // Last PurgeEx call.
}
// Represents a message store block and its data.
type msgBlock struct {
// Here for 32bit systems and atomic.
first msgId
last msgId
mu sync.RWMutex
fs *fileStore
aek cipher.AEAD
bek cipher.Stream
seed []byte
nonce []byte
mfn string
mfd *os.File
cmp StoreCompression // Effective compression at the time of loading the block
liwsz int64
index uint32
bytes uint64 // User visible bytes count.
rbytes uint64 // Total bytes (raw) including deleted. Used for rolling to new blk.
cbytes uint64 // Bytes count after last compaction. 0 if no compaction happened yet.
msgs uint64 // User visible message count.
fss *stree.SubjectTree[SimpleState]
kfn string
lwts int64
llts int64
lrts int64
lsts int64
llseq uint64
hh *highwayhash.Digest64
cache *cache
cloads uint64
cexp time.Duration
fexp time.Duration
ctmr *time.Timer
werr error
dmap avl.SequenceSet
fch chan struct{}
qch chan struct{}
lchk [8]byte
loading bool
flusher bool
noTrack bool
needSync bool
syncAlways bool
noCompact bool
closed bool
ttls uint64 // How many msgs have TTLs?
// Used to mock write failures.
mockWriteErr bool
}
// Write through caching layer that is also used on loading messages.
type cache struct {
buf []byte
off int
wp int
idx []uint32
lrl uint32
fseq uint64
nra bool
}
type msgId struct {
seq uint64
ts int64
}
const (
// Magic is used to identify the file store files.
magic = uint8(22)
// Version
version = uint8(1)
// New IndexInfo Version
newVersion = uint8(2)
// hdrLen
hdrLen = 2
// This is where we keep the streams.
streamsDir = "streams"
// This is where we keep the message store blocks.
msgDir = "msgs"
// This is where we temporarily move the messages dir.
purgeDir = "__msgs__"
// used to scan blk file names.
blkScan = "%d.blk"
// suffix of a block file
blkSuffix = ".blk"
// used for compacted blocks that are staged.
newScan = "%d.new"
// used to scan index file names.
indexScan = "%d.idx"
// used to store our block encryption key.
keyScan = "%d.key"
// to look for orphans
keyScanAll = "*.key"
// This is where we keep state on consumers.
consumerDir = "obs"
// Index file for a consumer.
consumerState = "o.dat"
// The suffix that will be given to a new temporary block for compression or when rewriting the full file.
blkTmpSuffix = ".tmp"
// This is where we keep state on templates.
tmplsDir = "templates"
// Maximum size of a write buffer we may consider for re-use.
maxBufReuse = 2 * 1024 * 1024
// default cache buffer expiration
defaultCacheBufferExpiration = 10 * time.Second
// default sync interval
defaultSyncInterval = 2 * time.Minute
// default idle timeout to close FDs.
closeFDsIdle = 30 * time.Second
// default expiration time for mb.fss when idle.
defaultFssExpiration = 2 * time.Minute
// coalesceMinimum
coalesceMinimum = 16 * 1024
// maxFlushWait is maximum we will wait to gather messages to flush.
maxFlushWait = 8 * time.Millisecond
// Metafiles for streams and consumers.
JetStreamMetaFile = "meta.inf"
JetStreamMetaFileSum = "meta.sum"
JetStreamMetaFileKey = "meta.key"
// This is the full snapshotted state for the stream.
streamStreamStateFile = "index.db"
// This is the encoded time hash wheel for TTLs.
ttlStreamStateFile = "thw.db"
// AEK key sizes
minMetaKeySize = 64
minBlkKeySize = 64
// Default stream block size.
defaultLargeBlockSize = 8 * 1024 * 1024 // 8MB
// Default for workqueue or interest based.
defaultMediumBlockSize = 4 * 1024 * 1024 // 4MB
// For smaller reuse buffers. Usually being generated during contention on the lead write buffer.
// E.g. mirrors/sources etc.
defaultSmallBlockSize = 1 * 1024 * 1024 // 1MB
// Maximum size for the encrypted head block.
maximumEncryptedBlockSize = 2 * 1024 * 1024 // 2MB
// Default for KV based
defaultKVBlockSize = defaultMediumBlockSize
// max block size for now.
maxBlockSize = defaultLargeBlockSize
// Compact minimum threshold.
compactMinimum = 2 * 1024 * 1024 // 2MB
// FileStoreMinBlkSize is minimum size we will do for a blk size.
FileStoreMinBlkSize = 32 * 1000 // 32kib
// FileStoreMaxBlkSize is maximum size we will do for a blk size.
FileStoreMaxBlkSize = maxBlockSize
// Check for bad record length value due to corrupt data.
rlBadThresh = 32 * 1024 * 1024
// Checksum size for hash for msg records.
recordHashSize = 8
)
func newFileStore(fcfg FileStoreConfig, cfg StreamConfig) (*fileStore, error) {
return newFileStoreWithCreated(fcfg, cfg, time.Now().UTC(), nil, nil)
}
func newFileStoreWithCreated(fcfg FileStoreConfig, cfg StreamConfig, created time.Time, prf, oldprf keyGen) (fs *fileStore, err error) {
if cfg.Name == _EMPTY_ {
return nil, fmt.Errorf("name required")
}
if cfg.Storage != FileStorage {
return nil, fmt.Errorf("fileStore requires file storage type in config")
}
// Default values.
if fcfg.BlockSize == 0 {
fcfg.BlockSize = dynBlkSize(cfg.Retention, cfg.MaxBytes, prf != nil)
}
if fcfg.BlockSize > maxBlockSize {
return nil, fmt.Errorf("filestore max block size is %s", friendlyBytes(maxBlockSize))
}
if fcfg.CacheExpire == 0 {
fcfg.CacheExpire = defaultCacheBufferExpiration
}
if fcfg.SubjectStateExpire == 0 {
fcfg.SubjectStateExpire = defaultFssExpiration
}
if fcfg.SyncInterval == 0 {
fcfg.SyncInterval = defaultSyncInterval
}
// Check the directory
if stat, err := os.Stat(fcfg.StoreDir); os.IsNotExist(err) {
if err := os.MkdirAll(fcfg.StoreDir, defaultDirPerms); err != nil {
return nil, fmt.Errorf("could not create storage directory - %v", err)
}
} else if stat == nil || !stat.IsDir() {
return nil, fmt.Errorf("storage directory is not a directory")
}
tmpfile, err := os.CreateTemp(fcfg.StoreDir, "_test_")
if err != nil {
return nil, fmt.Errorf("storage directory is not writable")
}
tmpfile.Close()
<-dios
os.Remove(tmpfile.Name())
dios <- struct{}{}
fs = &fileStore{
fcfg: fcfg,
psim: stree.NewSubjectTree[psi](),
bim: make(map[uint32]*msgBlock),
cfg: FileStreamInfo{Created: created, StreamConfig: cfg},
prf: prf,
oldprf: oldprf,
qch: make(chan struct{}),
fsld: make(chan struct{}),
srv: fcfg.srv,
}
// Register with access time service.
ats.Register()
// If we error before completion make sure to cleanup.
defer func() {
if err != nil {
ats.Unregister()
}
}()
// Only create a THW if we're going to allow TTLs.
if cfg.AllowMsgTTL {
fs.ttls = thw.NewHashWheel()
}
// Set flush in place to AsyncFlush which by default is false.
fs.fip = !fcfg.AsyncFlush
// Check if this is a new setup.
mdir := filepath.Join(fcfg.StoreDir, msgDir)
odir := filepath.Join(fcfg.StoreDir, consumerDir)
if err := os.MkdirAll(mdir, defaultDirPerms); err != nil {
return nil, fmt.Errorf("could not create message storage directory - %v", err)
}
if err := os.MkdirAll(odir, defaultDirPerms); err != nil {
return nil, fmt.Errorf("could not create consumer storage directory - %v", err)
}
// Create highway hash for message blocks. Use sha256 of directory as key.
key := sha256.Sum256([]byte(cfg.Name))
fs.hh, err = highwayhash.NewDigest64(key[:])
if err != nil {
return nil, fmt.Errorf("could not create hash: %v", err)
}
keyFile := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey)
_, err = os.Stat(keyFile)
// Either the file should exist (err=nil), or it shouldn't. Any other error is reported.
if err != nil && !os.IsNotExist(err) {
return nil, err
}
// Make sure we do not have an encrypted store underneath of us but no main key.
if fs.prf == nil && err == nil {
return nil, errNoMainKey
} else if fs.prf != nil && err == nil {
// If encryption is configured and the key file exists, recover our keys.
if err = fs.recoverAEK(); err != nil {
return nil, err
}
}
// Attempt to recover our state.
err = fs.recoverFullState()
if err != nil {
if !os.IsNotExist(err) {
fs.warn("Recovering stream state from index errored: %v", err)
}
// Hold onto state
prior := fs.state
// Reset anything that could have been set from above.
fs.state = StreamState{}
fs.psim, fs.tsl = fs.psim.Empty(), 0
fs.bim = make(map[uint32]*msgBlock)
fs.blks = nil
fs.tombs = nil
// Recover our message state the old way
if err := fs.recoverMsgs(); err != nil {
return nil, err
}
// Check if our prior state remembers a last sequence past where we can see.
if prior.LastSeq > fs.state.LastSeq {
fs.state.LastSeq, fs.state.LastTime = prior.LastSeq, prior.LastTime
if fs.state.Msgs == 0 {
fs.state.FirstSeq = fs.state.LastSeq + 1
fs.state.FirstTime = time.Time{}
}
if fs.ld != nil {
if _, err := fs.newMsgBlockForWrite(); err == nil {
if err = fs.writeTombstone(prior.LastSeq, prior.LastTime.UnixNano()); err != nil {
return nil, err
}
} else {
return nil, err
}
}
}
// Since we recovered here, make sure to kick ourselves to write out our stream state.
fs.dirty++
}
// See if we can bring back our TTL timed hash wheel state from disk.
if cfg.AllowMsgTTL {
if err = fs.recoverTTLState(); err != nil && !os.IsNotExist(err) {
fs.warn("Recovering TTL state from index errored: %v", err)
}
}
// Also make sure we get rid of old idx and fss files on return.
// Do this in separate go routine vs inline and at end of processing.
defer func() {
if fs != nil {
go fs.cleanupOldMeta()
}
}()
// Lock while we do enforcements and removals.
fs.mu.Lock()
// Check if we have any left over tombstones to process.
if len(fs.tombs) > 0 {
for _, seq := range fs.tombs {
fs.removeMsg(seq, false, true, false)
fs.removeFromLostData(seq)
}
// Not needed after this phase.
fs.tombs = nil
}
// Limits checks and enforcement.
fs.enforceMsgLimit()
fs.enforceBytesLimit()
// Do age checks too, make sure to call in place.
if fs.cfg.MaxAge != 0 {
err := fs.expireMsgsOnRecover()
if isPermissionError(err) {
return nil, err
}
fs.startAgeChk()
}
// If we have max msgs per subject make sure the is also enforced.
if fs.cfg.MaxMsgsPer > 0 {
fs.enforceMsgPerSubjectLimit(false)
}
// Grab first sequence for check below while we have lock.
firstSeq := fs.state.FirstSeq
fs.mu.Unlock()
// If the stream has an initial sequence number then make sure we
// have purged up until that point. We will do this only if the
// recovered first sequence number is before our configured first
// sequence. Need to do this locked as by now the age check timer
// has started.
if cfg.FirstSeq > 0 && firstSeq < cfg.FirstSeq {
if _, err := fs.purge(cfg.FirstSeq); err != nil {
return nil, err
}
}
// Write our meta data if it does not exist or is zero'd out.
meta := filepath.Join(fcfg.StoreDir, JetStreamMetaFile)
fi, err := os.Stat(meta)
if err != nil && os.IsNotExist(err) || fi != nil && fi.Size() == 0 {
if err := fs.writeStreamMeta(); err != nil {
return nil, err
}
}
// If we expect to be encrypted check that what we are restoring is not plaintext.
// This can happen on snapshot restores or conversions.
if fs.prf != nil {
if _, err := os.Stat(keyFile); err != nil && os.IsNotExist(err) {
if err := fs.writeStreamMeta(); err != nil {
return nil, err
}
}
}
// Setup our sync timer.
fs.setSyncTimer()
// Spin up the go routine that will write out our full state stream index.
go fs.flushStreamStateLoop(fs.qch, fs.fsld)
return fs, nil
}
// Lock all existing message blocks.
// Lock held on entry.
func (fs *fileStore) lockAllMsgBlocks() {
for _, mb := range fs.blks {
mb.mu.Lock()
}
}
// Unlock all existing message blocks.
// Lock held on entry.
func (fs *fileStore) unlockAllMsgBlocks() {
for _, mb := range fs.blks {
mb.mu.Unlock()
}
}
func (fs *fileStore) UpdateConfig(cfg *StreamConfig) error {
start := time.Now()
defer func() {
if took := time.Since(start); took > time.Minute {
fs.warn("UpdateConfig took %v", took.Round(time.Millisecond))
}
}()
if fs.isClosed() {
return ErrStoreClosed
}
if cfg.Name == _EMPTY_ {
return fmt.Errorf("name required")
}
if cfg.Storage != FileStorage {
return fmt.Errorf("fileStore requires file storage type in config")
}
if cfg.MaxMsgsPer < -1 {
cfg.MaxMsgsPer = -1
}
fs.mu.Lock()
new_cfg := FileStreamInfo{Created: fs.cfg.Created, StreamConfig: *cfg}
old_cfg := fs.cfg
// The reference story has changed here, so this full msg block lock
// may not be needed.
fs.lockAllMsgBlocks()
fs.cfg = new_cfg
fs.unlockAllMsgBlocks()
if err := fs.writeStreamMeta(); err != nil {
fs.lockAllMsgBlocks()
fs.cfg = old_cfg
fs.unlockAllMsgBlocks()
fs.mu.Unlock()
return err
}
// Create or delete the THW if needed.
if cfg.AllowMsgTTL && fs.ttls == nil {
fs.recoverTTLState()
} else if !cfg.AllowMsgTTL && fs.ttls != nil {
fs.ttls = nil
}
// Limits checks and enforcement.
fs.enforceMsgLimit()
fs.enforceBytesLimit()
// Do age timers.
if fs.ageChk == nil && fs.cfg.MaxAge != 0 {
fs.startAgeChk()
}
if fs.ageChk != nil && fs.cfg.MaxAge == 0 {
fs.ageChk.Stop()
fs.ageChk = nil
fs.ageChkTime = 0
}
if fs.cfg.MaxMsgsPer > 0 && (old_cfg.MaxMsgsPer == 0 || fs.cfg.MaxMsgsPer < old_cfg.MaxMsgsPer) {
fs.enforceMsgPerSubjectLimit(true)
}
fs.mu.Unlock()
if cfg.MaxAge != 0 || cfg.AllowMsgTTL {
fs.expireMsgs()
}
return nil
}
func dynBlkSize(retention RetentionPolicy, maxBytes int64, encrypted bool) uint64 {
if maxBytes > 0 {
blkSize := (maxBytes / 4) + 1 // (25% overhead)
// Round up to nearest 100
if m := blkSize % 100; m != 0 {
blkSize += 100 - m
}
if blkSize <= FileStoreMinBlkSize {
blkSize = FileStoreMinBlkSize
} else if blkSize >= FileStoreMaxBlkSize {
blkSize = FileStoreMaxBlkSize
} else {
blkSize = defaultMediumBlockSize
}
if encrypted && blkSize > maximumEncryptedBlockSize {
// Notes on this below.
blkSize = maximumEncryptedBlockSize
}
return uint64(blkSize)
}
switch {
case encrypted:
// In the case of encrypted stores, large blocks can result in worsened perf
// since many writes on disk involve re-encrypting the entire block. For now,
// we will enforce a cap on the block size when encryption is enabled to avoid
// this.
return maximumEncryptedBlockSize
case retention == LimitsPolicy:
// TODO(dlc) - Make the blocksize relative to this if set.
return defaultLargeBlockSize
default:
// TODO(dlc) - Make the blocksize relative to this if set.
return defaultMediumBlockSize
}
}
func genEncryptionKey(sc StoreCipher, seed []byte) (ek cipher.AEAD, err error) {
if sc == ChaCha {
ek, err = chacha20poly1305.NewX(seed)
} else if sc == AES {
block, e := aes.NewCipher(seed)
if e != nil {
return nil, e
}
ek, err = cipher.NewGCMWithNonceSize(block, block.BlockSize())
} else {
err = errUnknownCipher
}
return ek, err
}
// Generate an asset encryption key from the context and server PRF.
func (fs *fileStore) genEncryptionKeys(context string) (aek cipher.AEAD, bek cipher.Stream, seed, encrypted []byte, err error) {
if fs.prf == nil {
return nil, nil, nil, nil, errNoEncryption
}
// Generate key encryption key.
rb, err := fs.prf([]byte(context))
if err != nil {
return nil, nil, nil, nil, err
}
sc := fs.fcfg.Cipher
kek, err := genEncryptionKey(sc, rb)
if err != nil {
return nil, nil, nil, nil, err
}
// Generate random asset encryption key seed.
const seedSize = 32
seed = make([]byte, seedSize)
if n, err := rand.Read(seed); err != nil {
return nil, nil, nil, nil, err
} else if n != seedSize {
return nil, nil, nil, nil, fmt.Errorf("not enough seed bytes read (%d != %d", n, seedSize)
}
aek, err = genEncryptionKey(sc, seed)
if err != nil {
return nil, nil, nil, nil, err
}
// Generate our nonce. Use same buffer to hold encrypted seed.
nonce := make([]byte, kek.NonceSize(), kek.NonceSize()+len(seed)+kek.Overhead())
if n, err := rand.Read(nonce); err != nil {
return nil, nil, nil, nil, err
} else if n != len(nonce) {
return nil, nil, nil, nil, fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce))
}
bek, err = genBlockEncryptionKey(sc, seed[:], nonce)
if err != nil {
return nil, nil, nil, nil, err
}
return aek, bek, seed, kek.Seal(nonce, nonce, seed, nil), nil
}
// Will generate the block encryption key.
func genBlockEncryptionKey(sc StoreCipher, seed, nonce []byte) (cipher.Stream, error) {
if sc == ChaCha {
return chacha20.NewUnauthenticatedCipher(seed, nonce)
} else if sc == AES {
block, err := aes.NewCipher(seed)
if err != nil {
return nil, err
}
return cipher.NewCTR(block, nonce), nil
}
return nil, errUnknownCipher
}
// Lock should be held.
func (fs *fileStore) recoverAEK() error {
if fs.prf != nil && fs.aek == nil {
ekey, err := os.ReadFile(filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey))
if err != nil {
return err
}
rb, err := fs.prf([]byte(fs.cfg.Name))
if err != nil {
return err
}
kek, err := genEncryptionKey(fs.fcfg.Cipher, rb)
if err != nil {
return err
}
ns := kek.NonceSize()
seed, err := kek.Open(nil, ekey[:ns], ekey[ns:], nil)
if err != nil {
return err
}
aek, err := genEncryptionKey(fs.fcfg.Cipher, seed)
if err != nil {
return err
}
fs.aek = aek
}
return nil
}
// Lock should be held.
func (fs *fileStore) setupAEK() error {
if fs.prf != nil && fs.aek == nil {
key, _, _, encrypted, err := fs.genEncryptionKeys(fs.cfg.Name)
if err != nil {
return err
}
keyFile := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileKey)
if _, err := os.Stat(keyFile); err != nil && !os.IsNotExist(err) {
return err
}
err = fs.writeFileWithOptionalSync(keyFile, encrypted, defaultFilePerms)
if err != nil {
return err
}
// Set our aek.
fs.aek = key
}
return nil
}
// Write out meta and the checksum.
// Lock should be held.
func (fs *fileStore) writeStreamMeta() error {
if err := fs.setupAEK(); err != nil {
return err
}
meta := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFile)
if _, err := os.Stat(meta); err != nil && !os.IsNotExist(err) {
return err
}
b, err := json.Marshal(fs.cfg)
if err != nil {
return err
}
// Encrypt if needed.
if fs.aek != nil {
nonce := make([]byte, fs.aek.NonceSize(), fs.aek.NonceSize()+len(b)+fs.aek.Overhead())
if n, err := rand.Read(nonce); err != nil {
return err
} else if n != len(nonce) {
return fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce))
}
b = fs.aek.Seal(nonce, nonce, b, nil)
}
err = fs.writeFileWithOptionalSync(meta, b, defaultFilePerms)
if err != nil {
return err
}
fs.hh.Reset()
fs.hh.Write(b)
var hb [highwayhash.Size64]byte
checksum := hex.EncodeToString(fs.hh.Sum(hb[:0]))
sum := filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFileSum)
err = fs.writeFileWithOptionalSync(sum, []byte(checksum), defaultFilePerms)
if err != nil {
return err
}
return nil
}
// Pools to recycle the blocks to help with memory pressure.
var blkPoolBig sync.Pool // 16MB
var blkPoolMedium sync.Pool // 8MB
var blkPoolSmall sync.Pool // 2MB
// Get a new msg block based on sz estimate.
func getMsgBlockBuf(sz int) (buf []byte) {
var pb any
if sz <= defaultSmallBlockSize {
pb = blkPoolSmall.Get()
} else if sz <= defaultMediumBlockSize {
pb = blkPoolMedium.Get()
} else {
pb = blkPoolBig.Get()
}
if pb != nil {
buf = *(pb.(*[]byte))
} else {
// Here we need to make a new blk.
// If small leave as is..
if sz > defaultSmallBlockSize && sz <= defaultMediumBlockSize {
sz = defaultMediumBlockSize
} else if sz > defaultMediumBlockSize {
sz = defaultLargeBlockSize
}
buf = make([]byte, sz)
}
return buf[:0]
}
// Recycle the msg block.
func recycleMsgBlockBuf(buf []byte) {
if buf == nil || cap(buf) < defaultSmallBlockSize {
return
}
// Make sure to reset before placing back into pool.
buf = buf[:0]
// We need to make sure the load code gets a block that can fit the maximum for a size block.
// E.g. 8, 16 etc. otherwise we thrash and actually make things worse by pulling it out, and putting
// it right back in and making a new []byte.
// From above we know its already >= defaultSmallBlockSize
if sz := cap(buf); sz < defaultMediumBlockSize {
blkPoolSmall.Put(&buf)
} else if sz < defaultLargeBlockSize {
blkPoolMedium.Put(&buf)
} else {
blkPoolBig.Put(&buf)
}
}
const (
msgHdrSize = 22
checksumSize = 8
emptyRecordLen = msgHdrSize + checksumSize
)
// Lock should be held.
func (fs *fileStore) noTrackSubjects() bool {
return !(fs.psim.Size() > 0 || len(fs.cfg.Subjects) > 0 || fs.cfg.Mirror != nil || len(fs.cfg.Sources) > 0)
}
// Will init the basics for a message block.
func (fs *fileStore) initMsgBlock(index uint32) *msgBlock {
mb := &msgBlock{
fs: fs,
index: index,
cexp: fs.fcfg.CacheExpire,
fexp: fs.fcfg.SubjectStateExpire,
noTrack: fs.noTrackSubjects(),
syncAlways: fs.fcfg.SyncAlways,
}
mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
mb.mfn = filepath.Join(mdir, fmt.Sprintf(blkScan, index))
if mb.hh == nil {
key := sha256.Sum256(fs.hashKeyForBlock(index))
mb.hh, _ = highwayhash.NewDigest64(key[:])
}
return mb
}
// Check for encryption, we do not load keys on startup anymore so might need to load them here.
// Lock for fs should be held.
func (mb *msgBlock) checkAndLoadEncryption() error {
if mb.fs != nil && mb.fs.prf != nil && (mb.aek == nil || mb.bek == nil) {
if err := mb.fs.loadEncryptionForMsgBlock(mb); err != nil {
return err
}
}
return nil
}
// Lock for fs should be held.
func (fs *fileStore) loadEncryptionForMsgBlock(mb *msgBlock) error {
if fs.prf == nil {
return nil
}
var createdKeys bool
mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
ekey, err := os.ReadFile(filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index)))
if err != nil {
// We do not seem to have keys even though we should. Could be a plaintext conversion.
// Create the keys and we will double check below.
if err := fs.genEncryptionKeysForBlock(mb); err != nil {
return err
}
createdKeys = true
} else {
if len(ekey) < minBlkKeySize {
return errBadKeySize
}
// Recover key encryption key.
rb, err := fs.prf([]byte(fmt.Sprintf("%s:%d", fs.cfg.Name, mb.index)))
if err != nil {
return err
}
sc := fs.fcfg.Cipher
kek, err := genEncryptionKey(sc, rb)
if err != nil {
return err
}
ns := kek.NonceSize()
seed, err := kek.Open(nil, ekey[:ns], ekey[ns:], nil)
if err != nil {
// We may be here on a cipher conversion, so attempt to convert.
if err = mb.convertCipher(); err != nil {
return err
}
} else {
mb.seed, mb.nonce = seed, ekey[:ns]
}
mb.aek, err = genEncryptionKey(sc, mb.seed)
if err != nil {
return err
}
if mb.bek, err = genBlockEncryptionKey(sc, mb.seed, mb.nonce); err != nil {
return err
}
}
// If we created keys here, let's check the data and if it is plaintext convert here.
if createdKeys {
if err := mb.convertToEncrypted(); err != nil {
return err
}
}
return nil
}
// Load a last checksum if needed from the block file.
// Lock should be held.
func (mb *msgBlock) ensureLastChecksumLoaded() {
var empty [8]byte
if mb.lchk != empty {
return
}
copy(mb.lchk[0:], mb.lastChecksum())
}
// Lock held on entry
func (fs *fileStore) recoverMsgBlock(index uint32) (*msgBlock, error) {
mb := fs.initMsgBlock(index)
// Open up the message file, but we will try to recover from the index file.
// We will check that the last checksums match.
file, err := mb.openBlock()
if err != nil {
return nil, err
}
defer file.Close()
if fi, err := file.Stat(); fi != nil {
mb.rbytes = uint64(fi.Size())
} else {
return nil, err
}
// Make sure encryption loaded if needed.
fs.loadEncryptionForMsgBlock(mb)
// Grab last checksum from main block file.
var lchk [8]byte
if mb.rbytes >= checksumSize {
if mb.bek != nil {
if buf, _ := mb.loadBlock(nil); len(buf) >= checksumSize {
mb.bek.XORKeyStream(buf, buf)
copy(lchk[0:], buf[len(buf)-checksumSize:])
}
} else {
file.ReadAt(lchk[:], int64(mb.rbytes)-checksumSize)
}
}
file.Close()
// Read our index file. Use this as source of truth if possible.
// This not applicable in >= 2.10 servers. Here for upgrade paths from < 2.10.
if err := mb.readIndexInfo(); err == nil {
// Quick sanity check here.
// Note this only checks that the message blk file is not newer then this file, or is empty and we expect empty.
if (mb.rbytes == 0 && mb.msgs == 0) || bytes.Equal(lchk[:], mb.lchk[:]) {
if mb.msgs > 0 && !mb.noTrack && fs.psim != nil {
fs.populateGlobalPerSubjectInfo(mb)
// Try to dump any state we needed on recovery.
mb.tryForceExpireCacheLocked()
}
fs.addMsgBlock(mb)
return mb, nil
}
}
// If we get data loss rebuilding the message block state record that with the fs itself.
ld, tombs, _ := mb.rebuildState()
if ld != nil {
fs.addLostData(ld)
}
// Collect all tombstones.
if len(tombs) > 0 {
fs.tombs = append(fs.tombs, tombs...)
}
if mb.msgs > 0 && !mb.noTrack && fs.psim != nil {
fs.populateGlobalPerSubjectInfo(mb)
// Try to dump any state we needed on recovery.
mb.tryForceExpireCacheLocked()
}
mb.closeFDs()
fs.addMsgBlock(mb)
return mb, nil
}
func (fs *fileStore) lostData() *LostStreamData {
fs.mu.RLock()
defer fs.mu.RUnlock()
if fs.ld == nil {
return nil
}
nld := *fs.ld
return &nld
}
// Lock should be held.
func (fs *fileStore) addLostData(ld *LostStreamData) {
if ld == nil {
return
}
if fs.ld != nil {
var added bool
for _, seq := range ld.Msgs {
if _, found := fs.ld.exists(seq); !found {
fs.ld.Msgs = append(fs.ld.Msgs, seq)
added = true
}
}
if added {
msgs := fs.ld.Msgs
slices.Sort(msgs)
fs.ld.Bytes += ld.Bytes
}
} else {
fs.ld = ld
}
}
// Helper to see if we already have this sequence reported in our lost data.
func (ld *LostStreamData) exists(seq uint64) (int, bool) {
i := slices.IndexFunc(ld.Msgs, func(i uint64) bool {
return i == seq
})
return i, i > -1
}
func (fs *fileStore) removeFromLostData(seq uint64) {
if fs.ld == nil {
return
}
if i, found := fs.ld.exists(seq); found {
fs.ld.Msgs = append(fs.ld.Msgs[:i], fs.ld.Msgs[i+1:]...)
if len(fs.ld.Msgs) == 0 {
fs.ld = nil
}
}
}
func (fs *fileStore) rebuildState(ld *LostStreamData) {
fs.mu.Lock()
defer fs.mu.Unlock()
fs.rebuildStateLocked(ld)
}
// Lock should be held.
func (fs *fileStore) rebuildStateLocked(ld *LostStreamData) {
fs.addLostData(ld)
fs.state.Msgs, fs.state.Bytes = 0, 0
fs.state.FirstSeq, fs.state.LastSeq = 0, 0
for _, mb := range fs.blks {
mb.mu.RLock()
fs.state.Msgs += mb.msgs
fs.state.Bytes += mb.bytes
fseq := atomic.LoadUint64(&mb.first.seq)
if fs.state.FirstSeq == 0 || (fseq < fs.state.FirstSeq && mb.first.ts != 0) {
fs.state.FirstSeq = fseq
if mb.first.ts == 0 {
fs.state.FirstTime = time.Time{}
} else {
fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC()
}
}
// Preserve last time, could have erased the last message in one block, and then
// have a tombstone with the proper timestamp afterward in another block
if lseq := atomic.LoadUint64(&mb.last.seq); lseq >= fs.state.LastSeq {
fs.state.LastSeq = lseq
if mb.last.ts == 0 {
fs.state.LastTime = time.Time{}
} else {
fs.state.LastTime = time.Unix(0, mb.last.ts).UTC()
}
}
mb.mu.RUnlock()
}
}
// Attempt to convert the cipher used for this message block.
func (mb *msgBlock) convertCipher() error {
fs := mb.fs
sc := fs.fcfg.Cipher
var osc StoreCipher
switch sc {
case ChaCha:
osc = AES
case AES:
osc = ChaCha
}
mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
ekey, err := os.ReadFile(filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index)))
if err != nil {
return err
}
if len(ekey) < minBlkKeySize {
return errBadKeySize
}
type prfWithCipher struct {
keyGen
StoreCipher
}
var prfs []prfWithCipher
if fs.prf != nil {
prfs = append(prfs, prfWithCipher{fs.prf, sc})
prfs = append(prfs, prfWithCipher{fs.prf, osc})
}
if fs.oldprf != nil {
prfs = append(prfs, prfWithCipher{fs.oldprf, sc})
prfs = append(prfs, prfWithCipher{fs.oldprf, osc})
}
for _, prf := range prfs {
// Recover key encryption key.
rb, err := prf.keyGen([]byte(fmt.Sprintf("%s:%d", fs.cfg.Name, mb.index)))
if err != nil {
continue
}
kek, err := genEncryptionKey(prf.StoreCipher, rb)
if err != nil {
continue
}
ns := kek.NonceSize()
seed, err := kek.Open(nil, ekey[:ns], ekey[ns:], nil)
if err != nil {
continue
}
nonce := ekey[:ns]
bek, err := genBlockEncryptionKey(prf.StoreCipher, seed, nonce)
if err != nil {
return err
}
buf, _ := mb.loadBlock(nil)
bek.XORKeyStream(buf, buf)
// Check for compression, and make sure we can parse with old cipher and key file.
if nbuf, err := mb.decompressIfNeeded(buf); err != nil {
return err
} else if _, _, err = mb.rebuildStateFromBufLocked(nbuf, false); err != nil {
return err
} else if err = mb.indexCacheBuf(nbuf); err != nil {
return err
}
// Reset the cache since we just read everything in.
mb.cache = nil
// Generate new keys. If we error for some reason then we will put
// the old keyfile back.
if err := fs.genEncryptionKeysForBlock(mb); err != nil {
keyFile := filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index))
fs.writeFileWithOptionalSync(keyFile, ekey, defaultFilePerms)
return err
}
mb.bek.XORKeyStream(buf, buf)
<-dios
err = os.WriteFile(mb.mfn, buf, defaultFilePerms)
dios <- struct{}{}
if err != nil {
return err
}
return nil
}
return fmt.Errorf("unable to recover keys")
}
// Convert a plaintext block to encrypted.
func (mb *msgBlock) convertToEncrypted() error {
if mb.bek == nil {
return nil
}
buf, err := mb.loadBlock(nil)
if err != nil {
return err
}
// Check for compression.
if buf, err = mb.decompressIfNeeded(buf); err != nil {
return err
} else if _, _, err = mb.rebuildStateFromBufLocked(buf, false); err != nil {
return err
} else if err = mb.indexCacheBuf(buf); err != nil {
// This likely indicates this was already encrypted or corrupt.
mb.cache = nil
return err
}
// Undo cache from above for later.
mb.cache = nil
mb.bek.XORKeyStream(buf, buf)
<-dios
err = os.WriteFile(mb.mfn, buf, defaultFilePerms)
dios <- struct{}{}
if err != nil {
return err
}
return nil
}
// Return the mb's index.
func (mb *msgBlock) getIndex() uint32 {
mb.mu.RLock()
defer mb.mu.RUnlock()
return mb.index
}
// Rebuild the state of the blk based on what we have on disk in the N.blk file.
// We will return any lost data, and we will return any delete tombstones we encountered.
func (mb *msgBlock) rebuildState() (*LostStreamData, []uint64, error) {
mb.mu.Lock()
defer mb.mu.Unlock()
return mb.rebuildStateLocked()
}
// Rebuild the state of the blk based on what we have on disk in the N.blk file.
// Lock should be held.
func (mb *msgBlock) rebuildStateLocked() (*LostStreamData, []uint64, error) {
// Remove the .fss file and clear any cache we have set.
mb.clearCacheAndOffset()
buf, err := mb.loadBlock(nil)
defer recycleMsgBlockBuf(buf)
if err != nil || len(buf) == 0 {
var ld *LostStreamData
// No data to rebuild from here.
if mb.msgs > 0 {
// We need to declare lost data here.
ld = &LostStreamData{Msgs: make([]uint64, 0, mb.msgs), Bytes: mb.bytes}
firstSeq, lastSeq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
for seq := firstSeq; seq <= lastSeq; seq++ {
if !mb.dmap.Exists(seq) {
ld.Msgs = append(ld.Msgs, seq)
}
}
// Clear invalid state. We will let this blk be added in here.
mb.msgs, mb.bytes, mb.rbytes, mb.fss = 0, 0, 0, nil
mb.dmap.Empty()
atomic.StoreUint64(&mb.first.seq, atomic.LoadUint64(&mb.last.seq)+1)
}
return ld, nil, err
}
// Check if we need to decrypt.
if err = mb.encryptOrDecryptIfNeeded(buf); err != nil {
return nil, nil, err
}
// Check for compression.
if buf, err = mb.decompressIfNeeded(buf); err != nil {
return nil, nil, err
}
return mb.rebuildStateFromBufLocked(buf, true)
}
// Lock should be held.
func (mb *msgBlock) rebuildStateFromBufLocked(buf []byte, allowTruncate bool) (*LostStreamData, []uint64, error) {
var err error
startLastSeq := atomic.LoadUint64(&mb.last.seq)
// Clear state we need to rebuild.
mb.msgs, mb.bytes, mb.rbytes, mb.fss = 0, 0, 0, nil
atomic.StoreUint64(&mb.last.seq, 0)
mb.last.ts = 0
firstNeedsSet := true
mb.rbytes = uint64(len(buf))
addToDmap := func(seq uint64) {
if seq == 0 {
return
}
mb.dmap.Insert(seq)
}
var le = binary.LittleEndian
truncate := func(index uint32) {
// There are cases where we're not allowed to truncate, like for an encrypted or compressed
// block since the index will be the decrypted and decompressed index.
if !allowTruncate {
return
}
var fd *os.File
if mb.mfd != nil {
fd = mb.mfd
} else {
<-dios
fd, err = os.OpenFile(mb.mfn, os.O_RDWR, defaultFilePerms)
dios <- struct{}{}
if err == nil {
defer fd.Close()
}
}
if fd == nil {
return
}
if err := fd.Truncate(int64(index)); err == nil {
// Update our checksum.
if index >= 8 {
var lchk [8]byte
fd.ReadAt(lchk[:], int64(index-8))
copy(mb.lchk[0:], lchk[:])
}
fd.Sync()
}
}
gatherLost := func(lb uint32) *LostStreamData {
var ld LostStreamData
for seq := atomic.LoadUint64(&mb.last.seq) + 1; seq <= startLastSeq; seq++ {
ld.Msgs = append(ld.Msgs, seq)
}
ld.Bytes = uint64(lb)
return &ld
}
// For tombstones that we find and collect.
var (
tombstones []uint64
maxTombstoneSeq uint64
maxTombstoneTs int64
)
// To detect gaps from compaction, and to ensure the sequence keeps moving up.
var last uint64
var hb [highwayhash.Size64]byte
updateLast := func(seq uint64, ts int64) {
// The sequence needs to only ever move up.
if seq <= last {
return
}
// Check for any gaps from compaction, meaning no ebit entry.
if last > 0 && seq != last+1 && mb.msgs != 0 {
for dseq := last + 1; dseq < seq; dseq++ {
addToDmap(dseq)
}
}
last = seq
atomic.StoreUint64(&mb.last.seq, last)
mb.last.ts = ts
}
for index, lbuf := uint32(0), uint32(len(buf)); index < lbuf; {
if index+msgHdrSize > lbuf {
truncate(index)
return gatherLost(lbuf - index), tombstones, nil
}
hdr := buf[index : index+msgHdrSize]
rl, slen := le.Uint32(hdr[0:]), int(le.Uint16(hdr[20:]))
hasHeaders := rl&hbit != 0
var ttl int64
if mb.fs.ttls != nil && len(hdr) > 0 {
ttl, _ = getMessageTTL(hdr)
}
// Clear any headers bit that could be set.
rl &^= hbit
dlen := int(rl) - msgHdrSize
// Do some quick sanity checks here.
if dlen < 0 || slen > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh {
truncate(index)
return gatherLost(lbuf - index), tombstones, errBadMsg{mb.mfn, fmt.Sprintf("sanity check failed (dlen %d slen %d rl %d index %d lbuf %d)", dlen, slen, rl, index, lbuf)}
}
// Check for checksum failures before additional processing.
data := buf[index+msgHdrSize : index+rl]
if hh := mb.hh; hh != nil {
hh.Reset()
hh.Write(hdr[4:20])
hh.Write(data[:slen])
if hasHeaders {
hh.Write(data[slen+4 : dlen-recordHashSize])
} else {
hh.Write(data[slen : dlen-recordHashSize])
}
checksum := hh.Sum(hb[:0])
if !bytes.Equal(checksum, data[len(data)-recordHashSize:]) {
truncate(index)
return gatherLost(lbuf - index), tombstones, errBadMsg{mb.mfn, "invalid checksum"}
}
copy(mb.lchk[0:], checksum)
}
// Grab our sequence and timestamp.
seq := le.Uint64(hdr[4:])
ts := int64(le.Uint64(hdr[12:]))
// Check if this is a delete tombstone.
if seq&tbit != 0 {
seq = seq &^ tbit
// Need to process this here and make sure we have accounted for this properly.
tombstones = append(tombstones, seq)
if maxTombstoneSeq == 0 || seq > maxTombstoneSeq {
maxTombstoneSeq, maxTombstoneTs = seq, ts
}
index += rl
continue
}
fseq := atomic.LoadUint64(&mb.first.seq)
// This is an old erased message, or a new one that we can track.
if seq == 0 || seq&ebit != 0 || seq < fseq {
seq = seq &^ ebit
if seq >= fseq {
updateLast(seq, ts)
if mb.msgs == 0 {
atomic.StoreUint64(&mb.first.seq, seq+1)
mb.first.ts = 0
} else if seq != 0 {
// Only add to dmap if past recorded first seq and non-zero.
addToDmap(seq)
}
}
index += rl
continue
}
// This is for when we have index info that adjusts for deleted messages
// at the head. So the first.seq will be already set here. If this is larger
// replace what we have with this seq.
if firstNeedsSet && seq >= fseq {
atomic.StoreUint64(&mb.first.seq, seq)
firstNeedsSet, mb.first.ts = false, ts
}
// The sequence needs to only ever move up.
if seq <= last {
// Advance to next record.
// We've already accounted for this sequence and marked it as deleted.
index += rl
continue
}
if !mb.dmap.Exists(seq) {
mb.msgs++
mb.bytes += uint64(rl)
if mb.fs.ttls != nil && ttl > 0 {
expires := time.Duration(ts) + (time.Second * time.Duration(ttl))
mb.fs.ttls.Add(seq, int64(expires))
mb.ttls++
}
}
updateLast(seq, ts)
// Advance to next record.
index += rl
}
// For empty msg blocks make sure we recover last seq correctly based off of first.
// Or if we seem to have no messages but had a tombstone, which we use to remember
// sequences and timestamps now, use that to properly setup the first and last.
if mb.msgs == 0 {
fseq := atomic.LoadUint64(&mb.first.seq)
if fseq > 0 {
atomic.StoreUint64(&mb.last.seq, fseq-1)
} else if fseq == 0 && maxTombstoneSeq > 0 {
atomic.StoreUint64(&mb.first.seq, maxTombstoneSeq+1)
mb.first.ts = 0
if mb.last.seq == 0 {
atomic.StoreUint64(&mb.last.seq, maxTombstoneSeq)
mb.last.ts = maxTombstoneTs
}
}
}
return nil, tombstones, nil
}
// For doing warn logging.
// Lock should be held.
func (fs *fileStore) warn(format string, args ...any) {
// No-op if no server configured.
if fs.srv == nil {
return
}
fs.srv.Warnf(fmt.Sprintf("Filestore [%s] %s", fs.cfg.Name, format), args...)
}
// For doing debug logging.
// Lock should be held.
func (fs *fileStore) debug(format string, args ...any) {
// No-op if no server configured.
if fs.srv == nil {
return
}
fs.srv.Debugf(fmt.Sprintf("Filestore [%s] %s", fs.cfg.Name, format), args...)
}
// Track local state but ignore timestamps here.
func updateTrackingState(state *StreamState, mb *msgBlock) {
if state.FirstSeq == 0 {
state.FirstSeq = mb.first.seq
} else if mb.first.seq < state.FirstSeq && mb.first.ts != 0 {
state.FirstSeq = mb.first.seq
}
if mb.last.seq > state.LastSeq {
state.LastSeq = mb.last.seq
}
state.Msgs += mb.msgs
state.Bytes += mb.bytes
}
// Determine if our tracking states are the same.
func trackingStatesEqual(fs, mb *StreamState) bool {
// When a fs is brand new the fs state will have first seq of 0, but tracking mb may have 1.
// If either has a first sequence that is not 0 or 1 we will check if they are the same, otherwise skip.
if (fs.FirstSeq > 1 && mb.FirstSeq > 1) || mb.FirstSeq > 1 {
return fs.Msgs == mb.Msgs && fs.FirstSeq == mb.FirstSeq && fs.LastSeq == mb.LastSeq && fs.Bytes == mb.Bytes
}
return fs.Msgs == mb.Msgs && fs.LastSeq == mb.LastSeq && fs.Bytes == mb.Bytes
}
// recoverFullState will attempt to receover our last full state and re-process any state changes
// that happened afterwards.
func (fs *fileStore) recoverFullState() (rerr error) {
fs.mu.Lock()
defer fs.mu.Unlock()
// Check for any left over purged messages.
<-dios
pdir := filepath.Join(fs.fcfg.StoreDir, purgeDir)
if _, err := os.Stat(pdir); err == nil {
os.RemoveAll(pdir)
}
// Grab our stream state file and load it in.
fn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)
buf, err := os.ReadFile(fn)
dios <- struct{}{}
if err != nil {
if !os.IsNotExist(err) {
fs.warn("Could not read stream state file: %v", err)
}
return err
}
const minLen = 32
if len(buf) < minLen {
os.Remove(fn)
fs.warn("Stream state too short (%d bytes)", len(buf))
return errCorruptState
}
// The highwayhash will be on the end. Check that it still matches.
h := buf[len(buf)-highwayhash.Size64:]
buf = buf[:len(buf)-highwayhash.Size64]
fs.hh.Reset()
fs.hh.Write(buf)
var hb [highwayhash.Size64]byte
if !bytes.Equal(h, fs.hh.Sum(hb[:0])) {
os.Remove(fn)
fs.warn("Stream state checksum did not match")
return errCorruptState
}
// Decrypt if needed.
// We can be setup for encryption but if this is a snapshot restore we will be missing the keyfile
// since snapshots strip encryption.
if fs.prf != nil && fs.aek != nil {
ns := fs.aek.NonceSize()
buf, err = fs.aek.Open(nil, buf[:ns], buf[ns:], nil)
if err != nil {
fs.warn("Stream state error reading encryption key: %v", err)
return err
}
}
version := buf[1]
if buf[0] != fullStateMagic || version < fullStateMinVersion || version > fullStateVersion {
os.Remove(fn)
fs.warn("Stream state magic and version mismatch")
return errCorruptState
}
bi := hdrLen
readU64 := func() uint64 {
if bi < 0 {
return 0
}
v, n := binary.Uvarint(buf[bi:])
if n <= 0 {
bi = -1
return 0
}
bi += n
return v
}
readI64 := func() int64 {
if bi < 0 {
return 0
}
v, n := binary.Varint(buf[bi:])
if n <= 0 {
bi = -1
return -1
}
bi += n
return v
}
setTime := func(t *time.Time, ts int64) {
if ts == 0 {
*t = time.Time{}
} else {
*t = time.Unix(0, ts).UTC()
}
}
var state StreamState
state.Msgs = readU64()
state.Bytes = readU64()
state.FirstSeq = readU64()
baseTime := readI64()
setTime(&state.FirstTime, baseTime)
state.LastSeq = readU64()
setTime(&state.LastTime, readI64())
// Check for per subject info.
if numSubjects := int(readU64()); numSubjects > 0 {
fs.psim, fs.tsl = fs.psim.Empty(), 0
for i := 0; i < numSubjects; i++ {
if lsubj := int(readU64()); lsubj > 0 {
if bi+lsubj > len(buf) {
os.Remove(fn)
fs.warn("Stream state bad subject len (%d)", lsubj)
return errCorruptState
}
// If we have lots of subjects this will alloc for each one.
// We could reference the underlying buffer, but we could guess wrong if
// number of blocks is large and subjects is low, since we would reference buf.
subj := buf[bi : bi+lsubj]
// We had a bug that could cause memory corruption in the PSIM that could have gotten stored to disk.
// Only would affect subjects, so do quick check.
if !isValidSubject(bytesToString(subj), true) {
os.Remove(fn)
fs.warn("Stream state corrupt subject detected")
return errCorruptState
}
bi += lsubj
psi := psi{total: readU64(), fblk: uint32(readU64())}
if psi.total > 1 {
psi.lblk = uint32(readU64())
} else {
psi.lblk = psi.fblk
}
fs.psim.Insert(subj, psi)
fs.tsl += lsubj
}
}
}
// Track the state as represented by the blocks themselves.
var mstate StreamState
if numBlocks := readU64(); numBlocks > 0 {
lastIndex := int(numBlocks - 1)
fs.blks = make([]*msgBlock, 0, numBlocks)
for i := 0; i < int(numBlocks); i++ {
index, nbytes, fseq, fts, lseq, lts, numDeleted := uint32(readU64()), readU64(), readU64(), readI64(), readU64(), readI64(), readU64()
var ttls uint64
if version >= 2 {
ttls = readU64()
}
if bi < 0 {
os.Remove(fn)
return errCorruptState
}
mb := fs.initMsgBlock(index)
atomic.StoreUint64(&mb.first.seq, fseq)
atomic.StoreUint64(&mb.last.seq, lseq)
mb.msgs, mb.bytes = lseq-fseq+1, nbytes
mb.first.ts, mb.last.ts = fts+baseTime, lts+baseTime
mb.ttls = ttls
if numDeleted > 0 {
dmap, n, err := avl.Decode(buf[bi:])
if err != nil {
os.Remove(fn)
fs.warn("Stream state error decoding avl dmap: %v", err)
return errCorruptState
}
mb.dmap = *dmap
if mb.msgs > numDeleted {
mb.msgs -= numDeleted
} else {
mb.msgs = 0
}
bi += n
}
// Pre-emptively mark block as closed, we'll confirm this block
// still exists on disk and report it as lost if not.
mb.closed = true
// Only add in if not empty or the lmb.
if mb.msgs > 0 || i == lastIndex {
fs.addMsgBlock(mb)
updateTrackingState(&mstate, mb)
} else {
// Mark dirty to cleanup.
fs.dirty++
}
}
}
// Pull in last block index for the block that had last checksum when we wrote the full state.
blkIndex := uint32(readU64())
var lchk [8]byte
if bi+len(lchk) > len(buf) {
bi = -1
} else {
copy(lchk[0:], buf[bi:bi+len(lchk)])
}
// Check if we had any errors.
if bi < 0 {
os.Remove(fn)
fs.warn("Stream state has no checksum present")
return errCorruptState
}
// Move into place our state, msgBlks and subject info.
fs.state = state
// First let's check the happy path, open the blk file that was the lmb when we created the full state.
// See if we have the last block available.
var matched bool
mb := fs.lmb
if mb == nil || mb.index != blkIndex {
os.Remove(fn)
fs.warn("Stream state block does not exist or index mismatch")
return errCorruptState
}
if _, err := os.Stat(mb.mfn); err != nil && os.IsNotExist(err) {
// If our saved state is past what we see on disk, fallback and rebuild.
if ld, _, _ := mb.rebuildState(); ld != nil {
fs.addLostData(ld)
}
fs.warn("Stream state detected prior state, could not locate msg block %d", blkIndex)
return errPriorState
}
if matched = bytes.Equal(mb.lastChecksum(), lchk[:]); !matched {
// Detected a stale index.db, we didn't write it upon shutdown so can't rely on it being correct.
fs.warn("Stream state outdated, last block has additional entries, will rebuild")
return errPriorState
}
// We need to see if any blocks exist after our last one even though we matched the last record exactly.
mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
var dirs []os.DirEntry
<-dios
if f, err := os.Open(mdir); err == nil {
dirs, _ = f.ReadDir(-1)
f.Close()
}
dios <- struct{}{}
var index uint32
for _, fi := range dirs {
// Ensure it's actually a block file, otherwise fmt.Sscanf also matches %d.blk.tmp
if !strings.HasSuffix(fi.Name(), blkSuffix) {
continue
}
if n, err := fmt.Sscanf(fi.Name(), blkScan, &index); err == nil && n == 1 {
if index > blkIndex {
fs.warn("Stream state outdated, found extra blocks, will rebuild")
return errPriorState
} else if mb, ok := fs.bim[index]; ok {
mb.closed = false
}
}
}
var rebuild bool
for _, mb := range fs.blks {
if mb.closed {
rebuild = true
if ld, _, _ := mb.rebuildState(); ld != nil {
fs.addLostData(ld)
}
fs.warn("Stream state detected prior state, could not locate msg block %d", mb.index)
}
}
if rebuild {
return errPriorState
}
// We check first and last seq and number of msgs and bytes. If there is a difference,
// return and error so we rebuild from the message block state on disk.
if !trackingStatesEqual(&fs.state, &mstate) {
os.Remove(fn)
fs.warn("Stream state encountered internal inconsistency on recover")
return errCorruptState
}
return nil
}
// Lock should be held.
func (fs *fileStore) recoverTTLState() error {
// See if we have a timed hash wheel for TTLs.
<-dios
fn := filepath.Join(fs.fcfg.StoreDir, msgDir, ttlStreamStateFile)
buf, err := os.ReadFile(fn)
dios <- struct{}{}
if err != nil && !os.IsNotExist(err) {
return err
}
fs.ttls = thw.NewHashWheel()
var ttlseq uint64
if err == nil {
ttlseq, err = fs.ttls.Decode(buf)
if err != nil {
fs.warn("Error decoding TTL state: %s", err)
os.Remove(fn)
}
}
if ttlseq < fs.state.FirstSeq {
ttlseq = fs.state.FirstSeq
}
defer fs.resetAgeChk(0)
if fs.state.Msgs > 0 && ttlseq <= fs.state.LastSeq {
fs.warn("TTL state is outdated; attempting to recover using linear scan (seq %d to %d)", ttlseq, fs.state.LastSeq)
var (
mb *msgBlock
sm StoreMsg
mblseq uint64
)
for seq := ttlseq; seq <= fs.state.LastSeq; seq++ {
retry:
if mb == nil {
if mb = fs.selectMsgBlock(seq); mb == nil {
// Selecting the message block should return a block that contains this sequence,
// or a later block if it can't be found.
// It's an error if we can't find any block within the bounds of first and last seq.
fs.warn("Error loading msg block with seq %d for recovering TTL", seq)
continue
}
seq = atomic.LoadUint64(&mb.first.seq)
mblseq = atomic.LoadUint64(&mb.last.seq)
}
if mb.ttls == 0 {
// None of the messages in the block have message TTLs so don't
// bother doing anything further with this block, skip to the end.
seq = atomic.LoadUint64(&mb.last.seq) + 1
}
if seq > mblseq {
// We've reached the end of the loaded block, so let's go back to the
// beginning and process the next block.
mb.tryForceExpireCache()
mb = nil
if seq <= fs.state.LastSeq {
goto retry
}
// Done.
break
}
msg, _, err := mb.fetchMsgNoCopy(seq, &sm)
if err != nil {
fs.warn("Error loading msg seq %d for recovering TTL: %s", seq, err)
continue
}
if len(msg.hdr) == 0 {
continue
}
if ttl, _ := getMessageTTL(msg.hdr); ttl > 0 {
expires := time.Duration(msg.ts) + (time.Second * time.Duration(ttl))
fs.ttls.Add(seq, int64(expires))
}
}
}
return nil
}
// Grabs last checksum for the named block file.
// Takes into account encryption etc.
func (mb *msgBlock) lastChecksum() []byte {
f, err := mb.openBlock()
if err != nil {
return nil
}
defer f.Close()
var lchk [8]byte
if fi, _ := f.Stat(); fi != nil {
mb.rbytes = uint64(fi.Size())
}
if mb.rbytes < checksumSize {
return lchk[:]
}
// Encrypted?
if err := mb.checkAndLoadEncryption(); err != nil {
return nil
}
if mb.bek != nil {
if buf, _ := mb.loadBlock(nil); len(buf) >= checksumSize {
if err = mb.encryptOrDecryptIfNeeded(buf); err != nil {
return nil
}
copy(lchk[0:], buf[len(buf)-checksumSize:])
}
} else {
f.ReadAt(lchk[:], int64(mb.rbytes)-checksumSize)
}
return lchk[:]
}
// This will make sure we clean up old idx and fss files.
func (fs *fileStore) cleanupOldMeta() {
fs.mu.RLock()
mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
fs.mu.RUnlock()
<-dios
f, err := os.Open(mdir)
dios <- struct{}{}
if err != nil {
return
}
dirs, _ := f.ReadDir(-1)
f.Close()
const (
minLen = 4
idxSuffix = ".idx"
fssSuffix = ".fss"
)
for _, fi := range dirs {
if name := fi.Name(); strings.HasSuffix(name, idxSuffix) || strings.HasSuffix(name, fssSuffix) {
os.Remove(filepath.Join(mdir, name))
}
}
}
func (fs *fileStore) recoverMsgs() error {
fs.mu.Lock()
defer fs.mu.Unlock()
// Check for any left over purged messages.
<-dios
pdir := filepath.Join(fs.fcfg.StoreDir, purgeDir)
if _, err := os.Stat(pdir); err == nil {
os.RemoveAll(pdir)
}
mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
f, err := os.Open(mdir)
if err != nil {
dios <- struct{}{}
return errNotReadable
}
dirs, err := f.ReadDir(-1)
f.Close()
dios <- struct{}{}
if err != nil {
return errNotReadable
}
indices := make(sort.IntSlice, 0, len(dirs))
var index int
for _, fi := range dirs {
// Ensure it's actually a block file, otherwise fmt.Sscanf also matches %d.blk.tmp
if !strings.HasSuffix(fi.Name(), blkSuffix) {
continue
}
if n, err := fmt.Sscanf(fi.Name(), blkScan, &index); err == nil && n == 1 {
indices = append(indices, index)
}
}
indices.Sort()
// Recover all of the msg blocks.
// We now guarantee they are coming in order.
for _, index := range indices {
if mb, err := fs.recoverMsgBlock(uint32(index)); err == nil && mb != nil {
// This is a truncate block with possibly no index. If the OS got shutdown
// out from underneath of us this is possible.
if mb.first.seq == 0 {
mb.dirtyCloseWithRemove(true)
fs.removeMsgBlockFromList(mb)
continue
}
// If the stream is empty, reset the first/last sequences so these can
// properly move up based purely on tombstones spread over multiple blocks.
if fs.state.Msgs == 0 {
fs.state.FirstSeq, fs.state.LastSeq = 0, 0
fs.state.FirstTime, fs.state.LastTime = time.Time{}, time.Time{}
}
fseq := atomic.LoadUint64(&mb.first.seq)
if fs.state.FirstSeq == 0 || (fseq < fs.state.FirstSeq && mb.first.ts != 0) {
fs.state.FirstSeq = fseq
if mb.first.ts == 0 {
fs.state.FirstTime = time.Time{}
} else {
fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC()
}
}
// Preserve last time, could have erased the last message in one block, and then
// have a tombstone with the proper timestamp afterward in another block
if lseq := atomic.LoadUint64(&mb.last.seq); lseq >= fs.state.LastSeq {
fs.state.LastSeq = lseq
if mb.last.ts == 0 {
fs.state.LastTime = time.Time{}
} else {
fs.state.LastTime = time.Unix(0, mb.last.ts).UTC()
}
}
fs.state.Msgs += mb.msgs
fs.state.Bytes += mb.bytes
} else {
return err
}
}
if len(fs.blks) > 0 {
fs.lmb = fs.blks[len(fs.blks)-1]
} else {
_, err = fs.newMsgBlockForWrite()
}
// Check if we encountered any lost data.
if fs.ld != nil {
var emptyBlks []*msgBlock
for _, mb := range fs.blks {
if mb.msgs == 0 && mb.rbytes == 0 && mb != fs.lmb {
emptyBlks = append(emptyBlks, mb)
}
}
for _, mb := range emptyBlks {
// Need the mb lock here.
mb.mu.Lock()
fs.removeMsgBlock(mb)
mb.mu.Unlock()
}
}
if err != nil {
return err
}
// Check for keyfiles orphans.
if kms, err := filepath.Glob(filepath.Join(mdir, keyScanAll)); err == nil && len(kms) > 0 {
valid := make(map[uint32]bool)
for _, mb := range fs.blks {
valid[mb.index] = true
}
for _, fn := range kms {
var index uint32
shouldRemove := true
if n, err := fmt.Sscanf(filepath.Base(fn), keyScan, &index); err == nil && n == 1 && valid[index] {
shouldRemove = false
}
if shouldRemove {
os.Remove(fn)
}
}
}
return nil
}
// Will expire msgs that have aged out on restart.
// We will treat this differently in case we have a recovery
// that will expire alot of messages on startup.
// Should only be called on startup.
func (fs *fileStore) expireMsgsOnRecover() error {
if fs.state.Msgs == 0 {
return nil
}
// If subject delete markers is configured, can't expire on recover.
// When clustered we need to go through proposals.
if fs.cfg.SubjectDeleteMarkerTTL > 0 {
return nil
}
var minAge = time.Now().UnixNano() - int64(fs.cfg.MaxAge)
var purged, bytes uint64
var deleted int
var nts int64
// If we expire all make sure to write out a tombstone. Need to be done by hand here,
// usually taken care of by fs.removeMsgBlock() but we do not call that here.
var last msgId
deleteEmptyBlock := func(mb *msgBlock) error {
// If we are the last keep state to remember first/last sequence.
// Do this part by hand since not deleting one by one.
if mb == fs.lmb {
last.seq = atomic.LoadUint64(&mb.last.seq)
last.ts = mb.last.ts
}
// Make sure we do subject cleanup as well.
mb.ensurePerSubjectInfoLoaded()
mb.fss.IterOrdered(func(bsubj []byte, ss *SimpleState) bool {
subj := bytesToString(bsubj)
for i := uint64(0); i < ss.Msgs; i++ {
fs.removePerSubject(subj)
}
return true
})
err := mb.dirtyCloseWithRemove(true)
if isPermissionError(err) {
return err
}
deleted++
return nil
}
for _, mb := range fs.blks {
mb.mu.Lock()
if minAge < mb.first.ts {
nts = mb.first.ts
mb.mu.Unlock()
break
}
// Can we remove whole block here?
if mb.last.ts <= minAge {
purged += mb.msgs
bytes += mb.bytes
err := deleteEmptyBlock(mb)
mb.mu.Unlock()
if isPermissionError(err) {
return err
}
continue
}
// If we are here we have to process the interior messages of this blk.
// This will load fss as well.
if err := mb.loadMsgsWithLock(); err != nil {
mb.mu.Unlock()
break
}
var smv StoreMsg
var needNextFirst bool
// Walk messages and remove if expired.
fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
for seq := fseq; seq <= lseq; seq++ {
sm, err := mb.cacheLookupNoCopy(seq, &smv)
// Process interior deleted msgs.
if err == errDeletedMsg {
// Update dmap.
if mb.dmap.Exists(seq) {
mb.dmap.Delete(seq)
}
// Keep this updated just in case since we are removing dmap entries.
atomic.StoreUint64(&mb.first.seq, seq)
needNextFirst = true
continue
}
// Break on other errors.
if err != nil || sm == nil {
atomic.StoreUint64(&mb.first.seq, seq)
needNextFirst = true
break
}
// No error and sm != nil from here onward.
// Check for done.
if minAge < sm.ts {
atomic.StoreUint64(&mb.first.seq, sm.seq)
mb.first.ts = sm.ts
needNextFirst = false
nts = sm.ts
break
}
// Delete the message here.
if mb.msgs > 0 {
atomic.StoreUint64(&mb.first.seq, seq)
needNextFirst = true
sz := fileStoreMsgSize(sm.subj, sm.hdr, sm.msg)
if sz > mb.bytes {
sz = mb.bytes
}
mb.bytes -= sz
bytes += sz
mb.msgs--
purged++
}
// Update fss
// Make sure we have fss loaded.
mb.removeSeqPerSubject(sm.subj, seq)
fs.removePerSubject(sm.subj)
}
// Make sure we have a proper next first sequence.
if needNextFirst {
mb.selectNextFirst()
}
// Check if empty after processing, could happen if tail of messages are all deleted.
if mb.msgs == 0 {
deleteEmptyBlock(mb)
}
mb.mu.Unlock()
break
}
if nts > 0 {
// Make sure to set age check based on this value.
fs.resetAgeChk(nts - minAge)
}
if deleted > 0 {
// Update block map.
if fs.bim != nil {
for _, mb := range fs.blks[:deleted] {
delete(fs.bim, mb.index)
}
}
// Update blks slice.
fs.blks = copyMsgBlocks(fs.blks[deleted:])
if lb := len(fs.blks); lb == 0 {
fs.lmb = nil
} else {
fs.lmb = fs.blks[lb-1]
}
}
// Update top level accounting.
if purged < fs.state.Msgs {
fs.state.Msgs -= purged
} else {
fs.state.Msgs = 0
}
if bytes < fs.state.Bytes {
fs.state.Bytes -= bytes
} else {
fs.state.Bytes = 0
}
// Make sure to we properly set the fs first sequence and timestamp.
fs.selectNextFirst()
// Check if we have no messages and blocks left.
if fs.lmb == nil && last.seq != 0 {
if lmb, _ := fs.newMsgBlockForWrite(); lmb != nil {
fs.writeTombstone(last.seq, last.ts)
}
// Clear any global subject state.
fs.psim, fs.tsl = fs.psim.Empty(), 0
}
// If we purged anything, make sure we kick flush state loop.
if purged > 0 {
fs.dirty++
}
return nil
}
func copyMsgBlocks(src []*msgBlock) []*msgBlock {
if src == nil {
return nil
}
dst := make([]*msgBlock, len(src))
copy(dst, src)
return dst
}
// GetSeqFromTime looks for the first sequence number that has
// the message with >= timestamp.
func (fs *fileStore) GetSeqFromTime(t time.Time) uint64 {
fs.mu.RLock()
lastSeq := fs.state.LastSeq
closed := fs.isClosed()
fs.mu.RUnlock()
if closed {
return 0
}
mb := fs.selectMsgBlockForStart(t)
if mb == nil {
return lastSeq + 1
}
fseq := atomic.LoadUint64(&mb.first.seq)
lseq := atomic.LoadUint64(&mb.last.seq)
var (
smv StoreMsg
cts int64
cseq uint64
off uint64
)
ts := t.UnixNano()
// Using a binary search, but need to be aware of interior deletes in the block.
seq := lseq + 1
loop:
for fseq <= lseq {
mid := fseq + (lseq-fseq)/2
off = 0
// Potentially skip over gaps. We keep the original middle but keep track of a
// potential delete range with an offset.
for {
sm, _, err := mb.fetchMsgNoCopy(mid+off, &smv)
if err != nil || sm == nil {
off++
if mid+off <= lseq {
continue
} else {
// Continue search to the left. Purposely ignore the skipped deletes here.
lseq = mid - 1
continue loop
}
}
cts = sm.ts
cseq = sm.seq
break
}
if cts >= ts {
seq = cseq
if mid == fseq {
break
}
// Continue search to the left.
lseq = mid - 1
} else {
// Continue search to the right (potentially skipping over interior deletes).
fseq = mid + off + 1
}
}
return seq
}
// Find the first matching message against a sublist.
func (mb *msgBlock) firstMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
mb.mu.Lock()
var didLoad bool
var updateLLTS bool
defer func() {
if updateLLTS {
mb.llts = ats.AccessTime()
}
mb.mu.Unlock()
}()
if mb.fssNotLoaded() {
// Make sure we have fss loaded.
if err := mb.loadMsgsWithLock(); err != nil {
return nil, false, err
}
didLoad = true
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
// Make sure to start at mb.first.seq if fseq < mb.first.seq
if seq := atomic.LoadUint64(&mb.first.seq); seq > start {
start = seq
}
lseq := atomic.LoadUint64(&mb.last.seq)
// If the FSS state has fewer entries than sequences in the linear scan,
// then use intersection instead as likely going to be cheaper. This will
// often be the case with high numbers of deletes, as well as a smaller
// number of subjects in the block.
if uint64(mb.fss.Size()) < lseq-start {
// If there are no subject matches then this is effectively no-op.
hseq := uint64(math.MaxUint64)
var ierr error
stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) {
if ierr != nil {
return
}
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
// mb is already loaded into the cache so should be fast-ish.
mb.recalculateForSubj(bytesToString(subj), ss)
}
first := max(start, ss.First)
if first > ss.Last || first >= hseq {
// The start cutoff is after the last sequence for this subject,
// or we think we already know of a subject with an earlier msg
// than our first seq for this subject.
return
}
// Need messages loaded from here on out.
if mb.cacheNotLoaded() {
if ierr = mb.loadMsgsWithLock(); ierr != nil {
return
}
didLoad = true
}
if sm == nil {
sm = new(StoreMsg)
}
if first == ss.First {
// If the start floor is below where this subject starts then we can
// short-circuit, avoiding needing to scan for the next message.
if fsm, err := mb.cacheLookup(ss.First, sm); err == nil {
sm = fsm
hseq = ss.First
}
return
}
for seq := first; seq <= ss.Last; seq++ {
// Otherwise we have a start floor that intersects where this subject
// has messages in the block, so we need to walk up until we find a
// message matching the subject.
if mb.dmap.Exists(seq) {
// Optimisation to avoid calling cacheLookup which hits time.Now().
// Instead we will update it only once in a defer.
updateLLTS = true
continue
}
llseq := mb.llseq
fsm, err := mb.cacheLookup(seq, sm)
if err != nil {
continue
}
updateLLTS = false // cacheLookup already updated it.
if sl.HasInterest(fsm.subj) {
hseq = seq
sm = fsm
break
}
// If we are here we did not match, so put the llseq back.
mb.llseq = llseq
}
})
if ierr != nil {
return nil, false, ierr
}
if hseq < uint64(math.MaxUint64) && sm != nil {
return sm, didLoad && start == lseq, nil
}
} else {
// Need messages loaded from here on out.
if mb.cacheNotLoaded() {
if err := mb.loadMsgsWithLock(); err != nil {
return nil, false, err
}
didLoad = true
}
if sm == nil {
sm = new(StoreMsg)
}
for seq := start; seq <= lseq; seq++ {
if mb.dmap.Exists(seq) {
// Optimisation to avoid calling cacheLookup which hits time.Now().
// Instead we will update it only once in a defer.
updateLLTS = true
continue
}
llseq := mb.llseq
fsm, err := mb.cacheLookup(seq, sm)
if err != nil {
continue
}
expireOk := seq == lseq && mb.llseq != llseq && mb.llseq == seq
updateLLTS = false // cacheLookup already updated it.
if sl.HasInterest(fsm.subj) {
return fsm, expireOk, nil
}
// If we are here we did not match, so put the llseq back.
mb.llseq = llseq
}
}
return nil, didLoad, ErrStoreMsgNotFound
}
// Find the first matching message.
// fs lock should be held.
func (mb *msgBlock) firstMatching(filter string, wc bool, start uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
mb.mu.Lock()
var updateLLTS bool
defer func() {
if updateLLTS {
mb.llts = ats.AccessTime()
}
mb.mu.Unlock()
}()
fseq, isAll := start, filter == _EMPTY_ || filter == fwcs
var didLoad bool
if mb.fssNotLoaded() {
// Make sure we have fss loaded.
mb.loadMsgsWithLock()
didLoad = true
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
if filter == _EMPTY_ {
filter = fwcs
wc = true
}
// If we only have 1 subject currently and it matches our filter we can also set isAll.
if !isAll && mb.fss.Size() == 1 {
if !wc {
_, isAll = mb.fss.Find(stringToBytes(filter))
} else {
// Since mb.fss.Find won't work if filter is a wildcard, need to use Match instead.
mb.fss.Match(stringToBytes(filter), func(subject []byte, _ *SimpleState) {
isAll = true
})
}
// If the only subject in this block isn't our filter, can simply short-circuit.
if !isAll {
return nil, didLoad, ErrStoreMsgNotFound
}
}
// Make sure to start at mb.first.seq if fseq < mb.first.seq
fseq = max(fseq, atomic.LoadUint64(&mb.first.seq))
lseq := atomic.LoadUint64(&mb.last.seq)
// Optionally build the isMatch for wildcard filters.
var isMatch func(subj string) bool
// Decide to build.
if wc {
_tsa, _fsa := [32]string{}, [32]string{}
tsa, fsa := _tsa[:0], tokenizeSubjectIntoSlice(_fsa[:0], filter)
isMatch = func(subj string) bool {
tsa = tokenizeSubjectIntoSlice(tsa[:0], subj)
return isSubsetMatchTokenized(tsa, fsa)
}
}
subjs := mb.fs.cfg.Subjects
// If isAll or our single filter matches the filter arg do linear scan.
doLinearScan := isAll || (wc && len(subjs) == 1 && subjs[0] == filter)
// If we do not think we should do a linear scan check how many fss we
// would need to scan vs the full range of the linear walk. Optimize for
// 25th quantile of a match in a linear walk. Filter should be a wildcard.
// We should consult fss if our cache is not loaded and we only have fss loaded.
if !doLinearScan && wc && mb.cacheAlreadyLoaded() {
doLinearScan = mb.fss.Size()*4 > int(lseq-fseq)
}
if !doLinearScan {
// If we have a wildcard match against all tracked subjects we know about.
fseq = lseq + 1
if bfilter := stringToBytes(filter); wc {
mb.fss.Match(bfilter, func(bsubj []byte, ss *SimpleState) {
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
mb.recalculateForSubj(bytesToString(bsubj), ss)
}
if start <= ss.Last {
fseq = min(fseq, max(start, ss.First))
}
})
} else if ss, _ := mb.fss.Find(bfilter); ss != nil {
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
mb.recalculateForSubj(filter, ss)
}
if start <= ss.Last {
fseq = min(fseq, max(start, ss.First))
}
}
}
if fseq > lseq {
return nil, didLoad, ErrStoreMsgNotFound
}
// Need messages loaded from here on out.
if mb.cacheNotLoaded() {
if err := mb.loadMsgsWithLock(); err != nil {
return nil, false, err
}
didLoad = true
}
if sm == nil {
sm = new(StoreMsg)
}
for seq := fseq; seq <= lseq; seq++ {
if mb.dmap.Exists(seq) {
// Optimisation to avoid calling cacheLookup which hits time.Now().
// Instead we will update it only once in a defer.
updateLLTS = true
continue
}
llseq := mb.llseq
fsm, err := mb.cacheLookup(seq, sm)
if err != nil {
if err == errPartialCache || err == errNoCache {
return nil, false, err
}
continue
}
updateLLTS = false // cacheLookup already updated it.
expireOk := seq == lseq && mb.llseq != llseq && mb.llseq == seq
if isAll {
return fsm, expireOk, nil
}
if wc && isMatch(sm.subj) {
return fsm, expireOk, nil
} else if !wc && fsm.subj == filter {
return fsm, expireOk, nil
}
// If we are here we did not match, so put the llseq back.
mb.llseq = llseq
}
return nil, didLoad, ErrStoreMsgNotFound
}
// Find the previous matching message against a sublist, working BACKWARDS from start.
func (mb *msgBlock) prevMatchingMulti(sl *gsl.SimpleSublist, start uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
mb.mu.Lock()
var didLoad bool
var updateLLTS bool
defer func() {
if updateLLTS {
mb.llts = ats.AccessTime()
}
mb.mu.Unlock()
}()
// Need messages loaded from here on out.
if mb.cacheNotLoaded() {
if err := mb.loadMsgsWithLock(); err != nil {
return nil, false, err
}
didLoad = true
}
// Make sure to start at mb.last.seq if lseq < mb.last.seq
if seq := atomic.LoadUint64(&mb.last.seq); start > seq {
start = seq
}
lseq := atomic.LoadUint64(&mb.first.seq)
if sm == nil {
sm = new(StoreMsg)
}
// If the FSS state has fewer entries than sequences in the linear scan,
// then use intersection instead as likely going to be cheaper. This will
// often be the case with high numbers of deletes, as well as a smaller
// number of subjects in the block.
if uint64(mb.fss.Size()) < start-lseq {
// If there are no subject matches then this is effectively no-op.
hseq := uint64(0)
stree.IntersectGSL(mb.fss, sl, func(subj []byte, ss *SimpleState) {
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
// mb is already loaded into the cache so should be fast-ish.
mb.recalculateForSubj(bytesToString(subj), ss)
}
first := min(start, ss.Last)
// Skip if cutoff is before this subject's first, or if we already
// have a higher-or-equal candidate (hseq holds the highest found).
if first < ss.First || first <= hseq {
// The start cutoff is before the first sequence for this subject,
// or we already know of a subject with a later-or-equal msg.
return
}
if first == ss.Last {
// If the start floor is above where this subject starts then we can
// short-circuit, avoiding needing to scan for the next message.
if fsm, err := mb.cacheLookup(ss.Last, sm); err == nil {
sm = fsm
hseq = ss.Last
}
return
}
for seq := first; seq >= ss.First; seq-- {
// Otherwise we have a start floor that intersects where this subject
// has messages in the block, so we need to walk up until we find a
// message matching the subject.
if mb.dmap.Exists(seq) {
// Optimisation to avoid calling cacheLookup which hits time.Now().
// Instead we will update it only once in a defer.
updateLLTS = true
continue
}
llseq := mb.llseq
fsm, err := mb.cacheLookup(seq, sm)
if err != nil {
continue
}
updateLLTS = false // cacheLookup already updated it.
if sl.HasInterest(fsm.subj) {
hseq = seq
sm = fsm
break
}
// If we are here we did not match, so put the llseq back.
mb.llseq = llseq
}
})
if hseq > 0 && sm != nil {
return sm, didLoad && start == lseq, nil
}
} else {
for seq := start; seq >= lseq; seq-- {
if mb.dmap.Exists(seq) {
// Optimisation to avoid calling cacheLookup which hits time.Now().
// Instead we will update it only once in a defer.
updateLLTS = true
continue
}
llseq := mb.llseq
fsm, err := mb.cacheLookup(seq, sm)
if err != nil {
continue
}
expireOk := seq == lseq && mb.llseq != llseq && mb.llseq == seq
updateLLTS = false // cacheLookup already updated it.
if sl.HasInterest(fsm.subj) {
return fsm, expireOk, nil
}
// If we are here we did not match, so put the llseq back.
mb.llseq = llseq
}
}
return nil, didLoad, ErrStoreMsgNotFound
}
// This will traverse a message block and generate the filtered pending.
func (mb *msgBlock) filteredPending(subj string, wc bool, seq uint64) (total, first, last uint64) {
mb.mu.Lock()
defer mb.mu.Unlock()
return mb.filteredPendingLocked(subj, wc, seq)
}
// This will traverse a message block and generate the filtered pending.
// Lock should be held.
func (mb *msgBlock) filteredPendingLocked(filter string, wc bool, sseq uint64) (total, first, last uint64) {
isAll := filter == _EMPTY_ || filter == fwcs
// First check if we can optimize this part.
// This means we want all and the starting sequence was before this block.
if isAll {
if fseq := atomic.LoadUint64(&mb.first.seq); sseq <= fseq {
return mb.msgs, fseq, atomic.LoadUint64(&mb.last.seq)
}
}
if filter == _EMPTY_ {
filter, wc = fwcs, true
}
update := func(ss *SimpleState) {
total += ss.Msgs
if first == 0 || ss.First < first {
first = ss.First
}
if ss.Last > last {
last = ss.Last
}
}
// Make sure we have fss loaded.
mb.ensurePerSubjectInfoLoaded()
var havePartial bool
// If we are not a wildcard just use Find() here. Avoids allocations.
if !wc {
if ss, ok := mb.fss.Find(stringToBytes(filter)); ok && ss != nil {
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
mb.recalculateForSubj(filter, ss)
}
if sseq <= ss.First {
update(ss)
} else if sseq <= ss.Last {
// We matched but its a partial.
havePartial = true
}
}
} else {
mb.fss.Match(stringToBytes(filter), func(bsubj []byte, ss *SimpleState) {
if havePartial {
// If we already found a partial then don't do anything else.
return
}
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
mb.recalculateForSubj(bytesToString(bsubj), ss)
}
if sseq <= ss.First {
update(ss)
} else if sseq <= ss.Last {
// We matched but its a partial.
havePartial = true
}
})
}
// If we did not encounter any partials we can return here.
if !havePartial {
return total, first, last
}
// If we are here we need to scan the msgs.
// Clear what we had.
total, first, last = 0, 0, 0
// If we load the cache for a linear scan we want to expire that cache upon exit.
var shouldExpire bool
if mb.cacheNotLoaded() {
mb.loadMsgsWithLock()
shouldExpire = true
}
_tsa, _fsa := [32]string{}, [32]string{}
tsa, fsa := _tsa[:0], _fsa[:0]
var isMatch func(subj string) bool
if !wc {
isMatch = func(subj string) bool { return subj == filter }
} else {
fsa = tokenizeSubjectIntoSlice(fsa[:0], filter)
isMatch = func(subj string) bool {
tsa = tokenizeSubjectIntoSlice(tsa[:0], subj)
return isSubsetMatchTokenized(tsa, fsa)
}
}
// 1. See if we match any subs from fss.
// 2. If we match and the sseq is past ss.Last then we can use meta only.
// 3. If we match and we need to do a partial, break and clear any totals and do a full scan like num pending.
var smv StoreMsg
for seq, lseq := sseq, atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ {
sm, _ := mb.cacheLookupNoCopy(seq, &smv)
if sm == nil {
continue
}
if isAll || isMatch(sm.subj) {
total++
if first == 0 || seq < first {
first = seq
}
if seq > last {
last = seq
}
}
}
// If we loaded this block for this operation go ahead and expire it here.
if shouldExpire {
mb.tryForceExpireCacheLocked()
}
return total, first, last
}
// FilteredState will return the SimpleState associated with the filtered subject and a proposed starting sequence.
func (fs *fileStore) FilteredState(sseq uint64, subj string) SimpleState {
fs.mu.RLock()
defer fs.mu.RUnlock()
lseq := fs.state.LastSeq
if sseq < fs.state.FirstSeq {
sseq = fs.state.FirstSeq
}
// Returned state.
var ss SimpleState
// If past the end no results.
if sseq > lseq {
// Make sure we track sequences
ss.First = fs.state.FirstSeq
ss.Last = fs.state.LastSeq
return ss
}
// If we want all msgs that match we can shortcircuit.
// TODO(dlc) - This can be extended for all cases but would
// need to be careful on total msgs calculations etc.
if sseq == fs.state.FirstSeq {
fs.numFilteredPending(subj, &ss)
} else {
wc := subjectHasWildcard(subj)
// Tracking subject state.
// TODO(dlc) - Optimize for 2.10 with avl tree and no atomics per block.
for _, mb := range fs.blks {
// Skip blocks that are less than our starting sequence.
if sseq > atomic.LoadUint64(&mb.last.seq) {
continue
}
t, f, l := mb.filteredPending(subj, wc, sseq)
ss.Msgs += t
if ss.First == 0 || (f > 0 && f < ss.First) {
ss.First = f
}
if l > ss.Last {
ss.Last = l
}
}
}
return ss
}
// This is used to see if we can selectively jump start blocks based on filter subject and a starting block index.
// Will return -1 and ErrStoreEOF if no matches at all or no more from where we are.
func (fs *fileStore) checkSkipFirstBlock(filter string, wc bool, bi int) (int, error) {
// If we match everything, just move to next blk.
if filter == _EMPTY_ || filter == fwcs {
return bi + 1, nil
}
// Move through psim to gather start and stop bounds.
start, stop := uint32(math.MaxUint32), uint32(0)
if wc {
fs.psim.Match(stringToBytes(filter), func(_ []byte, psi *psi) {
if psi.fblk < start {
start = psi.fblk
}
if psi.lblk > stop {
stop = psi.lblk
}
})
} else if psi, ok := fs.psim.Find(stringToBytes(filter)); ok {
start, stop = psi.fblk, psi.lblk
}
// Nothing was found.
if start == uint32(math.MaxUint32) {
return -1, ErrStoreEOF
}
return fs.selectSkipFirstBlock(bi, start, stop)
}
// This is used to see if we can selectively jump start blocks based on filter subjects and a starting block index.
// Will return -1 and ErrStoreEOF if no matches at all or no more from where we are.
func (fs *fileStore) checkSkipFirstBlockMulti(sl *gsl.SimpleSublist, bi int) (int, error) {
// Move through psim to gather start and stop bounds.
start, stop := uint32(math.MaxUint32), uint32(0)
stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
if psi.fblk < start {
start = psi.fblk
}
if psi.lblk > stop {
stop = psi.lblk
}
})
// Nothing was found.
if start == uint32(math.MaxUint32) {
return -1, ErrStoreEOF
}
return fs.selectSkipFirstBlock(bi, start, stop)
}
func (fs *fileStore) selectSkipFirstBlock(bi int, start, stop uint32) (int, error) {
// Can not be nil so ok to inline dereference.
mbi := fs.blks[bi].getIndex()
// All matching msgs are behind us.
// Less than AND equal is important because we were called because we missed searching bi.
if stop <= mbi {
return -1, ErrStoreEOF
}
// If start is > index return dereference of fs.blks index.
if start > mbi {
if mb := fs.bim[start]; mb != nil {
ni, _ := fs.selectMsgBlockWithIndex(atomic.LoadUint64(&mb.last.seq))
return ni, nil
}
}
// Otherwise just bump to the next one.
return bi + 1, nil
}
// Optimized way for getting all num pending matching a filter subject.
// Lock should be held.
func (fs *fileStore) numFilteredPending(filter string, ss *SimpleState) {
fs.numFilteredPendingWithLast(filter, true, ss)
}
// Optimized way for getting all num pending matching a filter subject and first sequence only.
// Lock should be held.
func (fs *fileStore) numFilteredPendingNoLast(filter string, ss *SimpleState) {
fs.numFilteredPendingWithLast(filter, false, ss)
}
// Optimized way for getting all num pending matching a filter subject.
// Optionally look up last sequence. Sometimes do not need last and this avoids cost.
// Read lock should be held.
func (fs *fileStore) numFilteredPendingWithLast(filter string, last bool, ss *SimpleState) {
isAll := filter == _EMPTY_ || filter == fwcs
// If isAll we do not need to do anything special to calculate the first and last and total.
if isAll {
ss.First = fs.state.FirstSeq
ss.Last = fs.state.LastSeq
ss.Msgs = fs.state.Msgs
return
}
// Always reset.
ss.First, ss.Last, ss.Msgs = 0, 0, 0
// We do need to figure out the first and last sequences.
wc := subjectHasWildcard(filter)
start, stop := uint32(math.MaxUint32), uint32(0)
if wc {
fs.psim.Match(stringToBytes(filter), func(_ []byte, psi *psi) {
ss.Msgs += psi.total
// Keep track of start and stop indexes for this subject.
if psi.fblk < start {
start = psi.fblk
}
if psi.lblk > stop {
stop = psi.lblk
}
})
} else if psi, ok := fs.psim.Find(stringToBytes(filter)); ok {
ss.Msgs += psi.total
start, stop = psi.fblk, psi.lblk
}
// Did not find anything.
if stop == 0 {
return
}
// Do start
mb := fs.bim[start]
if mb != nil {
_, f, _ := mb.filteredPending(filter, wc, 0)
ss.First = f
}
if ss.First == 0 {
// This is a miss. This can happen since psi.fblk is lazy.
// We will make sure to update fblk.
// Hold this outside loop for psim fblk updates when done.
i := start + 1
for ; i <= stop; i++ {
mb := fs.bim[i]
if mb == nil {
continue
}
if _, f, _ := mb.filteredPending(filter, wc, 0); f > 0 {
ss.First = f
break
}
}
// Update fblk since fblk was outdated.
// We only require read lock here as that is desirable,
// so we need to do this in a go routine to acquire write lock.
go func() {
fs.mu.Lock()
defer fs.mu.Unlock()
if !wc {
if info, ok := fs.psim.Find(stringToBytes(filter)); ok {
if i > info.fblk {
info.fblk = i
}
}
} else {
fs.psim.Match(stringToBytes(filter), func(subj []byte, psi *psi) {
if i > psi.fblk {
psi.fblk = i
}
})
}
}()
}
// Now gather last sequence if asked to do so.
if last {
if mb = fs.bim[stop]; mb != nil {
_, _, l := mb.filteredPending(filter, wc, 0)
ss.Last = l
}
}
}
// SubjectsState returns a map of SimpleState for all matching subjects.
func (fs *fileStore) SubjectsState(subject string) map[string]SimpleState {
fs.mu.RLock()
defer fs.mu.RUnlock()
if fs.state.Msgs == 0 || fs.noTrackSubjects() {
return nil
}
if subject == _EMPTY_ {
subject = fwcs
}
start, stop := fs.blks[0], fs.lmb
// We can short circuit if not a wildcard using psim for start and stop.
if !subjectHasWildcard(subject) {
info, ok := fs.psim.Find(stringToBytes(subject))
if !ok {
return nil
}
if f := fs.bim[info.fblk]; f != nil {
start = f
}
if l := fs.bim[info.lblk]; l != nil {
stop = l
}
}
// Aggregate fss.
fss := make(map[string]SimpleState)
var startFound bool
for _, mb := range fs.blks {
if !startFound {
if mb != start {
continue
}
startFound = true
}
mb.mu.Lock()
var shouldExpire bool
if mb.fssNotLoaded() {
// Make sure we have fss loaded.
mb.loadMsgsWithLock()
shouldExpire = true
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
mb.fss.Match(stringToBytes(subject), func(bsubj []byte, ss *SimpleState) {
subj := string(bsubj)
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
mb.recalculateForSubj(subj, ss)
}
oss := fss[subj]
if oss.First == 0 { // New
fss[subj] = *ss
} else {
// Merge here.
oss.Last, oss.Msgs = ss.Last, oss.Msgs+ss.Msgs
fss[subj] = oss
}
})
if shouldExpire {
// Expire this cache before moving on.
mb.tryForceExpireCacheLocked()
}
mb.mu.Unlock()
if mb == stop {
break
}
}
return fss
}
// AllLastSeqs will return a sorted list of last sequences for all subjects.
func (fs *fileStore) AllLastSeqs() ([]uint64, error) {
fs.mu.RLock()
defer fs.mu.RUnlock()
return fs.allLastSeqsLocked()
}
// allLastSeqsLocked will return a sorted list of last sequences for all
// subjects, but won't take the lock to do it, to avoid the issue of compounding
// read locks causing a deadlock with a write lock.
func (fs *fileStore) allLastSeqsLocked() ([]uint64, error) {
if fs.state.Msgs == 0 || fs.noTrackSubjects() {
return nil, nil
}
numSubjects := fs.psim.Size()
seqs := make([]uint64, 0, numSubjects)
subs := make(map[string]struct{}, numSubjects)
for i := len(fs.blks) - 1; i >= 0; i-- {
if len(subs) == numSubjects {
break
}
mb := fs.blks[i]
mb.mu.Lock()
var shouldExpire bool
if mb.fssNotLoaded() {
// Make sure we have fss loaded.
mb.loadMsgsWithLock()
shouldExpire = true
}
mb.fss.IterFast(func(bsubj []byte, ss *SimpleState) bool {
// Check if already been processed and accounted.
if _, ok := subs[string(bsubj)]; !ok {
seqs = append(seqs, ss.Last)
subs[string(bsubj)] = struct{}{}
}
return true
})
if shouldExpire {
// Expire this cache before moving on.
mb.tryForceExpireCacheLocked()
}
mb.mu.Unlock()
}
slices.Sort(seqs)
return seqs, nil
}
// Helper to determine if the filter(s) represent all the subjects.
// Most clients send in subjects even if they match the stream's ingest subjects.
// Lock should be held.
func (fs *fileStore) filterIsAll(filters []string) bool {
if len(filters) != len(fs.cfg.Subjects) {
return false
}
// Sort so we can compare.
slices.Sort(filters)
for i, subj := range filters {
if !subjectIsSubsetMatch(fs.cfg.Subjects[i], subj) {
return false
}
}
return true
}
// MultiLastSeqs will return a sorted list of sequences that match all subjects presented in filters.
// We will not exceed the maxSeq, which if 0 becomes the store's last sequence.
func (fs *fileStore) MultiLastSeqs(filters []string, maxSeq uint64, maxAllowed int) ([]uint64, error) {
fs.mu.RLock()
defer fs.mu.RUnlock()
if fs.state.Msgs == 0 || fs.noTrackSubjects() {
return nil, nil
}
// See if we can short circuit if we think they are asking for all last sequences and have no maxSeq or maxAllowed set.
if maxSeq == 0 && maxAllowed <= 0 && fs.filterIsAll(filters) {
return fs.allLastSeqsLocked()
}
lastBlkIndex := len(fs.blks) - 1
lastMB := fs.blks[lastBlkIndex]
// Implied last sequence.
if maxSeq == 0 {
maxSeq = fs.state.LastSeq
} else {
// Udate last mb index if not last seq.
lastBlkIndex, lastMB = fs.selectMsgBlockWithIndex(maxSeq)
}
// Make sure non-nil
if lastMB == nil {
return nil, nil
}
// Grab our last mb index (not same as blk index).
lastMB.mu.RLock()
lastMBIndex := lastMB.index
lastMB.mu.RUnlock()
subs := make(map[string]*psi)
var numLess int
var maxBlk uint32
for _, filter := range filters {
fs.psim.Match(stringToBytes(filter), func(subj []byte, psi *psi) {
subs[string(subj)] = psi
if psi.lblk < lastMBIndex {
numLess++
if psi.lblk > maxBlk {
maxBlk = psi.lblk
}
}
})
}
// If all subjects have a lower last index, select the largest for our walk backwards.
if numLess == len(subs) {
lastMB = fs.bim[maxBlk]
}
// Collect all sequences needed.
seqs := make([]uint64, 0, len(subs))
for i, lnf := lastBlkIndex, false; i >= 0; i-- {
if len(subs) == 0 {
break
}
mb := fs.blks[i]
if !lnf {
if mb != lastMB {
continue
}
lnf = true
}
// We can start properly looking here.
mb.mu.Lock()
mb.ensurePerSubjectInfoLoaded()
// Iterate the fss and check against our subs. We will delete from subs as we add.
// Once len(subs) == 0 we are done.
mb.fss.IterFast(func(bsubj []byte, ss *SimpleState) bool {
// Already been processed and accounted for was not matched in the first place.
if subs[string(bsubj)] == nil {
return true
}
// Check if we need to recalculate. We only care about the last sequence.
if ss.lastNeedsUpdate {
// mb is already loaded into the cache so should be fast-ish.
mb.recalculateForSubj(bytesToString(bsubj), ss)
}
// If we are equal or below just add to seqs slice.
if ss.Last <= maxSeq {
seqs = append(seqs, ss.Last)
delete(subs, bytesToString(bsubj))
} else {
// Need to search for the real last since recorded last is > maxSeq.
if mb.cacheNotLoaded() {
mb.loadMsgsWithLock()
}
var smv StoreMsg
fseq := atomic.LoadUint64(&mb.first.seq)
lseq := min(atomic.LoadUint64(&mb.last.seq), maxSeq)
ssubj := bytesToString(bsubj)
for seq := lseq; seq >= fseq; seq-- {
sm, _ := mb.cacheLookupNoCopy(seq, &smv)
if sm == nil || sm.subj != ssubj {
continue
}
seqs = append(seqs, sm.seq)
delete(subs, ssubj)
break
}
}
return true
})
mb.mu.Unlock()
// If maxAllowed was sepcified check that we will not exceed that.
if maxAllowed > 0 && len(seqs) > maxAllowed {
return nil, ErrTooManyResults
}
}
if len(seqs) == 0 {
return nil, nil
}
slices.Sort(seqs)
return seqs, nil
}
// NumPending will return the number of pending messages matching the filter subject starting at sequence.
// Optimized for stream num pending calculations for consumers.
func (fs *fileStore) NumPending(sseq uint64, filter string, lastPerSubject bool) (total, validThrough uint64) {
fs.mu.RLock()
defer fs.mu.RUnlock()
// This can always be last for these purposes.
validThrough = fs.state.LastSeq
if fs.state.Msgs == 0 || sseq > fs.state.LastSeq {
return 0, validThrough
}
// If sseq is less then our first set to first.
if sseq < fs.state.FirstSeq {
sseq = fs.state.FirstSeq
}
// Track starting for both block for the sseq and staring block that matches any subject.
var seqStart int
// See if we need to figure out starting block per sseq.
if sseq > fs.state.FirstSeq {
// This should not, but can return -1, so make sure we check to avoid panic below.
if seqStart, _ = fs.selectMsgBlockWithIndex(sseq); seqStart < 0 {
seqStart = 0
}
}
isAll := filter == _EMPTY_ || filter == fwcs
if isAll && filter == _EMPTY_ {
filter = fwcs
}
wc := subjectHasWildcard(filter)
// See if filter was provided but its the only subject.
if !isAll && !wc && fs.psim.Size() == 1 {
_, isAll = fs.psim.Find(stringToBytes(filter))
}
// If we are isAll and have no deleted we can do a simpler calculation.
if !lastPerSubject && isAll && (fs.state.LastSeq-fs.state.FirstSeq+1) == fs.state.Msgs {
if sseq == 0 {
return fs.state.Msgs, validThrough
}
return fs.state.LastSeq - sseq + 1, validThrough
}
_tsa, _fsa := [32]string{}, [32]string{}
tsa, fsa := _tsa[:0], _fsa[:0]
if wc {
fsa = tokenizeSubjectIntoSlice(fsa[:0], filter)
}
isMatch := func(subj string) bool {
if isAll {
return true
}
if !wc {
return subj == filter
}
tsa = tokenizeSubjectIntoSlice(tsa[:0], subj)
return isSubsetMatchTokenized(tsa, fsa)
}
// Handle last by subject a bit differently.
// We will scan PSIM since we accurately track the last block we have seen the subject in. This
// allows us to only need to load at most one block now.
// For the last block, we need to track the subjects that we know are in that block, and track seen
// while in the block itself, but complexity there worth it.
if lastPerSubject {
// If we want all and our start sequence is equal or less than first return number of subjects.
if isAll && sseq <= fs.state.FirstSeq {
return uint64(fs.psim.Size()), validThrough
}
// If we are here we need to scan. We are going to scan the PSIM looking for lblks that are >= seqStart.
// This will build up a list of all subjects from the selected block onward.
lbm := make(map[string]bool)
mb := fs.blks[seqStart]
bi := mb.index
fs.psim.Match(stringToBytes(filter), func(subj []byte, psi *psi) {
// If the select blk start is greater than entry's last blk skip.
if bi > psi.lblk {
return
}
total++
// We will track the subjects that are an exact match to the last block.
// This is needed for last block processing.
if psi.lblk == bi {
lbm[string(subj)] = true
}
})
// Now check if we need to inspect the seqStart block.
// Grab write lock in case we need to load in msgs.
mb.mu.Lock()
var updateLLTS bool
var shouldExpire bool
// We need to walk this block to correct accounting from above.
if sseq > mb.first.seq {
// Track the ones we add back in case more than one.
seen := make(map[string]bool)
// We need to discount the total by subjects seen before sseq, but also add them right back in if they are >= sseq for this blk.
// This only should be subjects we know have the last blk in this block.
if mb.cacheNotLoaded() {
mb.loadMsgsWithLock()
shouldExpire = true
}
var smv StoreMsg
for seq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ {
if mb.dmap.Exists(seq) {
// Optimisation to avoid calling cacheLookup which hits time.Now().
updateLLTS = true
continue
}
sm, _ := mb.cacheLookupNoCopy(seq, &smv)
if sm == nil || sm.subj == _EMPTY_ || !lbm[sm.subj] {
continue
}
updateLLTS = false // cacheLookup already updated it.
if isMatch(sm.subj) {
// If less than sseq adjust off of total as long as this subject matched the last block.
if seq < sseq {
if !seen[sm.subj] {
total--
seen[sm.subj] = true
}
} else if seen[sm.subj] {
// This is equal or more than sseq, so add back in.
total++
// Make sure to not process anymore.
delete(seen, sm.subj)
}
}
}
}
// If we loaded the block try to force expire.
if shouldExpire {
mb.tryForceExpireCacheLocked()
}
if updateLLTS {
mb.llts = ats.AccessTime()
}
mb.mu.Unlock()
return total, validThrough
}
// If we would need to scan more from the beginning, revert back to calculating directly here.
// TODO(dlc) - Redo properly with sublists etc for subject-based filtering.
if seqStart >= (len(fs.blks) / 2) {
for i := seqStart; i < len(fs.blks); i++ {
var shouldExpire bool
mb := fs.blks[i]
// Hold write lock in case we need to load cache.
mb.mu.Lock()
if isAll && sseq <= atomic.LoadUint64(&mb.first.seq) {
total += mb.msgs
mb.mu.Unlock()
continue
}
// If we are here we need to at least scan the subject fss.
// Make sure we have fss loaded.
if mb.fssNotLoaded() {
mb.loadMsgsWithLock()
shouldExpire = true
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
var t uint64
var havePartial bool
mb.fss.Match(stringToBytes(filter), func(bsubj []byte, ss *SimpleState) {
if havePartial {
// If we already found a partial then don't do anything else.
return
}
subj := bytesToString(bsubj)
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
mb.recalculateForSubj(subj, ss)
}
if sseq <= ss.First {
t += ss.Msgs
} else if sseq <= ss.Last {
// We matched but its a partial.
havePartial = true
}
})
// See if we need to scan msgs here.
if havePartial {
// Make sure we have the cache loaded.
if mb.cacheNotLoaded() {
mb.loadMsgsWithLock()
shouldExpire = true
}
// Clear on partial.
t = 0
start := sseq
if fseq := atomic.LoadUint64(&mb.first.seq); fseq > start {
start = fseq
}
var smv StoreMsg
for seq, lseq := start, atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ {
if sm, _ := mb.cacheLookupNoCopy(seq, &smv); sm != nil && isMatch(sm.subj) {
t++
}
}
}
// If we loaded this block for this operation go ahead and expire it here.
if shouldExpire {
mb.tryForceExpireCacheLocked()
}
mb.mu.Unlock()
total += t
}
return total, validThrough
}
// If we are here it's better to calculate totals from psim and adjust downward by scanning less blocks.
// TODO(dlc) - Eventually when sublist uses generics, make this sublist driven instead.
start := uint32(math.MaxUint32)
fs.psim.Match(stringToBytes(filter), func(_ []byte, psi *psi) {
total += psi.total
// Keep track of start index for this subject.
if psi.fblk < start {
start = psi.fblk
}
})
// See if we were asked for all, if so we are done.
if sseq <= fs.state.FirstSeq {
return total, validThrough
}
// If we are here we need to calculate partials for the first blocks.
firstSubjBlk := fs.bim[start]
var firstSubjBlkFound bool
// Adjust in case not found.
if firstSubjBlk == nil {
firstSubjBlkFound = true
}
// Track how many we need to adjust against the total.
var adjust uint64
for i := 0; i <= seqStart; i++ {
mb := fs.blks[i]
// We can skip blks if we know they are below the first one that has any subject matches.
if !firstSubjBlkFound {
if firstSubjBlkFound = (mb == firstSubjBlk); !firstSubjBlkFound {
continue
}
}
// We need to scan this block.
var shouldExpire bool
var updateLLTS bool
mb.mu.Lock()
// Check if we should include all of this block in adjusting. If so work with metadata.
if sseq > atomic.LoadUint64(&mb.last.seq) {
if isAll {
adjust += mb.msgs
} else {
// We need to adjust for all matches in this block.
// Make sure we have fss loaded. This loads whole block now.
if mb.fssNotLoaded() {
mb.loadMsgsWithLock()
shouldExpire = true
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
mb.fss.Match(stringToBytes(filter), func(bsubj []byte, ss *SimpleState) {
adjust += ss.Msgs
})
}
} else {
// This is the last block. We need to scan per message here.
if mb.cacheNotLoaded() {
mb.loadMsgsWithLock()
shouldExpire = true
}
var last = atomic.LoadUint64(&mb.last.seq)
if sseq < last {
last = sseq
}
// We need to walk all messages in this block
var smv StoreMsg
for seq := atomic.LoadUint64(&mb.first.seq); seq < last; seq++ {
if mb.dmap.Exists(seq) {
// Optimisation to avoid calling cacheLookup which hits time.Now().
updateLLTS = true
continue
}
sm, _ := mb.cacheLookupNoCopy(seq, &smv)
if sm == nil || sm.subj == _EMPTY_ {
continue
}
updateLLTS = false // cacheLookup already updated it.
// Check if it matches our filter.
if sm.seq < sseq && isMatch(sm.subj) {
adjust++
}
}
}
// If we loaded the block try to force expire.
if shouldExpire {
mb.tryForceExpireCacheLocked()
}
if updateLLTS {
mb.llts = ats.AccessTime()
}
mb.mu.Unlock()
}
// Make final adjustment.
total -= adjust
return total, validThrough
}
// NumPending will return the number of pending messages matching any subject in the sublist starting at sequence.
// Optimized for stream num pending calculations for consumers with lots of filtered subjects.
// Subjects should not overlap, this property is held when doing multi-filtered consumers.
func (fs *fileStore) NumPendingMulti(sseq uint64, sl *gsl.SimpleSublist, lastPerSubject bool) (total, validThrough uint64) {
fs.mu.RLock()
defer fs.mu.RUnlock()
// This can always be last for these purposes.
validThrough = fs.state.LastSeq
if fs.state.Msgs == 0 || sseq > fs.state.LastSeq {
return 0, validThrough
}
// If sseq is less then our first set to first.
if sseq < fs.state.FirstSeq {
sseq = fs.state.FirstSeq
}
// Track starting for both block for the sseq and staring block that matches any subject.
var seqStart int
// See if we need to figure out starting block per sseq.
if sseq > fs.state.FirstSeq {
// This should not, but can return -1, so make sure we check to avoid panic below.
if seqStart, _ = fs.selectMsgBlockWithIndex(sseq); seqStart < 0 {
seqStart = 0
}
}
isAll := sl == nil
// See if filter was provided but its the only subject.
if !isAll && fs.psim.Size() == 1 {
fs.psim.IterFast(func(subject []byte, _ *psi) bool {
isAll = sl.HasInterest(bytesToString(subject))
return true
})
}
// If we are isAll and have no deleted we can do a simpler calculation.
if !lastPerSubject && isAll && (fs.state.LastSeq-fs.state.FirstSeq+1) == fs.state.Msgs {
if sseq == 0 {
return fs.state.Msgs, validThrough
}
return fs.state.LastSeq - sseq + 1, validThrough
}
// Setup the isMatch function.
isMatch := func(subj string) bool {
if isAll {
return true
}
return sl.HasInterest(subj)
}
// Handle last by subject a bit differently.
// We will scan PSIM since we accurately track the last block we have seen the subject in. This
// allows us to only need to load at most one block now.
// For the last block, we need to track the subjects that we know are in that block, and track seen
// while in the block itself, but complexity there worth it.
if lastPerSubject {
// If we want all and our start sequence is equal or less than first return number of subjects.
if isAll && sseq <= fs.state.FirstSeq {
return uint64(fs.psim.Size()), validThrough
}
// If we are here we need to scan. We are going to scan the PSIM looking for lblks that are >= seqStart.
// This will build up a list of all subjects from the selected block onward.
lbm := make(map[string]bool)
mb := fs.blks[seqStart]
bi := mb.index
stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
// If the select blk start is greater than entry's last blk skip.
if bi > psi.lblk {
return
}
total++
// We will track the subjects that are an exact match to the last block.
// This is needed for last block processing.
if psi.lblk == bi {
lbm[string(subj)] = true
}
})
// Now check if we need to inspect the seqStart block.
// Grab write lock in case we need to load in msgs.
mb.mu.Lock()
var shouldExpire bool
var updateLLTS bool
// We need to walk this block to correct accounting from above.
if sseq > mb.first.seq {
// Track the ones we add back in case more than one.
seen := make(map[string]bool)
// We need to discount the total by subjects seen before sseq, but also add them right back in if they are >= sseq for this blk.
// This only should be subjects we know have the last blk in this block.
if mb.cacheNotLoaded() {
mb.loadMsgsWithLock()
shouldExpire = true
}
var smv StoreMsg
for seq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ {
if mb.dmap.Exists(seq) {
// Optimisation to avoid calling cacheLookup which hits time.Now().
updateLLTS = true
continue
}
sm, _ := mb.cacheLookupNoCopy(seq, &smv)
if sm == nil || sm.subj == _EMPTY_ || !lbm[sm.subj] {
continue
}
updateLLTS = false // cacheLookup already updated it.
if isMatch(sm.subj) {
// If less than sseq adjust off of total as long as this subject matched the last block.
if seq < sseq {
if !seen[sm.subj] {
total--
seen[sm.subj] = true
}
} else if seen[sm.subj] {
// This is equal or more than sseq, so add back in.
total++
// Make sure to not process anymore.
delete(seen, sm.subj)
}
}
}
}
// If we loaded the block try to force expire.
if shouldExpire {
mb.tryForceExpireCacheLocked()
}
if updateLLTS {
mb.llts = ats.AccessTime()
}
mb.mu.Unlock()
return total, validThrough
}
// If we would need to scan more from the beginning, revert back to calculating directly here.
if seqStart >= (len(fs.blks) / 2) {
for i := seqStart; i < len(fs.blks); i++ {
var shouldExpire bool
mb := fs.blks[i]
// Hold write lock in case we need to load cache.
mb.mu.Lock()
if isAll && sseq <= atomic.LoadUint64(&mb.first.seq) {
total += mb.msgs
mb.mu.Unlock()
continue
}
// If we are here we need to at least scan the subject fss.
// Make sure we have fss loaded.
if mb.fssNotLoaded() {
mb.loadMsgsWithLock()
shouldExpire = true
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
var t uint64
var havePartial bool
var updateLLTS bool
stree.IntersectGSL[SimpleState](mb.fss, sl, func(bsubj []byte, ss *SimpleState) {
subj := bytesToString(bsubj)
if havePartial {
// If we already found a partial then don't do anything else.
return
}
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
mb.recalculateForSubj(subj, ss)
}
if sseq <= ss.First {
t += ss.Msgs
} else if sseq <= ss.Last {
// We matched but its a partial.
havePartial = true
}
})
// See if we need to scan msgs here.
if havePartial {
// Make sure we have the cache loaded.
if mb.cacheNotLoaded() {
mb.loadMsgsWithLock()
shouldExpire = true
}
// Clear on partial.
t = 0
start := sseq
if fseq := atomic.LoadUint64(&mb.first.seq); fseq > start {
start = fseq
}
var smv StoreMsg
for seq, lseq := start, atomic.LoadUint64(&mb.last.seq); seq <= lseq; seq++ {
if mb.dmap.Exists(seq) {
// Optimisation to avoid calling cacheLookup which hits time.Now().
updateLLTS = true
continue
}
if sm, _ := mb.cacheLookupNoCopy(seq, &smv); sm != nil && isMatch(sm.subj) {
t++
updateLLTS = false // cacheLookup already updated it.
}
}
}
// If we loaded this block for this operation go ahead and expire it here.
if shouldExpire {
mb.tryForceExpireCacheLocked()
}
if updateLLTS {
mb.llts = ats.AccessTime()
}
mb.mu.Unlock()
total += t
}
return total, validThrough
}
// If we are here it's better to calculate totals from psim and adjust downward by scanning less blocks.
start := uint32(math.MaxUint32)
stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
total += psi.total
// Keep track of start index for this subject.
if psi.fblk < start {
start = psi.fblk
}
})
// See if we were asked for all, if so we are done.
if sseq <= fs.state.FirstSeq {
return total, validThrough
}
// If we are here we need to calculate partials for the first blocks.
firstSubjBlk := fs.bim[start]
var firstSubjBlkFound bool
// Adjust in case not found.
if firstSubjBlk == nil {
firstSubjBlkFound = true
}
// Track how many we need to adjust against the total.
var adjust uint64
for i := 0; i <= seqStart; i++ {
mb := fs.blks[i]
// We can skip blks if we know they are below the first one that has any subject matches.
if !firstSubjBlkFound {
if firstSubjBlkFound = (mb == firstSubjBlk); !firstSubjBlkFound {
continue
}
}
// We need to scan this block.
var shouldExpire bool
var updateLLTS bool
mb.mu.Lock()
// Check if we should include all of this block in adjusting. If so work with metadata.
if sseq > atomic.LoadUint64(&mb.last.seq) {
if isAll {
adjust += mb.msgs
} else {
// We need to adjust for all matches in this block.
// Make sure we have fss loaded. This loads whole block now.
if mb.fssNotLoaded() {
mb.loadMsgsWithLock()
shouldExpire = true
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
stree.IntersectGSL(mb.fss, sl, func(bsubj []byte, ss *SimpleState) {
adjust += ss.Msgs
})
}
} else {
// This is the last block. We need to scan per message here.
if mb.cacheNotLoaded() {
mb.loadMsgsWithLock()
shouldExpire = true
}
var last = atomic.LoadUint64(&mb.last.seq)
if sseq < last {
last = sseq
}
// We need to walk all messages in this block
var smv StoreMsg
for seq := atomic.LoadUint64(&mb.first.seq); seq < last; seq++ {
if mb.dmap.Exists(seq) {
// Optimisation to avoid calling cacheLookup which hits time.Now().
updateLLTS = true
continue
}
sm, _ := mb.cacheLookupNoCopy(seq, &smv)
if sm == nil || sm.subj == _EMPTY_ {
continue
}
updateLLTS = false // cacheLookup already updated it.
// Check if it matches our filter.
if sm.seq < sseq && isMatch(sm.subj) {
adjust++
}
}
}
// If we loaded the block try to force expire.
if shouldExpire {
mb.tryForceExpireCacheLocked()
}
if updateLLTS {
mb.llts = ats.AccessTime()
}
mb.mu.Unlock()
}
// Make final adjustment.
total -= adjust
return total, validThrough
}
// SubjectsTotals return message totals per subject.
func (fs *fileStore) SubjectsTotals(filter string) map[string]uint64 {
fs.mu.RLock()
defer fs.mu.RUnlock()
return fs.subjectsTotalsLocked(filter)
}
// Lock should be held.
func (fs *fileStore) subjectsTotalsLocked(filter string) map[string]uint64 {
if fs.psim.Size() == 0 {
return nil
}
// Match all if no filter given.
if filter == _EMPTY_ {
filter = fwcs
}
fst := make(map[string]uint64)
fs.psim.Match(stringToBytes(filter), func(subj []byte, psi *psi) {
fst[string(subj)] = psi.total
})
return fst
}
// RegisterStorageUpdates registers a callback for updates to storage changes.
// It will present number of messages and bytes as a signed integer and an
// optional sequence number of the message if a single.
func (fs *fileStore) RegisterStorageUpdates(cb StorageUpdateHandler) {
fs.mu.Lock()
fs.scb = cb
bsz := fs.state.Bytes
fs.mu.Unlock()
if cb != nil && bsz > 0 {
cb(0, int64(bsz), 0, _EMPTY_)
}
}
// RegisterStorageRemoveMsg registers a callback to remove messages.
// Replicated streams should propose removals, R1 can remove inline.
func (fs *fileStore) RegisterStorageRemoveMsg(cb StorageRemoveMsgHandler) {
fs.mu.Lock()
fs.rmcb = cb
fs.mu.Unlock()
}
// RegisterSubjectDeleteMarkerUpdates registers a callback for updates to new tombstones.
func (fs *fileStore) RegisterSubjectDeleteMarkerUpdates(cb SubjectDeleteMarkerUpdateHandler) {
fs.mu.Lock()
fs.sdmcb = cb
fs.mu.Unlock()
}
// Helper to get hash key for specific message block.
// Lock should be held
func (fs *fileStore) hashKeyForBlock(index uint32) []byte {
return []byte(fmt.Sprintf("%s-%d", fs.cfg.Name, index))
}
func (mb *msgBlock) setupWriteCache(buf []byte) {
// Make sure we have a cache setup.
if mb.cache != nil {
return
}
// Setup simple cache.
mb.cache = &cache{buf: buf}
// Make sure we set the proper cache offset if we have existing data.
var fi os.FileInfo
if mb.mfd != nil {
fi, _ = mb.mfd.Stat()
} else if mb.mfn != _EMPTY_ {
fi, _ = os.Stat(mb.mfn)
}
if fi != nil {
mb.cache.off = int(fi.Size())
}
mb.llts = ats.AccessTime()
mb.startCacheExpireTimer()
}
// This rolls to a new append msg block.
// Lock should be held.
func (fs *fileStore) newMsgBlockForWrite() (*msgBlock, error) {
index := uint32(1)
var rbuf []byte
if lmb := fs.lmb; lmb != nil {
lmb.mu.Lock()
index = lmb.index + 1
// Flush any pending messages.
lmb.flushPendingMsgsLocked()
// Determine if we can reclaim any resources here.
lmb.closeFDsLockedNoCheck()
if lmb.cache != nil {
// Reset write timestamp and see if we can expire this cache.
rbuf = lmb.tryExpireWriteCache()
}
lmb.mu.Unlock()
if fs.fcfg.Compression != NoCompression {
// We've now reached the end of this message block, if we want
// to compress blocks then now's the time to do it.
go func() {
lmb.mu.Lock()
defer lmb.mu.Unlock()
lmb.recompressOnDiskIfNeeded()
}()
}
}
mb := fs.initMsgBlock(index)
// Lock should be held to quiet race detector.
mb.mu.Lock()
mb.setupWriteCache(rbuf)
mb.fss = stree.NewSubjectTree[SimpleState]()
// Set cache time to creation time to start.
mb.llts, mb.lwts = 0, ats.AccessTime()
// Remember our last sequence number.
atomic.StoreUint64(&mb.first.seq, fs.state.LastSeq+1)
atomic.StoreUint64(&mb.last.seq, fs.state.LastSeq)
mb.mu.Unlock()
// Now do local hash.
key := sha256.Sum256(fs.hashKeyForBlock(index))
hh, err := highwayhash.NewDigest64(key[:])
if err != nil {
return nil, fmt.Errorf("could not create hash: %v", err)
}
mb.hh = hh
<-dios
mfd, err := os.OpenFile(mb.mfn, os.O_CREATE|os.O_RDWR, defaultFilePerms)
dios <- struct{}{}
if err != nil {
if isPermissionError(err) {
return nil, err
}
mb.dirtyCloseWithRemove(true)
return nil, fmt.Errorf("Error creating msg block file: %v", err)
}
mb.mfd = mfd
// Check if encryption is enabled.
if fs.prf != nil {
if err := fs.genEncryptionKeysForBlock(mb); err != nil {
return nil, err
}
}
// If we know we will need this so go ahead and spin up.
if !fs.fip {
mb.spinUpFlushLoop()
}
// Add to our list of blocks and mark as last.
fs.addMsgBlock(mb)
return mb, nil
}
// Generate the keys for this message block and write them out.
func (fs *fileStore) genEncryptionKeysForBlock(mb *msgBlock) error {
if mb == nil {
return nil
}
key, bek, seed, encrypted, err := fs.genEncryptionKeys(fmt.Sprintf("%s:%d", fs.cfg.Name, mb.index))
if err != nil {
return err
}
mb.aek, mb.bek, mb.seed, mb.nonce = key, bek, seed, encrypted[:key.NonceSize()]
mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
keyFile := filepath.Join(mdir, fmt.Sprintf(keyScan, mb.index))
if _, err := os.Stat(keyFile); err != nil && !os.IsNotExist(err) {
return err
}
err = fs.writeFileWithOptionalSync(keyFile, encrypted, defaultFilePerms)
if err != nil {
return err
}
mb.kfn = keyFile
return nil
}
// Stores a raw message with expected sequence number and timestamp.
// Lock should be held.
func (fs *fileStore) storeRawMsg(subj string, hdr, msg []byte, seq uint64, ts, ttl int64) (err error) {
if fs.isClosed() {
return ErrStoreClosed
}
// Per subject max check needed.
mmp := uint64(fs.cfg.MaxMsgsPer)
var psmc uint64
psmax := mmp > 0 && len(subj) > 0
if psmax {
if info, ok := fs.psim.Find(stringToBytes(subj)); ok {
psmc = info.total
}
}
var fseq uint64
// Check if we are discarding new messages when we reach the limit.
if fs.cfg.Discard == DiscardNew {
var asl bool
if psmax && psmc >= mmp {
// If we are instructed to discard new per subject, this is an error.
if fs.cfg.DiscardNewPer {
return ErrMaxMsgsPerSubject
}
if fseq, err = fs.firstSeqForSubj(subj); err != nil {
return err
}
asl = true
}
// If we are discard new and limits policy and clustered, we do the enforcement
// above and should not disqualify the message here since it could cause replicas to drift.
if fs.cfg.Retention == LimitsPolicy || fs.cfg.Replicas == 1 {
if fs.cfg.MaxMsgs > 0 && fs.state.Msgs >= uint64(fs.cfg.MaxMsgs) && !asl {
return ErrMaxMsgs
}
if fs.cfg.MaxBytes > 0 && fs.state.Bytes+fileStoreMsgSize(subj, hdr, msg) >= uint64(fs.cfg.MaxBytes) {
if !asl || fs.sizeForSeq(fseq) <= int(fileStoreMsgSize(subj, hdr, msg)) {
return ErrMaxBytes
}
}
}
}
// Check sequence.
if seq != fs.state.LastSeq+1 {
if seq > 0 {
return ErrSequenceMismatch
}
seq = fs.state.LastSeq + 1
}
// Write msg record.
// Add expiry bit to sequence if needed. This is so that if we need to
// rebuild, we know which messages to look at more quickly.
n, err := fs.writeMsgRecord(seq, ts, subj, hdr, msg)
if err != nil {
return err
}
// Adjust top level tracking of per subject msg counts.
if len(subj) > 0 && fs.psim != nil {
index := fs.lmb.index
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})
fs.tsl += len(subj)
}
}
// Adjust first if needed.
now := time.Unix(0, ts).UTC()
if fs.state.Msgs == 0 {
fs.state.FirstSeq = seq
fs.state.FirstTime = now
}
fs.state.Msgs++
fs.state.Bytes += n
fs.state.LastSeq = seq
fs.state.LastTime = now
// Enforce per message limits.
// We snapshotted psmc before our actual write, so >= comparison needed.
if psmax && psmc >= mmp {
// We may have done this above.
if fseq == 0 {
fseq, _ = fs.firstSeqForSubj(subj)
}
if ok, _ := fs.removeMsgViaLimits(fseq); ok {
// Make sure we are below the limit.
if psmc--; psmc >= mmp {
bsubj := stringToBytes(subj)
for info, ok := fs.psim.Find(bsubj); ok && info.total > mmp; info, ok = fs.psim.Find(bsubj) {
if seq, _ := fs.firstSeqForSubj(subj); seq > 0 {
if ok, _ := fs.removeMsgViaLimits(seq); !ok {
break
}
} else {
break
}
}
}
} else if mb := fs.selectMsgBlock(fseq); mb != nil {
// If we are here we could not remove fseq from above, so rebuild.
var ld *LostStreamData
if ld, _, _ = mb.rebuildState(); ld != nil {
fs.rebuildStateLocked(ld)
}
}
}
// Limits checks and enforcement.
// If they do any deletions they will update the
// byte count on their own, so no need to compensate.
fs.enforceMsgLimit()
fs.enforceBytesLimit()
// Per-message TTL.
if fs.ttls != nil && ttl > 0 {
expires := time.Duration(ts) + (time.Second * time.Duration(ttl))
fs.ttls.Add(seq, int64(expires))
fs.lmb.ttls++
}
// Check if we have and need the age expiration timer running.
switch {
case fs.ttls != nil && ttl > 0:
fs.resetAgeChk(0)
case fs.ageChk == nil && (fs.cfg.MaxAge > 0 || fs.ttls != nil):
fs.startAgeChk()
}
return nil
}
// StoreRawMsg stores a raw message with expected sequence number and timestamp.
func (fs *fileStore) StoreRawMsg(subj string, hdr, msg []byte, seq uint64, ts, ttl int64) error {
fs.mu.Lock()
err := fs.storeRawMsg(subj, hdr, msg, seq, ts, ttl)
cb := fs.scb
// Check if first message timestamp requires expiry
// sooner than initial replica expiry timer set to MaxAge when initializing.
if !fs.receivedAny && fs.cfg.MaxAge != 0 && ts > 0 {
fs.receivedAny = true
fs.resetAgeChk(0)
}
fs.mu.Unlock()
if err == nil && cb != nil {
cb(1, int64(fileStoreMsgSize(subj, hdr, msg)), seq, subj)
}
return err
}
// Store stores a message. We hold the main filestore lock for any write operation.
func (fs *fileStore) StoreMsg(subj string, hdr, msg []byte, ttl int64) (uint64, int64, error) {
fs.mu.Lock()
seq, ts := fs.state.LastSeq+1, time.Now().UnixNano()
err := fs.storeRawMsg(subj, hdr, msg, seq, ts, ttl)
cb := fs.scb
fs.mu.Unlock()
if err != nil {
seq, ts = 0, 0
} else if cb != nil {
cb(1, int64(fileStoreMsgSize(subj, hdr, msg)), seq, subj)
}
return seq, ts, err
}
// skipMsg will update this message block for a skipped message.
// If we do not have any messages, just update the metadata, otherwise
// we will place an empty record marking the sequence as used. The
// sequence will be marked erased.
// fs lock should be held.
func (mb *msgBlock) skipMsg(seq uint64, now int64) {
if mb == nil {
return
}
var needsRecord bool
mb.mu.Lock()
// If we are empty can just do meta.
if mb.msgs == 0 {
atomic.StoreUint64(&mb.last.seq, seq)
mb.last.ts = now
atomic.StoreUint64(&mb.first.seq, seq+1)
mb.first.ts = 0
needsRecord = mb == mb.fs.lmb
} else {
needsRecord = true
mb.dmap.Insert(seq)
}
if needsRecord {
mb.writeMsgRecordLocked(emptyRecordLen, seq|ebit, _EMPTY_, nil, nil, now, true, true)
}
mb.mu.Unlock()
if !needsRecord {
mb.kickFlusher()
}
}
// SkipMsg will use the next sequence number but not store anything.
func (fs *fileStore) SkipMsg(seq uint64) (uint64, error) {
// Grab time.
now := ats.AccessTime()
fs.mu.Lock()
defer fs.mu.Unlock()
// Check sequence matches our last sequence.
if seq != fs.state.LastSeq+1 {
if seq > 0 {
return 0, ErrSequenceMismatch
}
seq = fs.state.LastSeq + 1
}
// Grab our current last message block.
mb, err := fs.checkLastBlock(emptyRecordLen)
if err != nil {
return 0, err
}
// Write skip msg.
mb.skipMsg(seq, now)
// Update fs state.
fs.state.LastSeq, fs.state.LastTime = seq, time.Unix(0, now).UTC()
if fs.state.Msgs == 0 {
fs.state.FirstSeq, fs.state.FirstTime = seq, time.Time{}
}
if seq == fs.state.FirstSeq {
fs.state.FirstSeq, fs.state.FirstTime = seq+1, time.Time{}
}
// Mark as dirty for stream state.
fs.dirty++
return seq, nil
}
// Skip multiple msgs. We will determine if we can fit into current lmb or we need to create a new block.
func (fs *fileStore) SkipMsgs(seq uint64, num uint64) error {
fs.mu.Lock()
defer fs.mu.Unlock()
// Check sequence matches our last sequence.
if seq != fs.state.LastSeq+1 {
if seq > 0 {
return ErrSequenceMismatch
}
seq = fs.state.LastSeq + 1
}
// Limit number of dmap entries
const maxDeletes = 64 * 1024
mb := fs.lmb
numDeletes := int(num)
if mb != nil {
numDeletes += mb.dmap.Size()
}
if mb == nil || numDeletes > maxDeletes && mb.msgs > 0 || mb.msgs > 0 && mb.blkSize()+emptyRecordLen > fs.fcfg.BlockSize {
var err error
if mb, err = fs.newMsgBlockForWrite(); err != nil {
return err
}
}
// Insert into dmap all entries and place last as marker.
now := ats.AccessTime()
lseq := seq + num - 1
mb.mu.Lock()
// If we are empty update meta directly.
if mb.msgs == 0 {
atomic.StoreUint64(&mb.last.seq, lseq)
mb.last.ts = now
atomic.StoreUint64(&mb.first.seq, lseq+1)
mb.first.ts = 0
} else {
for ; seq <= lseq; seq++ {
mb.dmap.Insert(seq)
}
}
// Write out our placeholder.
mb.writeMsgRecordLocked(emptyRecordLen, lseq|ebit, _EMPTY_, nil, nil, now, true, true)
mb.mu.Unlock()
// Now update FS accounting.
// Update fs state.
fs.state.LastSeq, fs.state.LastTime = lseq, time.Unix(0, now).UTC()
if fs.state.Msgs == 0 {
fs.state.FirstSeq, fs.state.FirstTime = lseq+1, time.Time{}
}
// Mark as dirty for stream state.
fs.dirty++
return nil
}
// Lock should be held.
func (fs *fileStore) rebuildFirst() {
if len(fs.blks) == 0 {
return
}
fmb := fs.blks[0]
if fmb == nil {
return
}
ld, _, _ := fmb.rebuildState()
fmb.mu.RLock()
isEmpty := fmb.msgs == 0
fmb.mu.RUnlock()
if isEmpty {
fmb.mu.Lock()
fs.removeMsgBlock(fmb)
fmb.mu.Unlock()
}
fs.selectNextFirst()
fs.rebuildStateLocked(ld)
}
// Optimized helper function to return first sequence.
// subj will always be publish subject here, meaning non-wildcard.
// We assume a fast check that this subj even exists already happened.
// Write lock should be held.
func (fs *fileStore) firstSeqForSubj(subj string) (uint64, error) {
if len(fs.blks) == 0 {
return 0, nil
}
// See if we can optimize where we start.
start, stop := fs.blks[0].index, fs.lmb.index
if info, ok := fs.psim.Find(stringToBytes(subj)); ok {
start, stop = info.fblk, info.lblk
}
for i := start; i <= stop; i++ {
mb := fs.bim[i]
if mb == nil {
continue
}
// If we need to load msgs here and we need to walk multiple blocks this
// could tie up the upper fs lock, so release while dealing with the block.
fs.mu.Unlock()
mb.mu.Lock()
var shouldExpire bool
if mb.fssNotLoaded() {
// Make sure we have fss loaded.
if err := mb.loadMsgsWithLock(); err != nil {
mb.mu.Unlock()
// Re-acquire fs lock
fs.mu.Lock()
return 0, err
}
shouldExpire = true
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
bsubj := stringToBytes(subj)
if ss, ok := mb.fss.Find(bsubj); ok && ss != nil {
// Adjust first if it was not where we thought it should be.
if i != start {
if info, ok := fs.psim.Find(bsubj); ok {
info.fblk = i
}
}
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
mb.recalculateForSubj(subj, ss)
}
mb.mu.Unlock()
// Re-acquire fs lock
fs.mu.Lock()
return ss.First, nil
}
// If we did not find it and we loaded this msgBlock try to expire as long as not the last.
if shouldExpire {
// Expire this cache before moving on.
mb.tryForceExpireCacheLocked()
}
mb.mu.Unlock()
// Re-acquire fs lock
fs.mu.Lock()
}
return 0, nil
}
// Will check the msg limit and drop firstSeq msg if needed.
// Lock should be held.
func (fs *fileStore) enforceMsgLimit() {
if fs.cfg.Discard != DiscardOld {
return
}
if fs.cfg.MaxMsgs <= 0 || fs.state.Msgs <= uint64(fs.cfg.MaxMsgs) {
return
}
for nmsgs := fs.state.Msgs; nmsgs > uint64(fs.cfg.MaxMsgs); nmsgs = fs.state.Msgs {
// If the first block can be removed fully, purge it entirely without needing to walk sequences.
if len(fs.blks) > 0 {
fmb := fs.blks[0]
fmb.mu.RLock()
msgs := fmb.msgs
fmb.mu.RUnlock()
if nmsgs-msgs > uint64(fs.cfg.MaxMsgs) {
fs.purgeMsgBlock(fmb)
continue
}
}
if removed, err := fs.deleteFirstMsg(); err != nil || !removed {
fs.rebuildFirst()
return
}
}
}
// Will check the bytes limit and drop msgs if needed.
// Lock should be held.
func (fs *fileStore) enforceBytesLimit() {
if fs.cfg.Discard != DiscardOld {
return
}
if fs.cfg.MaxBytes <= 0 || fs.state.Bytes <= uint64(fs.cfg.MaxBytes) {
return
}
for bs := fs.state.Bytes; bs > uint64(fs.cfg.MaxBytes); bs = fs.state.Bytes {
// If the first block can be removed fully, purge it entirely without needing to walk sequences.
if len(fs.blks) > 0 {
fmb := fs.blks[0]
fmb.mu.RLock()
bytes := fmb.bytes
fmb.mu.RUnlock()
if bs-bytes > uint64(fs.cfg.MaxBytes) {
fs.purgeMsgBlock(fmb)
continue
}
}
if removed, err := fs.deleteFirstMsg(); err != nil || !removed {
fs.rebuildFirst()
return
}
}
}
// Will make sure we have limits honored for max msgs per subject on recovery or config update.
// We will make sure to go through all msg blocks etc. but in practice this
// will most likely only be the last one, so can take a more conservative approach.
// Lock should be held.
func (fs *fileStore) enforceMsgPerSubjectLimit(fireCallback bool) {
start := time.Now()
defer func() {
if took := time.Since(start); took > time.Minute {
fs.warn("enforceMsgPerSubjectLimit took %v", took.Round(time.Millisecond))
}
}()
maxMsgsPer := uint64(fs.cfg.MaxMsgsPer)
// We may want to suppress callbacks from remove during this process
// since these should have already been deleted and accounted for.
if !fireCallback {
cb := fs.scb
fs.scb = nil
defer func() { fs.scb = cb }()
}
var numMsgs uint64
// collect all that are not correct.
needAttention := stree.NewSubjectTree[uint64]()
fblk, lblk := uint32(math.MaxUint32), uint32(0)
fs.psim.IterFast(func(subj []byte, psi *psi) bool {
numMsgs += psi.total
if psi.total > maxMsgsPer {
needAttention.Insert(subj, psi.total)
if psi.fblk < fblk {
fblk = psi.fblk
}
if psi.lblk > lblk {
lblk = psi.lblk
}
}
return true
})
// We had an issue with a use case where psim (and hence fss) were correct but idx was not and was not properly being caught.
// So do a quick sanity check here. If we detect a skew do a rebuild then re-check.
if numMsgs != fs.state.Msgs {
fs.warn("Detected skew in subject-based total (%d) vs raw total (%d), rebuilding", numMsgs, fs.state.Msgs)
// Clear any global subject state.
fs.psim, fs.tsl = fs.psim.Empty(), 0
for _, mb := range fs.blks {
ld, _, err := mb.rebuildState()
if err != nil && ld != nil {
fs.addLostData(ld)
}
fs.populateGlobalPerSubjectInfo(mb)
}
// Rebuild fs state too.
fs.rebuildStateLocked(nil)
// Need to redo blocks that need attention.
needAttention.Empty()
fblk, lblk = uint32(math.MaxUint32), uint32(0)
fs.psim.IterFast(func(subj []byte, psi *psi) bool {
if psi.total > maxMsgsPer {
needAttention.Insert(subj, psi.total)
if psi.fblk < fblk {
fblk = psi.fblk
}
if psi.lblk > lblk {
lblk = psi.lblk
}
}
return true
})
}
// If nothing to do then stop.
if fblk == math.MaxUint32 {
return
}
// Collect all the msgBlks we alter.
blks := make(map[*msgBlock]struct{})
// For re-use below.
var sm StoreMsg
var fss *stree.SubjectTree[*SimpleState]
for i := fblk; i <= lblk; i++ {
mb := fs.bim[i]
if mb == nil {
continue
}
mb.mu.Lock()
mb.ensurePerSubjectInfoLoaded()
// It isn't safe to intersect mb.fss directly, because removeMsgViaLimits modifies it
// during the iteration, which can cause us to miss keys. We won't copy the entire
// SimpleState structs though but rather just take pointers for speed.
fss = fss.Empty()
mb.fss.IterFast(func(subject []byte, val *SimpleState) bool {
fss.Insert(subject, val)
return true
})
mb.mu.Unlock()
stree.LazyIntersect(needAttention, fss, func(subj []byte, total *uint64, ssptr **SimpleState) {
if ssptr == nil || total == nil {
return
}
ss := *ssptr
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
mb.mu.Lock()
mb.recalculateForSubj(bytesToString(subj), ss)
mb.mu.Unlock()
}
for first := ss.First; *total > maxMsgsPer && first <= ss.Last; {
m, _, err := mb.firstMatching(bytesToString(subj), false, first, &sm)
if err != nil {
break
}
first = m.seq + 1
if removed, _ := fs.removeMsgViaLimits(m.seq); removed {
blks[mb] = struct{}{}
*total--
}
}
})
}
// Expire the cache if we can.
for mb := range blks {
mb.mu.Lock()
if mb.msgs > 0 {
mb.tryForceExpireCacheLocked()
}
mb.mu.Unlock()
}
}
// Lock should be held.
func (fs *fileStore) deleteFirstMsg() (bool, error) {
return fs.removeMsgViaLimits(fs.state.FirstSeq)
}
// If we remove via limits that can always be recovered on a restart we
// do not force the system to update the index file.
// Lock should be held.
func (fs *fileStore) removeMsgViaLimits(seq uint64) (bool, error) {
return fs.removeMsg(seq, false, true, false)
}
// RemoveMsg will remove the message from this store.
// Will return the number of bytes removed.
func (fs *fileStore) RemoveMsg(seq uint64) (bool, error) {
return fs.removeMsg(seq, false, false, true)
}
func (fs *fileStore) EraseMsg(seq uint64) (bool, error) {
return fs.removeMsg(seq, true, false, true)
}
// Convenience function to remove per subject tracking at the filestore level.
// Lock should be held.
func (fs *fileStore) removePerSubject(subj string) uint64 {
if len(subj) == 0 || fs.psim == nil {
return 0
}
// We do not update sense of fblk here but will do so when we resolve during lookup.
bsubj := stringToBytes(subj)
if info, ok := fs.psim.Find(bsubj); ok {
info.total--
if info.total == 1 {
info.fblk = info.lblk
} else if info.total == 0 {
if _, ok = fs.psim.Delete(bsubj); ok {
fs.tsl -= len(subj)
return 0
}
}
return info.total
}
return 0
}
// Remove a message, optionally rewriting the mb file.
func (fs *fileStore) removeMsg(seq uint64, secure, viaLimits, needFSLock bool) (bool, error) {
if seq == 0 {
return false, ErrStoreMsgNotFound
}
fsLock := func() {
if needFSLock {
fs.mu.Lock()
}
}
fsUnlock := func() {
if needFSLock {
fs.mu.Unlock()
}
}
fsLock()
if fs.isClosed() {
fsUnlock()
return false, ErrStoreClosed
}
// If in encrypted mode negate secure rewrite here.
if secure && fs.prf != nil {
secure = false
}
mb := fs.selectMsgBlock(seq)
if mb == nil {
var err = ErrStoreEOF
if seq <= fs.state.LastSeq {
err = ErrStoreMsgNotFound
}
fsUnlock()
return false, err
}
mb.mu.Lock()
// See if we are closed or the sequence number is still relevant or if we know its deleted.
if mb.closed || seq < atomic.LoadUint64(&mb.first.seq) || mb.dmap.Exists(seq) {
mb.mu.Unlock()
fsUnlock()
return false, nil
}
fifo := seq == atomic.LoadUint64(&mb.first.seq)
isLastBlock := mb == fs.lmb
isEmpty := mb.msgs == 1 // ... about to be zero though.
// We used to not have to load in the messages except with callbacks or the filtered subject state (which is now always on).
// Now just load regardless.
// TODO(dlc) - Figure out a way not to have to load it in, we need subject tracking outside main data block.
if mb.cacheNotLoaded() {
if err := mb.loadMsgsWithLock(); err != nil {
mb.mu.Unlock()
fsUnlock()
return false, err
}
}
var smv StoreMsg
var sm *StoreMsg
var err error
if secure {
// For a secure erase we can't use NoCopy, as eraseMsg will overwrite the
// cache and we won't be able to access sm.subj etc anymore later on.
sm, err = mb.cacheLookup(seq, &smv)
} else {
// For a non-secure erase it's fine to use NoCopy, as the cache won't change
// from underneath us.
sm, err = mb.cacheLookupNoCopy(seq, &smv)
}
if err != nil {
mb.mu.Unlock()
fsUnlock()
// Mimic err behavior from above check to dmap. No error returned if already removed.
if err == errDeletedMsg {
err = nil
}
return false, err
}
// Check if we need to write a deleted record tombstone.
// This is for user initiated removes or to hold the first seq
// when the last block is empty.
// If not via limits and not empty (empty writes tombstone below if last) write tombstone.
if !viaLimits && !isEmpty && sm != nil {
mb.mu.Unlock() // Only safe way to checkLastBlock is to unlock here...
lmb, err := fs.checkLastBlock(emptyRecordLen)
if err != nil {
fsUnlock()
return false, err
}
if err := lmb.writeTombstone(sm.seq, sm.ts); err != nil {
fsUnlock()
return false, err
}
mb.mu.Lock() // We'll need the lock back to carry on safely.
}
// Grab size
msz := fileStoreMsgSize(sm.subj, sm.hdr, sm.msg)
// Set cache timestamp for last remove.
mb.lrts = ats.AccessTime()
// Must always perform the erase, even if the block is empty as it could contain tombstones.
if secure {
// Grab record info, but use the pre-computed record length.
ri, _, _, err := mb.slotInfo(int(seq - mb.cache.fseq))
if err != nil {
mb.mu.Unlock()
fsUnlock()
return false, err
}
if err := mb.eraseMsg(seq, int(ri), int(msz), isLastBlock); err != nil {
mb.mu.Unlock()
fsUnlock()
return false, err
}
}
// Global stats
if fs.state.Msgs > 0 {
fs.state.Msgs--
}
if msz < fs.state.Bytes {
fs.state.Bytes -= msz
} else {
fs.state.Bytes = 0
}
// Now local mb updates.
if mb.msgs > 0 {
mb.msgs--
}
if msz < mb.bytes {
mb.bytes -= msz
} else {
mb.bytes = 0
}
// Allow us to check compaction again.
mb.noCompact = false
// Mark as dirty for stream state.
fs.dirty++
// If we are tracking subjects here make sure we update that accounting.
mb.ensurePerSubjectInfoLoaded()
// If we are tracking multiple subjects here make sure we update that accounting.
mb.removeSeqPerSubject(sm.subj, seq)
fs.removePerSubject(sm.subj)
if fs.ttls != nil {
if ttl, err := getMessageTTL(sm.hdr); err == nil {
expires := time.Duration(sm.ts) + (time.Second * time.Duration(ttl))
fs.ttls.Remove(seq, int64(expires))
}
}
if fifo {
mb.selectNextFirst()
if !isEmpty {
// Can update this one in place.
if seq == fs.state.FirstSeq {
fs.state.FirstSeq = atomic.LoadUint64(&mb.first.seq) // new one.
if mb.first.ts == 0 {
fs.state.FirstTime = time.Time{}
} else {
fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC()
}
}
}
} else if !isEmpty {
// Out of order delete.
mb.dmap.Insert(seq)
// Make simple check here similar to Compact(). If we can save 50% and over a certain threshold do inline.
// All other more thorough cleanup will happen in syncBlocks logic.
// Note that we do not have to store empty records for the deleted, so don't use to calculate.
// TODO(dlc) - This should not be inline, should kick the sync routine.
if !isLastBlock && mb.shouldCompactInline() {
mb.compact()
}
}
if secure {
if ld, _ := mb.flushPendingMsgsLocked(); ld != nil {
// We have the mb lock here, this needs the mb locks so do in its own go routine.
go fs.rebuildState(ld)
}
}
// If empty remove this block and check if we need to update first sequence.
// We will write a tombstone at the end.
var firstSeqNeedsUpdate bool
if isEmpty {
// This writes tombstone iff mb == lmb, so no need to do above.
fs.removeMsgBlock(mb)
firstSeqNeedsUpdate = seq == fs.state.FirstSeq
}
mb.mu.Unlock()
// If we emptied the current message block and the seq was state.FirstSeq
// then we need to jump message blocks. We will also write the index so
// we don't lose track of the first sequence.
if firstSeqNeedsUpdate {
fs.selectNextFirst()
}
if cb := fs.scb; cb != nil {
// If we have a callback registered we need to release lock regardless since cb might need it to lookup msg, etc.
fs.mu.Unlock()
// Storage updates.
delta := int64(msz)
cb(-1, -delta, seq, sm.subj)
if !needFSLock {
fs.mu.Lock()
}
} else if needFSLock {
// We acquired it so release it.
fs.mu.Unlock()
}
return true, nil
}
// Tests whether we should try to compact this block while inline removing msgs.
// We will want rbytes to be over the minimum and have a 2x potential savings.
// If we compacted before but rbytes didn't improve much, guard against constantly compacting.
// Lock should be held.
func (mb *msgBlock) shouldCompactInline() bool {
return mb.rbytes > compactMinimum && mb.bytes*2 < mb.rbytes && (mb.cbytes == 0 || mb.bytes*2 < mb.cbytes)
}
// Tests whether we should try to compact this block while running periodic sync.
// We will want rbytes to be over the minimum and have a 2x potential savings.
// Ignores 2MB minimum.
// Lock should be held.
func (mb *msgBlock) shouldCompactSync() bool {
return mb.bytes*2 < mb.rbytes && !mb.noCompact
}
// This will compact and rewrite this block. This version will not process any tombstone cleanup.
// Write lock needs to be held.
func (mb *msgBlock) compact() {
mb.compactWithFloor(0)
}
// This will compact and rewrite this block. This should only be called when we know we want to rewrite this block.
// This should not be called on the lmb since we will prune tail deleted messages which could cause issues with
// writing new messages. We will silently bail on any issues with the underlying block and let someone else detect.
// if fseq > 0 we will attempt to cleanup stale tombstones.
// Write lock needs to be held.
func (mb *msgBlock) compactWithFloor(floor uint64) {
wasLoaded := mb.cacheAlreadyLoaded()
if !wasLoaded {
if err := mb.loadMsgsWithLock(); err != nil {
return
}
}
buf := mb.cache.buf
nbuf := getMsgBlockBuf(len(buf))
// Recycle our nbuf when we are done.
defer recycleMsgBlockBuf(nbuf)
var le = binary.LittleEndian
var firstSet bool
var last uint64
var msgs uint64
fseq := atomic.LoadUint64(&mb.first.seq)
lseq := atomic.LoadUint64(&mb.last.seq)
isDeleted := func(seq uint64) bool {
return seq == 0 || seq&ebit != 0 || mb.dmap.Exists(seq) || seq < fseq
}
for index, lbuf := uint32(0), uint32(len(buf)); index < lbuf; {
if index+msgHdrSize > lbuf {
return
}
hdr := buf[index : index+msgHdrSize]
rl, slen := le.Uint32(hdr[0:]), int(le.Uint16(hdr[20:]))
// Clear any headers bit that could be set.
rl &^= hbit
dlen := int(rl) - msgHdrSize
// Do some quick sanity checks here.
if dlen < 0 || slen > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh {
return
}
// Only need to process non-deleted messages.
seq := le.Uint64(hdr[4:])
ts := int64(le.Uint64(hdr[12:]))
if !isDeleted(seq) {
// Check for tombstones.
if seq&tbit != 0 {
seq = seq &^ tbit
// If this entry is for a lower seq than ours then keep around.
// We also check that it is greater than our floor. Floor is zero on normal
// calls to compact.
if seq < fseq && seq >= floor {
nbuf = append(nbuf, buf[index:index+rl]...)
}
} else {
// Normal message here.
msgs++
nbuf = append(nbuf, buf[index:index+rl]...)
if !firstSet {
firstSet = true
atomic.StoreUint64(&mb.first.seq, seq)
}
if seq >= last {
last = seq
atomic.StoreUint64(&mb.last.seq, last)
mb.last.ts = ts
}
}
}
// Advance to next record.
index += rl
}
// Handle compression
if mb.cmp != NoCompression && len(nbuf) > 0 {
cbuf, err := mb.cmp.Compress(nbuf)
if err != nil {
return
}
meta := &CompressionInfo{
Algorithm: mb.cmp,
OriginalSize: uint64(len(nbuf)),
}
nbuf = append(meta.MarshalMetadata(), cbuf...)
}
// Check for encryption.
if mb.bek != nil && len(nbuf) > 0 {
// Recreate to reset counter.
rbek, err := genBlockEncryptionKey(mb.fs.fcfg.Cipher, mb.seed, mb.nonce)
if err != nil {
return
}
rbek.XORKeyStream(nbuf, nbuf)
}
// Close FDs first.
mb.closeFDsLocked()
// We will write to a new file and mv/rename it in case of failure.
mfn := filepath.Join(mb.fs.fcfg.StoreDir, msgDir, fmt.Sprintf(newScan, mb.index))
<-dios
err := os.WriteFile(mfn, nbuf, defaultFilePerms)
dios <- struct{}{}
if err != nil {
os.Remove(mfn)
return
}
if err := os.Rename(mfn, mb.mfn); err != nil {
os.Remove(mfn)
return
}
// Make sure to sync
mb.needSync = true
// Capture the updated rbytes.
if rbytes := uint64(len(nbuf)); rbytes == mb.rbytes {
// No change, so set our noCompact bool here to avoid attempting to continually compress in syncBlocks.
mb.noCompact = true
} else {
mb.rbytes = rbytes
}
mb.cbytes = mb.bytes
// Remove any seqs from the beginning of the blk.
for seq, nfseq := fseq, atomic.LoadUint64(&mb.first.seq); seq < nfseq; seq++ {
mb.dmap.Delete(seq)
}
// Remove any seqs from the ending of the blk.
for seq, nlseq := lseq, atomic.LoadUint64(&mb.last.seq); seq > nlseq; seq-- {
mb.dmap.Delete(seq)
}
// If the block itself has no messages anymore (could still contain tombstones though),
// then we need to account for that by resetting the last sequence and timestamp.
if msgs == 0 {
atomic.StoreUint64(&mb.last.seq, fseq-1)
mb.last.ts = 0
mb.dmap.Empty()
}
// Make sure we clear the cache since no longer valid.
mb.clearCacheAndOffset()
// If we entered with the msgs loaded make sure to reload them.
if wasLoaded {
mb.loadMsgsWithLock()
}
}
// Grab info from a slot.
// Lock should be held.
func (mb *msgBlock) slotInfo(slot int) (uint32, uint32, bool, error) {
if slot < 0 || mb.cache == nil || slot >= len(mb.cache.idx) {
return 0, 0, false, errPartialCache
}
bi := mb.cache.idx[slot]
ri, hashChecked := (bi &^ cbit), (bi&cbit) != 0
// If this is a deleted slot return here.
if bi == dbit {
return 0, 0, false, errDeletedMsg
}
// Determine record length
var rl uint32
if slot >= len(mb.cache.idx) {
rl = mb.cache.lrl
} else {
// Need to account for dbit markers in idx.
// So we will walk until we find valid idx slot to calculate rl.
for i := 1; slot+i < len(mb.cache.idx); i++ {
ni := mb.cache.idx[slot+i] &^ cbit
if ni == dbit {
continue
}
rl = ni - ri
break
}
// check if we had all trailing dbits.
// If so use len of cache buf minus ri.
if rl == 0 {
rl = uint32(len(mb.cache.buf)) - ri
}
}
if rl < msgHdrSize {
return 0, 0, false, errBadMsg{mb.mfn, fmt.Sprintf("length too short for slot %d", slot)}
}
return uint32(ri), rl, hashChecked, nil
}
func (fs *fileStore) isClosed() bool {
return fs.closed.Load()
}
// Will spin up our flush loop.
func (mb *msgBlock) spinUpFlushLoop() {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.spinUpFlushLoopLocked()
}
// Will spin up our flush loop.
// Lock should be held.
func (mb *msgBlock) spinUpFlushLoopLocked() {
// Are we already running or closed?
if mb.flusher || mb.closed {
return
}
mb.flusher = true
mb.fch = make(chan struct{}, 1)
mb.qch = make(chan struct{})
fch, qch := mb.fch, mb.qch
go mb.flushLoop(fch, qch)
}
// Raw low level kicker for flush loops.
func kickFlusher(fch chan struct{}) {
if fch != nil {
select {
case fch <- struct{}{}:
default:
}
}
}
// Kick flusher for this message block.
func (mb *msgBlock) kickFlusher() {
mb.mu.RLock()
defer mb.mu.RUnlock()
kickFlusher(mb.fch)
}
func (mb *msgBlock) setInFlusher() {
mb.mu.Lock()
mb.flusher = true
mb.mu.Unlock()
}
func (mb *msgBlock) clearInFlusher() {
mb.mu.Lock()
mb.flusher = false
mb.mu.Unlock()
}
// flushLoop watches for messages, index info, or recently closed msg block updates.
func (mb *msgBlock) flushLoop(fch, qch chan struct{}) {
mb.setInFlusher()
defer mb.clearInFlusher()
for {
select {
case <-fch:
// If we have pending messages process them first.
if waiting := mb.pendingWriteSize(); waiting != 0 {
ts := 1 * time.Millisecond
var waited time.Duration
for waiting < coalesceMinimum {
time.Sleep(ts)
select {
case <-qch:
return
default:
}
newWaiting := mb.pendingWriteSize()
if waited = waited + ts; waited > maxFlushWait || newWaiting <= waiting {
break
}
waiting = newWaiting
ts *= 2
}
mb.flushPendingMsgs()
// Check if we are no longer the last message block. If we are
// not we can close FDs and exit.
mb.fs.mu.RLock()
notLast := mb != mb.fs.lmb
mb.fs.mu.RUnlock()
if notLast {
if err := mb.closeFDs(); err == nil {
return
}
}
}
case <-qch:
return
}
}
}
// Lock should be held.
func (mb *msgBlock) eraseMsg(seq uint64, ri, rl int, isLastBlock bool) error {
var le = binary.LittleEndian
var hdr [msgHdrSize]byte
le.PutUint32(hdr[0:], uint32(rl))
le.PutUint64(hdr[4:], seq|ebit)
le.PutUint64(hdr[12:], 0)
le.PutUint16(hdr[20:], 0)
// Randomize record
data := make([]byte, rl-emptyRecordLen)
if n, err := rand.Read(data); err != nil {
return err
} else if n != len(data) {
return fmt.Errorf("not enough overwrite bytes read (%d != %d)", n, len(data))
}
// Now write to underlying buffer.
var b bytes.Buffer
b.Write(hdr[:])
b.Write(data)
// Calculate hash.
mb.hh.Reset()
mb.hh.Write(hdr[4:20])
mb.hh.Write(data)
var hb [highwayhash.Size64]byte
checksum := mb.hh.Sum(hb[:0])
// Write to msg record.
b.Write(checksum)
// Update both cache and disk.
nbytes := b.Bytes()
// Cache
if ri >= mb.cache.off {
li := ri - mb.cache.off
buf := mb.cache.buf[li : li+rl]
copy(buf, nbytes)
}
// Disk
if mb.cache.off+mb.cache.wp > ri {
if err := mb.atomicOverwriteFile(mb.cache.buf, !isLastBlock); err != nil {
return err
}
}
return nil
}
// Truncate this message block to the tseq and ts.
// Lock should be held.
func (mb *msgBlock) truncate(tseq uint64, ts int64) (nmsgs, nbytes uint64, err error) {
// Make sure we are loaded to process messages etc.
if err := mb.loadMsgsWithLock(); err != nil {
return 0, 0, err
}
// Calculate new eof using slot info from our new last sm.
ri, rl, _, err := mb.slotInfo(int(tseq - mb.cache.fseq))
if err != nil {
return 0, 0, err
}
// Calculate new eof.
eof := int64(ri + rl)
var purged, bytes uint64
checkDmap := mb.dmap.Size() > 0
var smv StoreMsg
for seq := atomic.LoadUint64(&mb.last.seq); seq > tseq; seq-- {
if checkDmap {
if mb.dmap.Exists(seq) {
// Delete and skip to next.
mb.dmap.Delete(seq)
checkDmap = !mb.dmap.IsEmpty()
continue
}
}
// We should have a valid msg to calculate removal stats.
if m, err := mb.cacheLookupNoCopy(seq, &smv); err == nil {
if mb.msgs > 0 {
rl := fileStoreMsgSize(m.subj, m.hdr, m.msg)
mb.msgs--
if rl > mb.bytes {
rl = mb.bytes
}
mb.bytes -= rl
mb.rbytes -= rl
// For return accounting.
purged++
bytes += uint64(rl)
}
}
}
// If the block is compressed then we have to load it into memory
// and decompress it, truncate it and then write it back out.
// Otherwise, truncate the file itself and close the descriptor.
if mb.cmp != NoCompression {
buf, err := mb.loadBlock(nil)
if err != nil {
return 0, 0, fmt.Errorf("failed to load block from disk: %w", err)
}
if err = mb.encryptOrDecryptIfNeeded(buf); err != nil {
return 0, 0, err
}
if buf, err = mb.decompressIfNeeded(buf); err != nil {
return 0, 0, fmt.Errorf("failed to decompress block: %w", err)
}
buf = buf[:eof]
copy(mb.lchk[0:], buf[:len(buf)-checksumSize])
// We did decompress but don't recompress the truncated buffer here since we're the last block
// and would otherwise have compressed data and allow to write uncompressed data in the same block.
if err = mb.atomicOverwriteFile(buf, false); err != nil {
return 0, 0, err
}
} else if mb.mfd != nil {
mb.mfd.Truncate(eof)
mb.mfd.Sync()
// Update our checksum.
var lchk [8]byte
mb.mfd.ReadAt(lchk[:], eof-8)
copy(mb.lchk[0:], lchk[:])
} else {
return 0, 0, fmt.Errorf("failed to truncate msg block %d, file not open", mb.index)
}
// Update our last msg.
atomic.StoreUint64(&mb.last.seq, tseq)
mb.last.ts = ts
// Clear our cache.
mb.clearCacheAndOffset()
// Redo per subject info for this block.
mb.resetPerSubjectInfo()
// Load msgs again.
mb.loadMsgsWithLock()
return purged, bytes, nil
}
// Helper to determine if the mb is empty.
func (mb *msgBlock) isEmpty() bool {
return atomic.LoadUint64(&mb.first.seq) > atomic.LoadUint64(&mb.last.seq)
}
// Lock should be held.
func (mb *msgBlock) selectNextFirst() {
var seq uint64
fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
for seq = fseq + 1; seq <= lseq; seq++ {
if mb.dmap.Exists(seq) {
// We will move past this so we can delete the entry.
mb.dmap.Delete(seq)
} else {
break
}
}
// Set new first sequence.
atomic.StoreUint64(&mb.first.seq, seq)
// Check if we are empty..
if seq > lseq {
mb.first.ts = 0
return
}
// Need to get the timestamp.
// We will try the cache direct and fallback if needed.
var smv StoreMsg
sm, _ := mb.cacheLookupNoCopy(seq, &smv)
if sm == nil {
// Slow path, need to unlock.
mb.mu.Unlock()
sm, _, _ = mb.fetchMsgNoCopy(seq, &smv)
mb.mu.Lock()
}
if sm != nil {
mb.first.ts = sm.ts
} else {
mb.first.ts = 0
}
}
// Select the next FirstSeq
// Also cleans up empty blocks at the start only containing tombstones.
// Lock should be held.
func (fs *fileStore) selectNextFirst() {
if len(fs.blks) > 0 {
for len(fs.blks) > 1 {
mb := fs.blks[0]
mb.mu.Lock()
empty := mb.msgs == 0
if !empty {
mb.mu.Unlock()
break
}
fs.forceRemoveMsgBlock(mb)
mb.mu.Unlock()
}
mb := fs.blks[0]
mb.mu.RLock()
fs.state.FirstSeq = atomic.LoadUint64(&mb.first.seq)
if mb.first.ts == 0 {
fs.state.FirstTime = time.Time{}
} else {
fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC()
}
mb.mu.RUnlock()
} else {
// Could not find anything, so treat like purge
fs.state.FirstSeq = fs.state.LastSeq + 1
fs.state.FirstTime = time.Time{}
}
// Mark first as moved. Plays into tombstone cleanup for syncBlocks.
fs.firstMoved = true
}
// Lock should be held.
func (mb *msgBlock) resetCacheExpireTimer(td time.Duration) {
if td == 0 {
td = mb.cexp + 100*time.Millisecond
}
if mb.ctmr == nil {
mb.ctmr = time.AfterFunc(td, mb.expireCache)
} else {
mb.ctmr.Reset(td)
}
}
// Lock should be held.
func (mb *msgBlock) startCacheExpireTimer() {
mb.resetCacheExpireTimer(0)
}
// Used when we load in a message block.
// Lock should be held.
func (mb *msgBlock) clearCacheAndOffset() {
// Reset linear scan tracker.
mb.llseq = 0
if mb.cache != nil {
mb.cache.off = 0
mb.cache.wp = 0
}
mb.clearCache()
}
// Lock should be held.
func (mb *msgBlock) clearCache() {
if mb.ctmr != nil {
tsla := mb.sinceLastActivity()
if mb.fss == nil || tsla > mb.fexp {
// Force
mb.fss = nil
mb.ctmr.Stop()
mb.ctmr = nil
} else {
mb.resetCacheExpireTimer(mb.fexp - tsla)
}
}
if mb.cache == nil {
return
}
buf := mb.cache.buf
if mb.cache.off == 0 {
mb.cache = nil
} else {
// Clear msgs and index.
mb.cache.buf = nil
mb.cache.idx = nil
mb.cache.wp = 0
}
recycleMsgBlockBuf(buf)
}
// Called to possibly expire a message block cache.
func (mb *msgBlock) expireCache() {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.expireCacheLocked()
}
func (mb *msgBlock) tryForceExpireCache() {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.tryForceExpireCacheLocked()
}
// We will attempt to force expire this by temporarily clearing the last load time.
func (mb *msgBlock) tryForceExpireCacheLocked() {
llts := mb.llts
mb.llts = 0
mb.expireCacheLocked()
mb.llts = llts
}
// This is for expiration of the write cache, which will be partial with fip.
// So we want to bypass the Pools here.
// Lock should be held.
func (mb *msgBlock) tryExpireWriteCache() []byte {
if mb.cache == nil {
return nil
}
lwts, buf, llts, nra := mb.lwts, mb.cache.buf, mb.llts, mb.cache.nra
mb.lwts, mb.cache.nra = 0, true
mb.expireCacheLocked()
mb.lwts = lwts
if mb.cache != nil {
mb.cache.nra = nra
}
// We could check for a certain time since last load, but to be safe just reuse if no loads at all.
if llts == 0 && (mb.cache == nil || mb.cache.buf == nil) {
// Clear last write time since we now are about to move on to a new lmb.
mb.lwts = 0
return buf[:0]
}
return nil
}
// Lock should be held.
func (mb *msgBlock) expireCacheLocked() {
if mb.cache == nil && mb.fss == nil {
if mb.ctmr != nil {
mb.ctmr.Stop()
mb.ctmr = nil
}
return
}
// Can't expire if we still have pending.
if mb.cache != nil && len(mb.cache.buf)-int(mb.cache.wp) > 0 {
mb.resetCacheExpireTimer(mb.cexp)
return
}
// Grab timestamp to compare.
tns := ats.AccessTime()
// For the core buffer of messages, we care about reads and writes, but not removes.
bufts := mb.llts
if mb.lwts > bufts {
bufts = mb.lwts
}
// Check for activity on the cache that would prevent us from expiring.
if tns-bufts <= int64(mb.cexp) {
mb.resetCacheExpireTimer(mb.cexp - time.Duration(tns-bufts))
return
}
// If we are here we will at least expire the core msg buffer.
// We need to capture offset in case we do a write next before a full load.
if mb.cache != nil {
mb.cache.off += len(mb.cache.buf)
if !mb.cache.nra {
recycleMsgBlockBuf(mb.cache.buf)
}
mb.cache.buf = nil
mb.cache.wp = 0
}
// Check if we can clear out our idx unless under force expire.
// fss we keep longer and expire under sync timer checks.
mb.clearCache()
}
func (fs *fileStore) startAgeChk() {
if fs.ageChk != nil {
return
}
if fs.cfg.MaxAge != 0 || fs.ttls != nil {
fs.ageChk = time.AfterFunc(fs.cfg.MaxAge, fs.expireMsgs)
}
}
// Lock should be held.
func (fs *fileStore) resetAgeChk(delta int64) {
// If we're already expiring messages, it will make sure to reset.
// Don't trigger again, as that could result in many expire goroutines.
if fs.ageChkRun {
return
}
var next int64 = math.MaxInt64
if fs.ttls != nil {
next = fs.ttls.GetNextExpiration(next)
}
// If there's no MaxAge and there's nothing waiting to be expired then
// don't bother continuing. The next storeRawMsg() will wake us up if
// needs be.
if fs.cfg.MaxAge <= 0 && next == math.MaxInt64 {
clearTimer(&fs.ageChk)
return
}
// Check to see if we should be firing sooner than MaxAge for an expiring TTL.
fireIn := fs.cfg.MaxAge
// If delta for next-to-expire message is unset, but we still have messages to remove.
// Assume messages are removed through proposals, and we need to speed up subsequent age check.
if delta == 0 && fs.state.Msgs > 0 {
if until := 2 * time.Second; until < fireIn {
fireIn = until
}
}
if next < math.MaxInt64 {
// Looks like there's a next expiration, use it either if there's no
// MaxAge set or if it looks to be sooner than MaxAge is.
if until := time.Until(time.Unix(0, next)); fireIn == 0 || until < fireIn {
fireIn = until
}
}
// If not then look at the delta provided (usually gap to next age expiry).
if delta > 0 {
if fireIn == 0 || time.Duration(delta) < fireIn {
fireIn = time.Duration(delta)
}
}
// Make sure we aren't firing too often either way, otherwise we can
// negatively impact stream ingest performance.
if fireIn < 250*time.Millisecond {
fireIn = 250 * time.Millisecond
}
// If we want to kick the timer to run later than what was assigned before, don't reset it.
// Otherwise, we could get in a situation where the timer is continuously reset, and it never runs.
expires := ats.AccessTime() + fireIn.Nanoseconds()
if fs.ageChkTime > 0 && expires > fs.ageChkTime {
return
}
fs.ageChkTime = expires
if fs.ageChk != nil {
fs.ageChk.Reset(fireIn)
} else {
fs.ageChk = time.AfterFunc(fireIn, fs.expireMsgs)
}
}
// Lock should be held.
func (fs *fileStore) cancelAgeChk() {
if fs.ageChk != nil {
fs.ageChk.Stop()
fs.ageChk = nil
fs.ageChkTime = 0
}
}
// Will expire msgs that are too old.
func (fs *fileStore) expireMsgs() {
// We need to delete one by one here and can not optimize for the time being.
// Reason is that we need more information to adjust ack pending in consumers.
var smv StoreMsg
var sm *StoreMsg
fs.mu.Lock()
maxAge := int64(fs.cfg.MaxAge)
minAge := ats.AccessTime() - maxAge
rmcb := fs.rmcb
sdmcb := fs.sdmcb
sdmTTL := int64(fs.cfg.SubjectDeleteMarkerTTL.Seconds())
sdmEnabled := sdmTTL > 0
// If SDM is enabled, but handlers aren't set up yet. Try again later.
if sdmEnabled && (rmcb == nil || sdmcb == nil) {
fs.resetAgeChk(0)
fs.mu.Unlock()
return
}
fs.ageChkRun = true
fs.mu.Unlock()
if maxAge > 0 {
var seq uint64
for sm, seq, _ = fs.LoadNextMsg(fwcs, true, 0, &smv); sm != nil && sm.ts <= minAge; sm, seq, _ = fs.LoadNextMsg(fwcs, true, seq+1, &smv) {
if len(sm.hdr) > 0 {
if ttl, err := getMessageTTL(sm.hdr); err == nil && ttl < 0 {
// The message has a negative TTL, therefore it must "never expire".
minAge = ats.AccessTime() - maxAge
continue
}
}
// Remove the message and then, if LimitsTTL is enabled, try and work out
// if it was the last message of that particular subject that we just deleted.
if sdmEnabled {
if last, ok := fs.shouldProcessSdm(seq, sm.subj); ok {
sdm := last && !isSubjectDeleteMarker(sm.hdr)
fs.handleRemovalOrSdm(seq, sm.subj, sdm, sdmTTL)
}
} else {
fs.mu.Lock()
fs.removeMsgViaLimits(sm.seq)
fs.mu.Unlock()
}
// Recalculate in case we are expiring a bunch.
minAge = ats.AccessTime() - maxAge
}
}
var ageDelta int64
if sm != nil {
ageDelta = sm.ts - minAge
}
fs.mu.Lock()
defer fs.mu.Unlock()
// TODO: Not great that we're holding the lock here, but the timed hash wheel isn't thread-safe.
nextTTL := int64(math.MaxInt64)
var rmSeqs []thw.HashWheelEntry
if fs.ttls != nil {
fs.ttls.ExpireTasks(func(seq uint64, ts int64) bool {
rmSeqs = append(rmSeqs, thw.HashWheelEntry{Seq: seq, Expires: ts})
// We might need to remove messages out of band, those can fail, and we can be shutdown halfway
// through so don't remove from THW just yet.
return false
})
nextTTL = fs.ttls.GetNextExpiration(math.MaxInt64)
}
// Remove messages collected by THW.
if !sdmEnabled {
for _, rm := range rmSeqs {
fs.removeMsg(rm.Seq, false, false, false)
}
} else {
// THW is unordered, so must sort by sequence and must not be holding the lock.
fs.mu.Unlock()
slices.SortFunc(rmSeqs, func(a, b thw.HashWheelEntry) int {
if a.Seq == b.Seq {
return 0
} else if a.Seq < b.Seq {
return -1
} else {
return 1
}
})
for _, rm := range rmSeqs {
// Need to grab subject for the specified sequence if for SDM, and check
// if the message hasn't been removed in the meantime.
// We need to grab the message and check if we should process SDM while holding the lock,
// otherwise we can race if a deletion of this message is in progress.
fs.mu.Lock()
sm, _ = fs.msgForSeqLocked(rm.Seq, &smv, false)
if sm == nil {
fs.ttls.Remove(rm.Seq, rm.Expires)
fs.mu.Unlock()
continue
}
last, ok := fs.shouldProcessSdmLocked(rm.Seq, sm.subj)
fs.mu.Unlock()
if ok {
sdm := last && !isSubjectDeleteMarker(sm.hdr)
fs.handleRemovalOrSdm(rm.Seq, sm.subj, sdm, sdmTTL)
}
}
fs.mu.Lock()
}
// Only cancel if no message left, not on potential lookup error that would result in sm == nil.
fs.ageChkRun, fs.ageChkTime = false, 0
if fs.state.Msgs == 0 && nextTTL == math.MaxInt64 {
fs.cancelAgeChk()
} else {
fs.resetAgeChk(ageDelta)
}
}
func (fs *fileStore) shouldProcessSdm(seq uint64, subj string) (bool, bool) {
fs.mu.Lock()
defer fs.mu.Unlock()
return fs.shouldProcessSdmLocked(seq, subj)
}
// Lock should be held.
func (fs *fileStore) shouldProcessSdmLocked(seq uint64, subj string) (bool, bool) {
if fs.sdm == nil {
fs.sdm = newSDMMeta()
}
if p, ok := fs.sdm.pending[seq]; ok {
// Don't allow more proposals for the same sequence if we already did recently.
if time.Since(time.Unix(0, p.ts)) < 2*time.Second {
return p.last, false
}
// If we're about to use the cached value, and we knew it was last before,
// quickly check that we don't have more remaining messages for the subject now.
// Which means we are not the last anymore and must reset to not remove later data.
if p.last {
msgs := fs.subjectsTotalsLocked(subj)[subj]
numPending := fs.sdm.totals[subj]
if remaining := msgs - numPending; remaining > 0 {
p.last = false
}
}
fs.sdm.pending[seq] = SDMBySeq{p.last, time.Now().UnixNano()}
return p.last, true
}
msgs := fs.subjectsTotalsLocked(subj)[subj]
if msgs == 0 {
return false, true
}
numPending := fs.sdm.totals[subj]
remaining := msgs - numPending
return fs.sdm.trackPending(seq, subj, remaining == 1), true
}
func (fs *fileStore) handleRemovalOrSdm(seq uint64, subj string, sdm bool, sdmTTL int64) {
if sdm {
var _hdr [128]byte
hdr := fmt.Appendf(
_hdr[:0],
"NATS/1.0\r\n%s: %s\r\n%s: %s\r\n%s: %s\r\n\r\n",
JSMarkerReason, JSMarkerReasonMaxAge,
JSMessageTTL, time.Duration(sdmTTL)*time.Second,
JSMsgRollup, JSMsgRollupSubject,
)
msg := &inMsg{
subj: subj,
hdr: hdr,
}
fs.sdmcb(msg)
} else {
fs.rmcb(seq)
}
}
// Lock should be held.
func (fs *fileStore) checkAndFlushAllBlocks() {
for _, mb := range fs.blks {
if mb.pendingWriteSize() > 0 {
// Since fs lock is held need to pull this apart in case we need to rebuild state.
mb.mu.Lock()
ld, _ := mb.flushPendingMsgsLocked()
mb.mu.Unlock()
if ld != nil {
fs.rebuildStateLocked(ld)
}
}
}
}
// This will check all the checksums on messages and report back any sequence numbers with errors.
func (fs *fileStore) checkMsgs() *LostStreamData {
fs.mu.Lock()
defer fs.mu.Unlock()
fs.checkAndFlushAllBlocks()
// Clear any global subject state.
fs.psim, fs.tsl = fs.psim.Empty(), 0
for _, mb := range fs.blks {
// Make sure encryption loaded if needed for the block.
fs.loadEncryptionForMsgBlock(mb)
// FIXME(dlc) - check tombstones here too?
if ld, _, err := mb.rebuildState(); err != nil && ld != nil {
// Rebuild fs state too.
fs.rebuildStateLocked(ld)
}
fs.populateGlobalPerSubjectInfo(mb)
}
return fs.ld
}
// Lock should be held.
func (mb *msgBlock) enableForWriting(fip bool) error {
if mb == nil {
return errNoMsgBlk
}
if mb.mfd != nil {
return nil
}
<-dios
mfd, err := os.OpenFile(mb.mfn, os.O_CREATE|os.O_RDWR, defaultFilePerms)
dios <- struct{}{}
if err != nil {
return fmt.Errorf("error opening msg block file [%q]: %v", mb.mfn, err)
}
mb.mfd = mfd
// Spin up our flusher loop if needed.
if !fip {
mb.spinUpFlushLoopLocked()
}
return nil
}
// Helper function to place a delete tombstone.
func (mb *msgBlock) writeTombstone(seq uint64, ts int64) error {
return mb.writeMsgRecord(emptyRecordLen, seq|tbit, _EMPTY_, nil, nil, ts, true)
}
// Helper function to place a delete tombstone without flush.
// Lock should not be held.
func (mb *msgBlock) writeTombstoneNoFlush(seq uint64, ts int64) error {
mb.mu.Lock()
defer mb.mu.Unlock()
return mb.writeMsgRecordLocked(emptyRecordLen, seq|tbit, _EMPTY_, nil, nil, ts, false, false)
}
// Will write the message record to the underlying message block.
// filestore lock will be held.
func (mb *msgBlock) writeMsgRecord(rl, seq uint64, subj string, mhdr, msg []byte, ts int64, flush bool) error {
mb.mu.Lock()
defer mb.mu.Unlock()
return mb.writeMsgRecordLocked(rl, seq, subj, mhdr, msg, ts, flush, true)
}
// Will write the message record to the underlying message block.
// filestore lock will be held.
// mb lock should be held.
func (mb *msgBlock) writeMsgRecordLocked(rl, seq uint64, subj string, mhdr, msg []byte, ts int64, flush, kick bool) error {
// Enable for writing if our mfd is not open.
if mb.mfd == nil {
if err := mb.enableForWriting(flush && kick); err != nil {
return err
}
}
// Make sure we have a cache setup.
if mb.cache == nil {
mb.setupWriteCache(nil)
}
// Check if we are tracking per subject for our simple state.
// Do this before changing the cache that would trigger a flush pending msgs call
// if we needed to regenerate the per subject info.
// Note that tombstones have no subject so will not trigger here.
if len(subj) > 0 && !mb.noTrack {
if err := mb.ensurePerSubjectInfoLoaded(); err != nil {
return err
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
if ss, ok := mb.fss.Find(stringToBytes(subj)); ok && ss != nil {
ss.Msgs++
ss.Last = seq
ss.lastNeedsUpdate = false
} else {
mb.fss.Insert(stringToBytes(subj), SimpleState{Msgs: 1, First: seq, Last: seq})
}
}
// Indexing
index := len(mb.cache.buf) + int(mb.cache.off)
// Formats
// Format with no header
// total_len(4) sequence(8) timestamp(8) subj_len(2) subj msg hash(8)
// With headers, high bit on total length will be set.
// total_len(4) sequence(8) timestamp(8) subj_len(2) subj hdr_len(4) hdr msg hash(8)
var le = binary.LittleEndian
l := uint32(rl)
hasHeaders := len(mhdr) > 0
if hasHeaders {
l |= hbit
}
// Reserve space for the header on the underlying buffer.
mb.cache.buf = append(mb.cache.buf, make([]byte, msgHdrSize)...)
hdr := mb.cache.buf[len(mb.cache.buf)-msgHdrSize : len(mb.cache.buf)]
le.PutUint32(hdr[0:], l)
le.PutUint64(hdr[4:], seq)
le.PutUint64(hdr[12:], uint64(ts))
le.PutUint16(hdr[20:], uint16(len(subj)))
// Now write to underlying buffer.
mb.cache.buf = append(mb.cache.buf, subj...)
if hasHeaders {
var hlen [4]byte
le.PutUint32(hlen[0:], uint32(len(mhdr)))
mb.cache.buf = append(mb.cache.buf, hlen[:]...)
mb.cache.buf = append(mb.cache.buf, mhdr...)
}
mb.cache.buf = append(mb.cache.buf, msg...)
// Calculate hash.
mb.hh.Reset()
mb.hh.Write(hdr[4:20])
mb.hh.Write(stringToBytes(subj))
if hasHeaders {
mb.hh.Write(mhdr)
}
mb.hh.Write(msg)
checksum := mb.hh.Sum(mb.lchk[:0:highwayhash.Size64])
copy(mb.lchk[0:], checksum)
// Update write through cache.
// Write to msg record.
mb.cache.buf = append(mb.cache.buf, checksum...)
mb.cache.lrl = uint32(rl)
// Set cache timestamp for last store.
mb.lwts = ts
// Only update index and do accounting if not a delete tombstone.
if seq&tbit == 0 {
// Accounting, do this before stripping ebit, it is ebit aware.
mb.updateAccounting(seq, ts, rl)
// Strip ebit if set.
seq = seq &^ ebit
if mb.cache.fseq == 0 {
mb.cache.fseq = seq
}
// Write index
mb.cache.idx = append(mb.cache.idx, uint32(index)|cbit)
} else {
// If the block is empty, still adjust the accounting accordingly.
tseq := seq &^ tbit
if mb.msgs == 0 && tseq > atomic.LoadUint64(&mb.last.seq) {
atomic.StoreUint64(&mb.last.seq, tseq)
mb.last.ts = ts
atomic.StoreUint64(&mb.first.seq, tseq+1)
mb.first.ts = 0
}
// Make sure to account for tombstones in rbytes.
mb.rbytes += rl
}
fch, werr := mb.fch, mb.werr
// If we should be flushing, or had a write error, do so here.
if flush || werr != nil {
ld, err := mb.flushPendingMsgsLocked()
if ld != nil && mb.fs != nil {
// We have the mb lock here, this needs the mb locks so do in its own go routine.
go mb.fs.rebuildState(ld)
}
if err != nil {
return err
}
} else if kick {
// Kick the flusher here.
kickFlusher(fch)
}
return nil
}
// How many bytes pending to be written for this message block.
func (mb *msgBlock) pendingWriteSize() int {
if mb == nil {
return 0
}
mb.mu.RLock()
defer mb.mu.RUnlock()
return mb.pendingWriteSizeLocked()
}
// How many bytes pending to be written for this message block.
func (mb *msgBlock) pendingWriteSizeLocked() int {
if mb == nil {
return 0
}
var pending int
if !mb.closed && mb.mfd != nil && mb.cache != nil {
pending = len(mb.cache.buf) - int(mb.cache.wp)
}
return pending
}
// Try to close our FDs if we can.
func (mb *msgBlock) closeFDs() error {
mb.mu.Lock()
defer mb.mu.Unlock()
return mb.closeFDsLocked()
}
func (mb *msgBlock) closeFDsLocked() error {
if buf, _ := mb.bytesPending(); len(buf) > 0 {
return errPendingData
}
mb.closeFDsLockedNoCheck()
return nil
}
func (mb *msgBlock) closeFDsLockedNoCheck() {
if mb.mfd != nil {
mb.mfd.Close()
mb.mfd = nil
}
}
// bytesPending returns the buffer to be used for writing to the underlying file.
// This marks we are in flush and will return nil if asked again until cleared.
// Lock should be held.
func (mb *msgBlock) bytesPending() ([]byte, error) {
if mb == nil || mb.mfd == nil {
return nil, errNoPending
}
if mb.cache == nil {
return nil, errNoCache
}
if len(mb.cache.buf) <= mb.cache.wp {
return nil, errNoPending
}
buf := mb.cache.buf[mb.cache.wp:]
if len(buf) == 0 {
return nil, errNoPending
}
return buf, nil
}
// Returns the current blkSize including deleted msgs etc.
func (mb *msgBlock) blkSize() uint64 {
if mb == nil {
return 0
}
mb.mu.RLock()
nb := mb.rbytes
mb.mu.RUnlock()
return nb
}
// Update accounting on a write msg.
// Lock should be held.
func (mb *msgBlock) updateAccounting(seq uint64, ts int64, rl uint64) {
isDeleted := seq&ebit != 0
if isDeleted {
seq = seq &^ ebit
}
fseq := atomic.LoadUint64(&mb.first.seq)
if (fseq == 0 || mb.first.ts == 0) && seq >= fseq {
atomic.StoreUint64(&mb.first.seq, seq)
mb.first.ts = ts
}
// Need atomics here for selectMsgBlock speed.
atomic.StoreUint64(&mb.last.seq, seq)
mb.last.ts = ts
mb.rbytes += rl
if !isDeleted {
mb.bytes += rl
mb.msgs++
}
}
// Helper to check last msg block and create new one if too big.
// Lock should be held.
func (fs *fileStore) checkLastBlock(rl uint64) (lmb *msgBlock, err error) {
// Grab our current last message block.
lmb = fs.lmb
rbytes := lmb.blkSize()
if lmb == nil || (rbytes > 0 && rbytes+rl > fs.fcfg.BlockSize) {
if lmb, err = fs.newMsgBlockForWrite(); err != nil {
return nil, err
}
}
return lmb, nil
}
// Lock should be held.
func (fs *fileStore) writeMsgRecord(seq uint64, ts int64, subj string, hdr, msg []byte) (uint64, error) {
// Get size for this message.
rl := fileStoreMsgSize(subj, hdr, msg)
if rl&hbit != 0 || rl > rlBadThresh {
return 0, ErrMsgTooLarge
}
// Grab our current last message block.
mb, err := fs.checkLastBlock(rl)
if err != nil {
return 0, err
}
// Mark as dirty for stream state.
fs.dirty++
// Ask msg block to store in write through cache.
err = mb.writeMsgRecord(rl, seq, subj, hdr, msg, ts, fs.fip)
return rl, err
}
// For writing tombstones to our lmb. This version will enforce maximum block sizes.
// Lock should be held.
func (fs *fileStore) writeTombstone(seq uint64, ts int64) error {
// Grab our current last message block.
lmb, err := fs.checkLastBlock(emptyRecordLen)
if err != nil {
return err
}
return lmb.writeTombstone(seq, ts)
}
// For writing tombstones to our lmb. This version will enforce maximum block sizes.
// This version does not flush contents.
// Lock should be held.
func (fs *fileStore) writeTombstoneNoFlush(seq uint64, ts int64) error {
lmb, err := fs.checkLastBlock(emptyRecordLen)
if err != nil {
return err
}
// Write tombstone without flush or kick.
return lmb.writeTombstoneNoFlush(seq, ts)
}
// Lock should be held.
func (mb *msgBlock) recompressOnDiskIfNeeded() error {
alg := mb.fs.fcfg.Compression
// Open up the file block and read in the entire contents into memory.
// One of two things will happen:
// 1. The block will be compressed already and have a valid metadata
// header, in which case we do nothing.
// 2. The block will be uncompressed, in which case we will compress it
// and then write it back out to disk, re-encrypting if necessary.
<-dios
origBuf, err := os.ReadFile(mb.mfn)
dios <- struct{}{}
if err != nil {
return fmt.Errorf("failed to read original block from disk: %w", err)
}
// If the block is encrypted then we will need to decrypt it before
// doing anything. We always encrypt after compressing because then the
// compression can be as efficient as possible on the raw data, whereas
// the encrypted ciphertext will not compress anywhere near as well.
// The block encryption also covers the optional compression metadata.
if err = mb.encryptOrDecryptIfNeeded(origBuf); err != nil {
return err
}
meta := &CompressionInfo{}
if _, err := meta.UnmarshalMetadata(origBuf); err != nil {
// An error is only returned here if there's a problem with parsing
// the metadata. If the file has no metadata at all, no error is
// returned and the algorithm defaults to no compression.
return fmt.Errorf("failed to read existing metadata header: %w", err)
}
if meta.Algorithm == alg {
// The block is already compressed with the chosen algorithm so there
// is nothing else to do. This is not a common case, it is here only
// to ensure we don't do unnecessary work in case something asked us
// to recompress an already compressed block with the same algorithm.
return nil
} else if alg != NoCompression {
// The block is already compressed using some algorithm, so we need
// to decompress the block using the existing algorithm before we can
// recompress it with the new one.
if origBuf, err = meta.Algorithm.Decompress(origBuf); err != nil {
return fmt.Errorf("failed to decompress original block: %w", err)
}
}
return mb.atomicOverwriteFile(origBuf, true)
}
// Lock should be held.
func (mb *msgBlock) atomicOverwriteFile(buf []byte, allowCompress bool) error {
if mb.mfd != nil {
mb.closeFDsLockedNoCheck()
defer mb.enableForWriting(mb.fs.fip)
}
origFN := mb.mfn // The original message block on disk.
tmpFN := mb.mfn + blkTmpSuffix // The new block will be written here.
// Rather than modifying the existing block on disk (which is a dangerous
// operation if something goes wrong), create a new temporary file. We will
// write out the new block here and then swap the files around afterwards
// once everything else has succeeded correctly.
<-dios
tmpFD, err := os.OpenFile(tmpFN, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, defaultFilePerms)
dios <- struct{}{}
if err != nil {
return fmt.Errorf("failed to create temporary file: %w", err)
}
errorCleanup := func(err error) error {
tmpFD.Close()
os.Remove(tmpFN)
return err
}
alg := NoCompression
if calg := mb.fs.fcfg.Compression; calg != NoCompression && allowCompress {
alg = calg
// The original buffer at this point is uncompressed, so we will now compress
// it if needed. Note that if the selected algorithm is NoCompression, the
// Compress function will just return the input buffer unmodified.
if buf, err = alg.Compress(buf); err != nil {
return errorCleanup(fmt.Errorf("failed to compress block: %w", err))
}
// We only need to write out the metadata header if compression is enabled.
// If we're trying to uncompress the file on disk at this point, don't bother
// writing metadata.
meta := &CompressionInfo{
Algorithm: alg,
OriginalSize: uint64(len(buf)),
}
buf = append(meta.MarshalMetadata(), buf...)
}
// Re-encrypt the block if necessary.
if err = mb.encryptOrDecryptIfNeeded(buf); err != nil {
return errorCleanup(err)
}
// Write the new block data (which might be compressed or encrypted) to the
// temporary file.
if n, err := tmpFD.Write(buf); err != nil {
return errorCleanup(fmt.Errorf("failed to write to temporary file: %w", err))
} else if n != len(buf) {
return errorCleanup(fmt.Errorf("short write to temporary file (%d != %d)", n, len(buf)))
}
if err := tmpFD.Sync(); err != nil {
return errorCleanup(fmt.Errorf("failed to sync temporary file: %w", err))
}
if err := tmpFD.Close(); err != nil {
return errorCleanup(fmt.Errorf("failed to close temporary file: %w", err))
}
// Now replace the original file with the newly updated temp file.
if err := os.Rename(tmpFN, origFN); err != nil {
return fmt.Errorf("failed to move temporary file into place: %w", err)
}
// Since the message block might be retained in memory, make sure the
// compression algorithm is up-to-date, since this will be needed when
// compacting or truncating.
mb.cmp = alg
// Also update rbytes
mb.rbytes = uint64(len(buf))
return nil
}
// Lock should be held.
func (mb *msgBlock) decompressIfNeeded(buf []byte) ([]byte, error) {
var meta CompressionInfo
if n, err := meta.UnmarshalMetadata(buf); err != nil {
// There was a problem parsing the metadata header of the block.
// If there's no metadata header, an error isn't returned here,
// we will instead just use default values of no compression.
return nil, err
} else if n == 0 {
// There were no metadata bytes, so we assume the block is not
// compressed and return it as-is.
return buf, nil
} else {
// Metadata was present so it's quite likely the block contents
// are compressed. If by any chance the metadata claims that the
// block is uncompressed, then the input slice is just returned
// unmodified.
return meta.Algorithm.Decompress(buf[n:])
}
}
// Lock should be held.
func (mb *msgBlock) encryptOrDecryptIfNeeded(buf []byte) error {
if mb.bek != nil && len(buf) > 0 {
bek, err := genBlockEncryptionKey(mb.fs.fcfg.Cipher, mb.seed, mb.nonce)
if err != nil {
return err
}
mb.bek = bek
mb.bek.XORKeyStream(buf, buf)
}
return nil
}
// Lock should be held.
func (mb *msgBlock) ensureRawBytesLoaded() error {
if mb.rbytes > 0 {
return nil
}
f, err := mb.openBlock()
if err != nil {
return err
}
defer f.Close()
if fi, err := f.Stat(); fi != nil && err == nil {
mb.rbytes = uint64(fi.Size())
} else {
return err
}
return nil
}
// Sync msg and index files as needed. This is called from a timer.
func (fs *fileStore) syncBlocks() {
if fs.isClosed() {
return
}
fs.mu.Lock()
blks := append([]*msgBlock(nil), fs.blks...)
lmb, firstMoved, firstSeq := fs.lmb, fs.firstMoved, fs.state.FirstSeq
// Clear first moved.
fs.firstMoved = false
fs.mu.Unlock()
var markDirty bool
for _, mb := range blks {
// Do actual sync. Hold lock for consistency.
mb.mu.Lock()
if mb.closed {
mb.mu.Unlock()
continue
}
// See if we can close FDs due to being idle.
if mb.mfd != nil && mb.sinceLastWriteActivity() > closeFDsIdle && mb.pendingWriteSizeLocked() == 0 {
mb.dirtyCloseWithRemove(false)
}
// If our first has moved and we are set to noCompact (which is from tombstones),
// clear so that we might cleanup tombstones.
if firstMoved && mb.noCompact {
mb.noCompact = false
}
// Check if we should compact here as well.
// Do not compact last mb.
var needsCompact bool
if mb != lmb && mb.ensureRawBytesLoaded() == nil && mb.shouldCompactSync() {
needsCompact = true
markDirty = true
}
// Flush anything that may be pending.
mb.flushPendingMsgsLocked()
// Check if we need to sync. We will not hold lock during actual sync.
needSync := mb.needSync
mb.mu.Unlock()
// Check if we should compact here.
// Need to hold fs lock in case we reference psim when loading in the mb and we may remove this block if truly empty.
if needsCompact {
fs.mu.RLock()
mb.mu.Lock()
mb.compactWithFloor(firstSeq)
// If this compact removed all raw bytes due to tombstone cleanup, schedule to remove.
shouldRemove := mb.rbytes == 0
mb.mu.Unlock()
fs.mu.RUnlock()
// Check if we should remove. This will not be common, so we will re-take fs write lock here vs changing
// it above which we would prefer to be a readlock such that other lookups can occur while compacting this block.
if shouldRemove {
fs.mu.Lock()
mb.mu.Lock()
fs.removeMsgBlock(mb)
mb.mu.Unlock()
fs.mu.Unlock()
needSync = false
}
}
// Check if we need to sync this block.
if needSync {
mb.mu.Lock()
var fd *os.File
var didOpen bool
if mb.mfd != nil {
fd = mb.mfd
} else {
<-dios
fd, _ = os.OpenFile(mb.mfn, os.O_RDWR, defaultFilePerms)
dios <- struct{}{}
didOpen = true
}
// If we have an fd.
if fd != nil {
canClear := fd.Sync() == nil
// If we opened the file close the fd.
if didOpen {
fd.Close()
}
// Only clear sync flag on success.
if canClear {
mb.needSync = false
}
}
mb.mu.Unlock()
}
}
if fs.isClosed() {
return
}
fs.mu.Lock()
fs.setSyncTimer()
if markDirty {
fs.dirty++
}
// Sync state file if we are not running with sync always.
if !fs.fcfg.SyncAlways {
fn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)
<-dios
fd, _ := os.OpenFile(fn, os.O_RDWR, defaultFilePerms)
dios <- struct{}{}
if fd != nil {
fd.Sync()
fd.Close()
}
}
fs.mu.Unlock()
}
// Select the message block where this message should be found.
// Return nil if not in the set.
// Read lock should be held.
func (fs *fileStore) selectMsgBlock(seq uint64) *msgBlock {
_, mb := fs.selectMsgBlockWithIndex(seq)
return mb
}
// Lock should be held.
func (fs *fileStore) selectMsgBlockWithIndex(seq uint64) (int, *msgBlock) {
// Check for out of range.
if seq < fs.state.FirstSeq || seq > fs.state.LastSeq || fs.state.Msgs == 0 {
return -1, nil
}
const linearThresh = 32
nb := len(fs.blks) - 1
if nb < linearThresh {
for i, mb := range fs.blks {
if seq <= atomic.LoadUint64(&mb.last.seq) {
return i, mb
}
}
return -1, nil
}
// Do traditional binary search here since we know the blocks are sorted by sequence first and last.
for low, high, mid := 0, nb, nb/2; low <= high; mid = (low + high) / 2 {
mb := fs.blks[mid]
// Right now these atomic loads do not factor in, so fine to leave. Was considering
// uplifting these to fs scope to avoid atomic load but not needed.
first, last := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
if seq > last {
low = mid + 1
} else if seq < first {
// A message block's first sequence can change here meaning we could find a gap.
// We want to behave like above, which if inclusive (we check at start) should
// always return an index and a valid mb.
// If we have a gap then our seq would be > fs.blks[mid-1].last.seq
if mid == 0 || seq > atomic.LoadUint64(&fs.blks[mid-1].last.seq) {
return mid, mb
}
high = mid - 1
} else {
return mid, mb
}
}
return -1, nil
}
// Select the message block where this message should be found.
// Return nil if not in the set.
func (fs *fileStore) selectMsgBlockForStart(minTime time.Time) *msgBlock {
fs.mu.RLock()
defer fs.mu.RUnlock()
// Binary search for first block where last.ts >= t.
i, _ := slices.BinarySearchFunc(fs.blks, minTime.UnixNano(), func(mb *msgBlock, target int64) int {
mb.mu.RLock()
last := mb.last.ts
mb.mu.RUnlock()
switch {
case last < target:
return -1
case last > target:
return 1
default:
return 0
}
})
// BinarySearchFunc returns an insertion point if not found.
// Either way, i is the index of the first mb where mb.last.ts >= t.
if i < len(fs.blks) {
return fs.blks[i]
}
return nil
}
// Index a raw msg buffer.
// Lock should be held.
func (mb *msgBlock) indexCacheBuf(buf []byte) error {
var le = binary.LittleEndian
var fseq uint64
var idx []uint32
var index uint32
mbFirstSeq := atomic.LoadUint64(&mb.first.seq)
mbLastSeq := atomic.LoadUint64(&mb.last.seq)
// Sanity check here since we calculate size to allocate based on this.
if mbFirstSeq > (mbLastSeq + 1) { // Purged state first == last + 1
mb.fs.warn("indexCacheBuf corrupt state in %s: mb.first %d mb.last %d", mb.mfn, mbFirstSeq, mbLastSeq)
// This would cause idxSz to wrap.
return errCorruptState
}
idxSz := mbLastSeq - mbFirstSeq + 1
if mb.cache == nil || mb.cache.buf == nil {
// Approximation, may adjust below.
fseq = mbFirstSeq
if mb.cache != nil && mb.cache.idx != nil {
idx = mb.cache.idx[:0]
} else {
idx = make([]uint32, 0, idxSz)
}
if mb.cache == nil {
mb.cache = &cache{}
} else {
*mb.cache = cache{}
}
} else {
fseq = mb.cache.fseq
idx = mb.cache.idx
if len(idx) == 0 {
idx = make([]uint32, 0, idxSz)
}
index = uint32(len(mb.cache.buf))
buf = append(mb.cache.buf, buf...)
}
// Create FSS if we should track.
var popFss bool
if mb.fssNotLoaded() {
mb.fss = stree.NewSubjectTree[SimpleState]()
popFss = true
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
mb.ttls = 0
lbuf := uint32(len(buf))
var seq, ttls uint64
var sm StoreMsg // Used for finding TTL headers
// To ensure the sequence keeps moving up. As well as confirming our index
// is aligned with the mb's first and last sequence.
var first uint64
var last uint64
for index < lbuf {
if index+msgHdrSize > lbuf {
return errCorruptState
}
hdr := buf[index : index+msgHdrSize]
rl, slen := le.Uint32(hdr[0:]), int(le.Uint16(hdr[20:]))
seq = le.Uint64(hdr[4:])
// Clear any headers bit that could be set.
hasHeaders := rl&hbit != 0
rl &^= hbit
dlen := int(rl) - msgHdrSize
// Do some quick sanity checks here.
if dlen < 0 || slen > (dlen-recordHashSize) || dlen > int(rl) || index+rl > lbuf || rl > rlBadThresh {
mb.fs.warn("indexCacheBuf corrupt record state in %s: dlen %d slen %d index %d rl %d lbuf %d", mb.mfn, dlen, slen, index, rl, lbuf)
// This means something is off.
// TODO(dlc) - Add into bad list?
return errCorruptState
}
// Check for tombstones which we can skip in terms of indexing.
if seq&tbit != 0 {
index += rl
continue
}
// Clear any erase bits.
erased := seq&ebit != 0
seq = seq &^ ebit
// The sequence needs to only ever move up.
if seq <= last {
// Advance to next record.
// We've already accounted for this sequence and marked it as deleted.
index += rl
continue
}
// We defer checksum checks to individual msg cache lookups to amortorize costs and
// not introduce latency for first message from a newly loaded block.
if seq >= mbFirstSeq {
last = seq
// If the first sequence doesn't align with what we had in-memory, we need to rebuild.
if first == 0 {
first = seq
if mbFirstSeq != first {
return errCorruptState
}
}
// Track that we do not have holes.
if slot := int(seq - mbFirstSeq); slot != len(idx) {
// If we have a hole fill it.
for dseq := mbFirstSeq + uint64(len(idx)); dseq < seq; dseq++ {
idx = append(idx, dbit)
if dseq != 0 {
mb.dmap.Insert(dseq)
}
}
}
// Add to our index.
idx = append(idx, index)
mb.cache.lrl = uint32(rl)
// Adjust if we guessed wrong.
if seq != 0 && seq < fseq {
fseq = seq
}
// Make sure our dmap has this entry if it was erased.
// If not, that means this erased message was not accounted for in our in-memory state.
if erased && seq != 0 && !mb.dmap.Exists(seq) {
return errCorruptState
}
// Handle FSS inline here.
if popFss && slen > 0 && !mb.noTrack && !erased && !mb.dmap.Exists(seq) {
bsubj := buf[index+msgHdrSize : index+msgHdrSize+uint32(slen)]
if ss, ok := mb.fss.Find(bsubj); ok && ss != nil {
ss.Msgs++
ss.Last = seq
ss.lastNeedsUpdate = false
} else {
mb.fss.Insert(bsubj, SimpleState{
Msgs: 1,
First: seq,
Last: seq,
})
}
}
// Count how many TTLs we think are in this message block.
// TODO(nat): Not terribly optimal...
if hasHeaders {
if fsm, err := mb.msgFromBufNoCopy(buf[index:], &sm, nil); err == nil && fsm != nil {
if ttl := sliceHeader(JSMessageTTL, fsm.hdr); len(ttl) > 0 {
ttls++
}
}
}
}
index += rl
}
// If we ended up with a smaller or larger index, or the first/last sequence
// doesn't align with what we had in-memory, we need to rebuild.
if len(idx) != int(idxSz) || (first > 0 && mbFirstSeq != first) || (last > 0 && mbLastSeq != last) {
return errCorruptState
}
mb.cache.buf = buf
mb.cache.idx = idx
mb.cache.fseq = mbFirstSeq
mb.cache.wp = int(lbuf)
mb.ttls = ttls
return nil
}
// flushPendingMsgs writes out any messages for this message block.
func (mb *msgBlock) flushPendingMsgs() error {
mb.mu.Lock()
fsLostData, err := mb.flushPendingMsgsLocked()
fs := mb.fs
mb.mu.Unlock()
// Signals us that we need to rebuild filestore state.
if fsLostData != nil && fs != nil {
// Rebuild fs state too.
fs.rebuildState(fsLostData)
}
return err
}
// Write function for actual data.
// mb.mfd should not be nil.
// Lock should held.
func (mb *msgBlock) writeAt(buf []byte, woff int64) (int, error) {
// Used to mock write failures.
if mb.mockWriteErr {
// Reset on trip.
mb.mockWriteErr = false
return 0, errors.New("mock write error")
}
<-dios
n, err := mb.mfd.WriteAt(buf, woff)
dios <- struct{}{}
return n, err
}
// flushPendingMsgsLocked writes out any messages for this message block.
// Lock should be held.
func (mb *msgBlock) flushPendingMsgsLocked() (*LostStreamData, error) {
// Signals us that we need to rebuild filestore state.
var fsLostData *LostStreamData
if mb.cache == nil || mb.mfd == nil {
return nil, nil
}
buf, err := mb.bytesPending()
// If we got an error back return here.
if err != nil {
// No pending data to be written is not an error.
if err == errNoPending || err == errNoCache {
err = nil
}
return nil, err
}
woff := int64(mb.cache.off + mb.cache.wp)
lob := len(buf)
// TODO(dlc) - Normally we would not hold the lock across I/O so we can improve performance.
// We will hold to stabilize the code base, as we have had a few anomalies with partial cache errors
// under heavy load.
// Check if we need to encrypt.
if err := mb.checkAndLoadEncryption(); err != nil {
return nil, err
}
if mb.bek != nil && lob > 0 {
// Need to leave original alone.
var dst []byte
if lob <= defaultLargeBlockSize {
dst = getMsgBlockBuf(lob)[:lob]
} else {
dst = make([]byte, lob)
}
mb.bek.XORKeyStream(dst, buf)
buf = dst
}
// Append new data to the message block file.
for lbb := lob; lbb > 0; lbb = len(buf) {
n, err := mb.writeAt(buf, woff)
if err != nil {
mb.dirtyCloseWithRemove(false)
ld, _, _ := mb.rebuildStateLocked()
mb.werr = err
return ld, err
}
// Update our write offset.
woff += int64(n)
// Partial write.
if n != lbb {
buf = buf[n:]
} else {
// Done.
break
}
}
// Clear any error.
mb.werr = nil
// Cache may be gone.
if mb.cache == nil || mb.mfd == nil {
return fsLostData, mb.werr
}
// Check if we are in sync always mode.
if mb.syncAlways {
mb.mfd.Sync()
} else {
mb.needSync = true
}
// Check for additional writes while we were writing to the disk.
moreBytes := len(mb.cache.buf) - mb.cache.wp - lob
// Decide what we want to do with the buffer in hand. If we have load interest
// we will hold onto the whole thing, otherwise empty the buffer, possibly reusing it.
if ts := ats.AccessTime(); ts < mb.llts || (ts-mb.llts) <= int64(mb.cexp) {
mb.cache.wp += lob
} else {
if moreBytes == 0 {
if cap(mb.cache.buf) <= maxBufReuse {
// Reuse the entire underlying buffer
buf = mb.cache.buf[:0]
} else {
recycleMsgBlockBuf(mb.cache.buf)
buf = nil
}
} else {
// Move the additional bytes to the beginning of the buffer and re-slice so
// we will have more usable capacity at the end again for additional writes.
copy(mb.cache.buf[:moreBytes], mb.cache.buf[len(mb.cache.buf)-moreBytes:])
buf = mb.cache.buf[:moreBytes]
}
// Update our cache offset.
mb.cache.off = int(woff)
// Reset write pointer.
mb.cache.wp = 0
// Place buffer back in the cache structure.
mb.cache.buf = buf
// Mark fseq to 0
mb.cache.fseq = 0
}
return fsLostData, mb.werr
}
// Lock should be held.
func (mb *msgBlock) clearLoading() {
mb.loading = false
}
// Will load msgs from disk.
func (mb *msgBlock) loadMsgs() error {
// We hold the lock here the whole time by design.
mb.mu.Lock()
defer mb.mu.Unlock()
return mb.loadMsgsWithLock()
}
// Lock should be held.
func (mb *msgBlock) cacheAlreadyLoaded() bool {
if mb.cache == nil || mb.cache.off != 0 || mb.cache.fseq == 0 || len(mb.cache.buf) == 0 {
return false
}
numEntries := mb.msgs + uint64(mb.dmap.Size()) + (atomic.LoadUint64(&mb.first.seq) - mb.cache.fseq)
return numEntries == uint64(len(mb.cache.idx))
}
// Lock should be held.
func (mb *msgBlock) cacheNotLoaded() bool {
return !mb.cacheAlreadyLoaded()
}
// Report if our fss is not loaded.
// Lock should be held.
func (mb *msgBlock) fssNotLoaded() bool {
return mb.fss == nil && !mb.noTrack
}
// Wrap openBlock for the gated semaphore processing.
// Lock should be held
func (mb *msgBlock) openBlock() (*os.File, error) {
// Gate with concurrent IO semaphore.
<-dios
f, err := os.Open(mb.mfn)
dios <- struct{}{}
return f, err
}
// Used to load in the block contents.
// Lock should be held and all conditionals satisfied prior.
func (mb *msgBlock) loadBlock(buf []byte) ([]byte, error) {
var f *os.File
// Re-use if we have mfd open.
if mb.mfd != nil {
f = mb.mfd
if n, err := f.Seek(0, 0); n != 0 || err != nil {
f = nil
mb.closeFDsLockedNoCheck()
}
}
if f == nil {
var err error
f, err = mb.openBlock()
if err != nil {
if os.IsNotExist(err) {
err = errNoBlkData
}
return nil, err
}
defer f.Close()
}
var sz int
if info, err := f.Stat(); err == nil {
sz64 := info.Size()
if int64(int(sz64)) == sz64 {
sz = int(sz64)
} else {
return nil, errMsgBlkTooBig
}
}
if buf == nil {
buf = getMsgBlockBuf(sz)
if sz > cap(buf) {
// We know we will make a new one so just recycle for now.
recycleMsgBlockBuf(buf)
buf = nil
}
}
if sz > cap(buf) {
buf = make([]byte, sz)
} else {
buf = buf[:sz]
}
<-dios
n, err := io.ReadFull(f, buf)
dios <- struct{}{}
// On success capture raw bytes size.
if err == nil {
mb.rbytes = uint64(n)
}
return buf[:n], err
}
// Lock should be held.
func (mb *msgBlock) loadMsgsWithLock() error {
if err := mb.checkAndLoadEncryption(); err != nil {
return err
}
// Check to see if we are loading already.
if mb.loading {
return nil
}
// Set loading status.
mb.loading = true
defer mb.clearLoading()
var nchecks int
checkCache:
nchecks++
if nchecks > 8 {
return errNoCache
}
// Check to see if we have a full cache.
if mb.cacheAlreadyLoaded() {
return nil
}
mb.llts = ats.AccessTime()
// FIXME(dlc) - We could be smarter here.
if buf, _ := mb.bytesPending(); len(buf) > 0 {
ld, err := mb.flushPendingMsgsLocked()
if ld != nil && mb.fs != nil {
// We do not know if fs is locked or not at this point.
// This should be an exceptional condition so do so in Go routine.
go mb.fs.rebuildState(ld)
}
if err != nil {
return err
}
goto checkCache
}
// Load in the whole block.
// We want to hold the mb lock here to avoid any changes to state.
buf, err := mb.loadBlock(nil)
if err != nil {
mb.fs.warn("loadBlock error: %v", err)
if err == errNoBlkData {
if ld, _, err := mb.rebuildStateLocked(); err != nil && ld != nil {
// Rebuild fs state too.
go mb.fs.rebuildState(ld)
}
}
return err
}
// Reset the cache since we just read everything in.
// Make sure this is cleared in case we had a partial when we started.
mb.clearCacheAndOffset()
// Check if we need to decrypt.
if err = mb.encryptOrDecryptIfNeeded(buf); err != nil {
return err
}
// Check for compression.
if buf, err = mb.decompressIfNeeded(buf); err != nil {
return err
}
if err := mb.indexCacheBuf(buf); err != nil {
if err == errCorruptState {
var ld *LostStreamData
ld, _, err = mb.rebuildStateLocked()
// We do not know if fs is locked or not at this point.
// This should be an exceptional condition so do so in Go routine.
// Always rebuild the filestore's state if indexing fails, even if no data was lost,
// our in-memory state was stale in that case.
go mb.fs.rebuildState(ld)
}
if err != nil {
return err
}
goto checkCache
}
if len(buf) > 0 {
mb.cloads++
mb.startCacheExpireTimer()
}
return nil
}
// Fetch a message from this block, possibly reading in and caching the messages.
// We assume the block was selected and is correct, so we do not do range checks.
// Lock should not be held.
func (mb *msgBlock) fetchMsg(seq uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
return mb.fetchMsgEx(seq, sm, true)
}
// Fetch a message from this block, possibly reading in and caching the messages.
// We assume the block was selected and is correct, so we do not do range checks.
// We will not copy the msg data.
// Lock should not be held.
func (mb *msgBlock) fetchMsgNoCopy(seq uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
return mb.fetchMsgEx(seq, sm, false)
}
// Fetch a message from this block, possibly reading in and caching the messages.
// We assume the block was selected and is correct, so we do not do range checks.
// We will copy the msg data based on doCopy boolean.
// Lock should not be held.
func (mb *msgBlock) fetchMsgEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg, bool, error) {
mb.mu.Lock()
defer mb.mu.Unlock()
fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
if seq < fseq || seq > lseq {
return nil, false, ErrStoreMsgNotFound
}
// See if we can short circuit if we already know msg deleted.
if mb.dmap.Exists(seq) {
// Update for scanning like cacheLookup would have.
llseq := mb.llseq
if mb.llseq == 0 || seq < mb.llseq || seq == mb.llseq+1 || seq == mb.llseq-1 {
mb.llseq = seq
}
expireOk := (seq == lseq && llseq == seq-1) || (seq == fseq && llseq == seq+1)
return nil, expireOk, errDeletedMsg
}
if mb.cacheNotLoaded() {
if err := mb.loadMsgsWithLock(); err != nil {
return nil, false, err
}
}
llseq := mb.llseq
fsm, err := mb.cacheLookupEx(seq, sm, doCopy)
if err != nil {
return nil, false, err
}
expireOk := (seq == lseq && llseq == seq-1) || (seq == fseq && llseq == seq+1)
return fsm, expireOk, err
}
var (
errNoCache = errors.New("no message cache")
errDeletedMsg = errors.New("deleted message")
errPartialCache = errors.New("partial cache")
errNoPending = errors.New("message block does not have pending data")
errNotReadable = errors.New("storage directory not readable")
errCorruptState = errors.New("corrupt state file")
errPriorState = errors.New("prior state file")
errPendingData = errors.New("pending data still present")
errNoEncryption = errors.New("encryption not enabled")
errBadKeySize = errors.New("encryption bad key size")
errNoMsgBlk = errors.New("no message block")
errMsgBlkTooBig = errors.New("message block size exceeded int capacity")
errUnknownCipher = errors.New("unknown cipher")
errNoMainKey = errors.New("encrypted store encountered with no main key")
errNoBlkData = errors.New("message block data missing")
errStateTooBig = errors.New("store state too big for optional write")
)
type (
errBadMsg struct{ fn, detail string }
)
func (e errBadMsg) Error() string {
if e.detail != _EMPTY_ {
return fmt.Sprintf("malformed or corrupt message in %s: %s", filepath.Base(e.fn), e.detail)
}
return fmt.Sprintf("malformed or corrupt message in %s", filepath.Base(e.fn))
}
const (
// "Checksum bit" is used in "mb.cache.idx" for marking messages that have had their checksums checked.
cbit = 1 << 31
// "Delete bit" is used in "mb.cache.idx" to mark an index as deleted and non-existent.
dbit = 1 << 30
// "Header bit" is used in "rl" to signal a message record with headers.
hbit = 1 << 31
// "Erase bit" is used in "seq" for marking erased messages sequences.
ebit = 1 << 63
// "Tombstone bit" is used in "seq" for marking tombstone sequences.
tbit = 1 << 62
)
// Will do a lookup from cache.
// This will copy the msg from the cache.
// Lock should be held.
func (mb *msgBlock) cacheLookup(seq uint64, sm *StoreMsg) (*StoreMsg, error) {
return mb.cacheLookupEx(seq, sm, true)
}
// Will do a lookup from cache.
// This will NOT copy the msg from the cache.
// Lock should be held.
func (mb *msgBlock) cacheLookupNoCopy(seq uint64, sm *StoreMsg) (*StoreMsg, error) {
return mb.cacheLookupEx(seq, sm, false)
}
// Will do a lookup from cache.
// Lock should be held.
func (mb *msgBlock) cacheLookupEx(seq uint64, sm *StoreMsg, doCopy bool) (*StoreMsg, error) {
if seq < atomic.LoadUint64(&mb.first.seq) || seq > atomic.LoadUint64(&mb.last.seq) {
return nil, ErrStoreMsgNotFound
}
// The llseq signals us when we can expire a cache at the end of a linear scan.
// We want to only update when we know the last reads (multiple consumers) are sequential.
// We want to account for forwards and backwards linear scans.
if mb.llseq == 0 || seq < mb.llseq || seq == mb.llseq+1 || seq == mb.llseq-1 {
mb.llseq = seq
}
// If we have a delete map check it.
if mb.dmap.Exists(seq) {
mb.llts = ats.AccessTime()
return nil, errDeletedMsg
}
// Detect no cache loaded.
if mb.cache == nil || mb.cache.fseq == 0 || len(mb.cache.idx) == 0 || len(mb.cache.buf) == 0 {
var reason string
if mb.cache == nil {
reason = "no cache"
} else if mb.cache.fseq == 0 {
reason = "fseq is 0"
} else if len(mb.cache.idx) == 0 {
reason = "no idx present"
} else {
reason = "cache buf empty"
}
mb.fs.warn("Cache lookup detected no cache: %s", reason)
return nil, errNoCache
}
// Check partial cache status.
if seq < mb.cache.fseq {
mb.fs.warn("Cache lookup detected partial cache: seq %d vs cache fseq %d", seq, mb.cache.fseq)
return nil, errPartialCache
}
bi, _, hashChecked, err := mb.slotInfo(int(seq - mb.cache.fseq))
if err != nil {
return nil, err
}
// Update cache activity.
mb.llts = ats.AccessTime()
li := int(bi) - mb.cache.off
if li >= len(mb.cache.buf) {
return nil, errPartialCache
}
buf := mb.cache.buf[li:]
// We use the high bit to denote we have already checked the checksum.
var hh *highwayhash.Digest64
if !hashChecked {
hh = mb.hh // This will force the hash check in msgFromBuf.
}
// Parse from the raw buffer.
fsm, err := mb.msgFromBufEx(buf, sm, hh, doCopy)
if err != nil || fsm == nil {
return nil, err
}
// Deleted messages that are decoded return a 0 for sequence.
if fsm.seq == 0 {
return nil, errDeletedMsg
}
if seq != fsm.seq { // See TestFileStoreInvalidIndexesRebuilt.
recycleMsgBlockBuf(mb.cache.buf)
mb.cache.buf = nil
return nil, fmt.Errorf("sequence numbers for cache load did not match, %d vs %d", seq, fsm.seq)
}
// Clear the check bit here after we know all is good.
if !hashChecked {
mb.cache.idx[seq-mb.cache.fseq] = (bi | cbit)
}
return fsm, nil
}
// Used when we are checking if discarding a message due to max msgs per subject will give us
// enough room for a max bytes condition.
// Lock should be already held.
func (fs *fileStore) sizeForSeq(seq uint64) int {
if seq == 0 {
return 0
}
var smv StoreMsg
if mb := fs.selectMsgBlock(seq); mb != nil {
if sm, _, _ := mb.fetchMsgNoCopy(seq, &smv); sm != nil {
return int(fileStoreMsgSize(sm.subj, sm.hdr, sm.msg))
}
}
return 0
}
// Will return message for the given sequence number.
// This will be returned to external callers.
func (fs *fileStore) msgForSeq(seq uint64, sm *StoreMsg) (*StoreMsg, error) {
return fs.msgForSeqLocked(seq, sm, true)
}
// Will return message for the given sequence number.
func (fs *fileStore) msgForSeqLocked(seq uint64, sm *StoreMsg, needFSLock bool) (*StoreMsg, error) {
if fs.isClosed() {
return nil, ErrStoreClosed
}
// TODO(dlc) - Since Store, Remove, Skip all hold the write lock on fs this will
// be stalled. Need another lock if want to happen in parallel.
if needFSLock {
fs.mu.RLock()
}
// Indicates we want first msg.
if seq == 0 {
seq = fs.state.FirstSeq
}
// Make sure to snapshot here.
mb, lseq := fs.selectMsgBlock(seq), fs.state.LastSeq
if needFSLock {
fs.mu.RUnlock()
}
if mb == nil {
var err = ErrStoreEOF
if seq <= lseq {
err = ErrStoreMsgNotFound
}
return nil, err
}
fsm, expireOk, err := mb.fetchMsg(seq, sm)
if err != nil {
return nil, err
}
// We detected a linear scan and access to the last message.
// If we are not the last message block we can try to expire the cache.
if expireOk {
mb.tryForceExpireCache()
}
return fsm, nil
}
// Internal function to return msg parts from a raw buffer.
// Raw buffer will be copied into sm.
// Lock should be held.
func (mb *msgBlock) msgFromBuf(buf []byte, sm *StoreMsg, hh *highwayhash.Digest64) (*StoreMsg, error) {
return mb.msgFromBufEx(buf, sm, hh, true)
}
// Internal function to return msg parts from a raw buffer.
// Raw buffer will NOT be copied into sm.
// Only use for internal use, any message that is passed to upper layers should use mb.msgFromBuf.
// Lock should be held.
func (mb *msgBlock) msgFromBufNoCopy(buf []byte, sm *StoreMsg, hh *highwayhash.Digest64) (*StoreMsg, error) {
return mb.msgFromBufEx(buf, sm, hh, false)
}
// Internal function to return msg parts from a raw buffer.
// copy boolean will determine if we make a copy or not.
// Lock should be held.
func (mb *msgBlock) msgFromBufEx(buf []byte, sm *StoreMsg, hh *highwayhash.Digest64, doCopy bool) (*StoreMsg, error) {
if len(buf) < emptyRecordLen {
return nil, errBadMsg{mb.mfn, "record too short"}
}
var le = binary.LittleEndian
hdr := buf[:msgHdrSize]
rl := le.Uint32(hdr[0:])
hasHeaders := rl&hbit != 0
rl &^= hbit // clear header bit
dlen := int(rl) - msgHdrSize
slen := int(le.Uint16(hdr[20:]))
// Simple sanity check.
if dlen < 0 || slen > (dlen-recordHashSize) || dlen > int(rl) || int(rl) > len(buf) || rl > rlBadThresh {
return nil, errBadMsg{mb.mfn, fmt.Sprintf("sanity check failed (dlen %d slen %d rl %d buf %d)", dlen, slen, rl, buf)}
}
data := buf[msgHdrSize : msgHdrSize+dlen]
// Do checksum tests here if requested.
if hh != nil {
hh.Reset()
hh.Write(hdr[4:20])
hh.Write(data[:slen])
if hasHeaders {
hh.Write(data[slen+4 : dlen-recordHashSize])
} else {
hh.Write(data[slen : dlen-recordHashSize])
}
var hb [highwayhash.Size64]byte
if !bytes.Equal(hh.Sum(hb[:0]), data[len(data)-8:]) {
return nil, errBadMsg{mb.mfn, "invalid checksum"}
}
}
seq := le.Uint64(hdr[4:])
if seq&ebit != 0 {
seq = 0
}
ts := int64(le.Uint64(hdr[12:]))
// Create a StoreMsg if needed.
if sm == nil {
sm = new(StoreMsg)
} else {
sm.clear()
}
// To recycle the large blocks we can never pass back a reference, so need to copy for the upper
// layers and for us to be safe to expire, and recycle, the large msgBlocks.
end := dlen - 8
if len(data) < end {
return nil, errBadMsg{mb.mfn, "invalid data length"}
}
if hasHeaders {
if slen+4 > len(data) {
return nil, errBadMsg{mb.mfn, "invalid subject length greataer than data length"}
}
hl := le.Uint32(data[slen:])
bi := slen + 4
li := bi + int(hl)
if bi > end {
return nil, errBadMsg{mb.mfn, "invalid buffer index"}
}
if doCopy {
sm.buf = append(sm.buf, data[bi:end]...)
} else {
sm.buf = data[bi:end]
}
li, end = li-bi, end-bi
if li > len(sm.buf) || end > len(sm.buf) {
return nil, errBadMsg{mb.mfn, "invalid message length or end greater than buffer length"}
}
sm.hdr = sm.buf[0:li:li]
sm.msg = sm.buf[li:end]
} else {
if slen > end {
return nil, errBadMsg{mb.mfn, "invalid subject length greater than end"}
}
if doCopy {
sm.buf = append(sm.buf, data[slen:end]...)
} else {
sm.buf = data[slen:end]
}
mlen := end - slen
if mlen > len(sm.buf) {
return nil, errBadMsg{mb.mfn, "invalid message length greater than buffer length"}
}
sm.msg = sm.buf[0:mlen]
}
sm.seq, sm.ts = seq, ts
if slen > 0 {
if slen > len(data) {
return nil, errBadMsg{mb.mfn, "invalid subject length greater than data length"}
}
if doCopy {
// Make a copy since sm.subj lifetime may last longer.
sm.subj = string(data[:slen])
} else {
sm.subj = bytesToString(data[:slen])
}
}
return sm, nil
}
// SubjectForSeq will return what the subject is for this sequence if found.
func (fs *fileStore) SubjectForSeq(seq uint64) (string, error) {
fs.mu.RLock()
if seq < fs.state.FirstSeq {
fs.mu.RUnlock()
return _EMPTY_, ErrStoreMsgNotFound
}
var smv StoreMsg
mb := fs.selectMsgBlock(seq)
fs.mu.RUnlock()
if mb != nil {
if sm, _, _ := mb.fetchMsgNoCopy(seq, &smv); sm != nil {
return sm.subj, nil
}
}
return _EMPTY_, ErrStoreMsgNotFound
}
// LoadMsg will lookup the message by sequence number and return it if found.
func (fs *fileStore) LoadMsg(seq uint64, sm *StoreMsg) (*StoreMsg, error) {
return fs.msgForSeq(seq, sm)
}
// loadLast will load the last message for a subject. Subject should be non empty and not ">".
func (fs *fileStore) loadLast(subj string, sm *StoreMsg) (lsm *StoreMsg, err error) {
if fs.isClosed() {
return nil, ErrStoreClosed
}
fs.mu.RLock()
defer fs.mu.RUnlock()
if fs.lmb == nil {
return nil, ErrStoreClosed
}
if len(fs.blks) == 0 {
return nil, ErrStoreMsgNotFound
}
wc := subjectHasWildcard(subj)
var start, stop uint32
// If literal subject check for presence.
if wc {
start = fs.lmb.index
fs.psim.Match(stringToBytes(subj), func(_ []byte, psi *psi) {
// Keep track of start and stop indexes for this subject.
if psi.fblk < start {
start = psi.fblk
}
if psi.lblk > stop {
stop = psi.lblk
}
})
// None matched.
if stop == 0 {
return nil, ErrStoreMsgNotFound
}
// These need to be swapped.
start, stop = stop, start
} else if info, ok := fs.psim.Find(stringToBytes(subj)); ok {
start, stop = info.lblk, info.fblk
} else {
return nil, ErrStoreMsgNotFound
}
// Walk blocks backwards.
for i := start; i >= stop; i-- {
mb := fs.bim[i]
if mb == nil {
continue
}
mb.mu.Lock()
if err := mb.ensurePerSubjectInfoLoaded(); err != nil {
mb.mu.Unlock()
return nil, err
}
// Mark fss activity.
mb.lsts = ats.AccessTime()
var l uint64
// Optimize if subject is not a wildcard.
if !wc {
if ss, ok := mb.fss.Find(stringToBytes(subj)); ok && ss != nil {
l = ss.Last
}
}
if l == 0 {
_, _, l = mb.filteredPendingLocked(subj, wc, atomic.LoadUint64(&mb.first.seq))
}
if l > 0 {
if mb.cacheNotLoaded() {
if err := mb.loadMsgsWithLock(); err != nil {
mb.mu.Unlock()
return nil, err
}
}
lsm, err = mb.cacheLookup(l, sm)
}
mb.mu.Unlock()
if l > 0 {
break
}
}
return lsm, err
}
// LoadLastMsg will return the last message we have that matches a given subject.
// The subject can be a wildcard.
func (fs *fileStore) LoadLastMsg(subject string, smv *StoreMsg) (sm *StoreMsg, err error) {
if subject == _EMPTY_ || subject == fwcs {
sm, err = fs.msgForSeq(fs.lastSeq(), smv)
} else {
sm, err = fs.loadLast(subject, smv)
}
if sm == nil || (err != nil && err != ErrStoreClosed) {
err = ErrStoreMsgNotFound
}
return sm, err
}
// LoadNextMsgMulti will find the next message matching any entry in the sublist.
func (fs *fileStore) LoadNextMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error) {
if fs.isClosed() {
return nil, 0, ErrStoreClosed
}
if sl == nil || sl.MatchesFullWildcard() {
return fs.LoadNextMsg(_EMPTY_, false, start, smp)
}
if filter, ok := sl.MatchesSingleFilter(); ok {
return fs.LoadNextMsg(filter, subjectHasWildcard(filter), start, smp)
}
fs.mu.RLock()
defer fs.mu.RUnlock()
if fs.state.Msgs == 0 || start > fs.state.LastSeq {
return nil, fs.state.LastSeq, ErrStoreEOF
}
if start < fs.state.FirstSeq {
start = fs.state.FirstSeq
}
// If start is less than or equal to beginning of our stream, meaning our first call,
// let's check the psim to see if we can skip ahead.
if start <= fs.state.FirstSeq {
var total uint64
blkStart := uint32(math.MaxUint32)
stree.IntersectGSL(fs.psim, sl, func(subj []byte, psi *psi) {
total += psi.total
// Keep track of start index for this subject.
if psi.fblk < blkStart {
blkStart = psi.fblk
}
})
// Nothing available.
if total == 0 {
return nil, fs.state.LastSeq, ErrStoreEOF
}
// We can skip ahead.
if mb := fs.bim[blkStart]; mb != nil {
fseq := atomic.LoadUint64(&mb.first.seq)
if fseq > start {
start = fseq
}
}
}
if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 {
for i := bi; i < len(fs.blks); i++ {
mb := fs.blks[i]
if sm, expireOk, err := mb.firstMatchingMulti(sl, start, smp); err == nil {
if expireOk {
mb.tryForceExpireCache()
}
return sm, sm.seq, nil
} else if err != ErrStoreMsgNotFound {
return nil, 0, err
} else {
// Nothing found in this block. We missed, if first block (bi) check psim.
// Similar to above if start <= first seq.
// TODO(dlc) - For v2 track these by filter subject since they will represent filtered consumers.
// We should not do this at all if we are already on the last block.
if i == bi && i < len(fs.blks)-1 {
nbi, err := fs.checkSkipFirstBlockMulti(sl, bi)
// Nothing available.
if err == ErrStoreEOF {
return nil, fs.state.LastSeq, ErrStoreEOF
}
// See if we can jump ahead here.
// Right now we can only spin on first, so if we have interior sparseness need to favor checking per block fss if loaded.
// For v2 will track all blocks that have matches for psim.
if nbi > i {
i = nbi - 1 // For the iterator condition i++
}
}
// Check if we can expire.
if expireOk {
mb.tryForceExpireCache()
}
}
}
}
return nil, fs.state.LastSeq, ErrStoreEOF
}
func (fs *fileStore) LoadNextMsg(filter string, wc bool, start uint64, sm *StoreMsg) (*StoreMsg, uint64, error) {
if fs.isClosed() {
return nil, 0, ErrStoreClosed
}
fs.mu.RLock()
defer fs.mu.RUnlock()
if fs.state.Msgs == 0 || start > fs.state.LastSeq {
return nil, fs.state.LastSeq, ErrStoreEOF
}
if start < fs.state.FirstSeq {
start = fs.state.FirstSeq
}
// If start is less than or equal to beginning of our stream, meaning our first call,
// let's check the psim to see if we can skip ahead.
if start <= fs.state.FirstSeq {
var ss SimpleState
fs.numFilteredPendingNoLast(filter, &ss)
// Nothing available.
if ss.Msgs == 0 {
return nil, fs.state.LastSeq, ErrStoreEOF
}
// We can skip ahead.
if ss.First > start {
start = ss.First
}
}
if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 {
for i := bi; i < len(fs.blks); i++ {
mb := fs.blks[i]
if sm, expireOk, err := mb.firstMatching(filter, wc, start, sm); err == nil {
if expireOk {
mb.tryForceExpireCache()
}
return sm, sm.seq, nil
} else if err != ErrStoreMsgNotFound {
return nil, 0, err
} else {
// Nothing found in this block. We missed, if first block (bi) check psim.
// Similar to above if start <= first seq.
// TODO(dlc) - For v2 track these by filter subject since they will represent filtered consumers.
// We should not do this at all if we are already on the last block.
// Also if we are a wildcard do not check if large subject space.
const wcMaxSizeToCheck = 64 * 1024
if i == bi && i < len(fs.blks)-1 && (!wc || fs.psim.Size() < wcMaxSizeToCheck) {
nbi, err := fs.checkSkipFirstBlock(filter, wc, bi)
// Nothing available.
if err == ErrStoreEOF {
return nil, fs.state.LastSeq, ErrStoreEOF
}
// See if we can jump ahead here.
// Right now we can only spin on first, so if we have interior sparseness need to favor checking per block fss if loaded.
// For v2 will track all blocks that have matches for psim.
if nbi > i {
i = nbi - 1 // For the iterator condition i++
}
}
// Check if we can expire.
if expireOk {
mb.tryForceExpireCache()
}
}
}
}
return nil, fs.state.LastSeq, ErrStoreEOF
}
// Find the previous matching message.
// fs lock should be held.
func (mb *msgBlock) prevMatching(filter string, wc bool, start uint64, sm *StoreMsg) (*StoreMsg, bool, error) {
mb.mu.Lock()
var updateLLTS bool
defer func() {
if updateLLTS {
mb.llts = ats.AccessTime()
}
mb.mu.Unlock()
}()
end, isAll := start, filter == _EMPTY_ || filter == fwcs
var didLoad bool
if mb.fssNotLoaded() {
if err := mb.loadMsgsWithLock(); err != nil {
return nil, false, err
}
didLoad = true
}
mb.lsts = ats.AccessTime()
if filter == _EMPTY_ {
filter = fwcs
wc = true
}
if !isAll && mb.fss.Size() == 1 {
if !wc {
_, isAll = mb.fss.Find(stringToBytes(filter))
} else {
mb.fss.Match(stringToBytes(filter), func(subject []byte, _ *SimpleState) {
isAll = true
})
}
if !isAll {
return nil, didLoad, ErrStoreMsgNotFound
}
}
lseq := atomic.LoadUint64(&mb.first.seq)
end = min(end, atomic.LoadUint64(&mb.last.seq))
var isMatch func(subj string) bool
if wc {
_tsa, _fsa := [32]string{}, [32]string{}
tsa, fsa := _tsa[:0], tokenizeSubjectIntoSlice(_fsa[:0], filter)
isMatch = func(subj string) bool {
tsa = tokenizeSubjectIntoSlice(tsa[:0], subj)
return isSubsetMatchTokenized(tsa, fsa)
}
}
subjs := mb.fs.cfg.Subjects
doLinearScan := isAll || (wc && len(subjs) == 1 && subjs[0] == filter)
if !doLinearScan && wc && mb.cacheAlreadyLoaded() {
doLinearScan = mb.fss.Size()*4 > int(end-lseq)
}
if !doLinearScan {
var found bool
var first, last uint64
if bfilter := stringToBytes(filter); wc {
var ierr error
mb.fss.Match(bfilter, func(bsubj []byte, ss *SimpleState) {
if ierr != nil {
return
}
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
mb.recalculateForSubj(bytesToString(bsubj), ss)
}
if end < ss.First {
return
}
if !found {
found = true
first, last = ss.First, min(end, ss.Last)
return
}
first = min(first, ss.First)
last = max(last, min(end, ss.Last))
})
if ierr != nil {
return nil, false, ierr
}
} else if ss, _ := mb.fss.Find(bfilter); ss != nil {
if ss.firstNeedsUpdate || ss.lastNeedsUpdate {
mb.recalculateForSubj(filter, ss)
}
if end >= ss.First {
found = true
first, last = ss.First, min(end, ss.Last)
}
}
if !found || first > last {
return nil, didLoad, ErrStoreMsgNotFound
}
lseq, end = max(lseq, first), last
}
if mb.cacheNotLoaded() {
if err := mb.loadMsgsWithLock(); err != nil {
return nil, false, err
}
didLoad = true
}
if sm == nil {
sm = new(StoreMsg)
}
for seq := end; seq >= lseq; seq-- {
if mb.dmap.Exists(seq) {
updateLLTS = true
continue
}
llseq := mb.llseq
fsm, err := mb.cacheLookup(seq, sm)
if err != nil {
if err == errPartialCache || err == errNoCache {
return nil, false, err
}
continue
}
updateLLTS = false
expireOk := seq == lseq && mb.llseq != llseq && mb.llseq == seq
if isAll {
return fsm, expireOk, nil
}
if wc && isMatch(sm.subj) {
return fsm, expireOk, nil
} else if !wc && fsm.subj == filter {
return fsm, expireOk, nil
}
mb.llseq = llseq
}
return nil, didLoad, ErrStoreMsgNotFound
}
// Will load the previous message matching the filter subject, starting at the start sequence and walking backwards.
func (fs *fileStore) LoadPrevMsg(filter string, wc bool, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error) {
if fs.isClosed() {
return nil, 0, ErrStoreClosed
}
fs.mu.RLock()
defer fs.mu.RUnlock()
if fs.state.Msgs == 0 || start < fs.state.FirstSeq {
return nil, fs.state.FirstSeq, ErrStoreEOF
}
if start > fs.state.LastSeq {
start = fs.state.LastSeq
}
if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 {
for i := bi; i >= 0; i-- {
mb := fs.blks[i]
if sm, expireOk, err := mb.prevMatching(filter, wc, start, smp); err == nil {
if expireOk {
mb.tryForceExpireCache()
}
return sm, sm.seq, nil
} else if err != ErrStoreMsgNotFound {
return nil, 0, err
} else if expireOk {
mb.tryForceExpireCache()
}
}
}
return nil, fs.state.FirstSeq, ErrStoreEOF
}
// LoadPrevMsgMulti will find the previous message matching any entry in the sublist.
func (fs *fileStore) LoadPrevMsgMulti(sl *gsl.SimpleSublist, start uint64, smp *StoreMsg) (sm *StoreMsg, skip uint64, err error) {
if fs.isClosed() {
return nil, 0, ErrStoreClosed
}
if sl == nil || sl.MatchesFullWildcard() {
return fs.LoadPrevMsg(_EMPTY_, false, start, smp)
}
if filter, ok := sl.MatchesSingleFilter(); ok {
return fs.LoadPrevMsg(filter, subjectHasWildcard(filter), start, smp)
}
fs.mu.RLock()
defer fs.mu.RUnlock()
if fs.state.Msgs == 0 || start < fs.state.FirstSeq {
return nil, fs.state.FirstSeq, ErrStoreEOF
}
if start > fs.state.LastSeq {
start = fs.state.LastSeq
}
if bi, _ := fs.selectMsgBlockWithIndex(start); bi >= 0 {
for i := bi; i >= 0; i-- {
mb := fs.blks[i]
if sm, expireOk, err := mb.prevMatchingMulti(sl, start, smp); err == nil {
if expireOk {
mb.tryForceExpireCache()
}
return sm, sm.seq, nil
} else if err != ErrStoreMsgNotFound {
return nil, 0, err
} else if expireOk {
mb.tryForceExpireCache()
}
}
}
return nil, fs.state.FirstSeq, ErrStoreEOF
}
// Type returns the type of the underlying store.
func (fs *fileStore) Type() StorageType {
return FileStorage
}
// Returns number of subjects in this store.
// Lock should be held.
func (fs *fileStore) numSubjects() int {
return fs.psim.Size()
}
// numConsumers uses new lock.
func (fs *fileStore) numConsumers() int {
fs.cmu.RLock()
defer fs.cmu.RUnlock()
return len(fs.cfs)
}
// FastState will fill in state with only the following.
// Msgs, Bytes, First and Last Sequence and Time and NumDeleted.
func (fs *fileStore) FastState(state *StreamState) {
fs.mu.RLock()
state.Msgs = fs.state.Msgs
state.Bytes = fs.state.Bytes
state.FirstSeq = fs.state.FirstSeq
state.FirstTime = fs.state.FirstTime
state.LastSeq = fs.state.LastSeq
state.LastTime = fs.state.LastTime
// Make sure to reset if being re-used.
state.Deleted, state.NumDeleted = nil, 0
if state.LastSeq > state.FirstSeq {
state.NumDeleted = int((state.LastSeq - state.FirstSeq + 1) - state.Msgs)
if state.NumDeleted < 0 {
state.NumDeleted = 0
}
}
state.Consumers = fs.numConsumers()
state.NumSubjects = fs.numSubjects()
fs.mu.RUnlock()
}
// State returns the current state of the stream.
func (fs *fileStore) State() StreamState {
fs.mu.RLock()
state := fs.state
state.Consumers = fs.numConsumers()
state.NumSubjects = fs.numSubjects()
state.Deleted = nil // make sure.
if numDeleted := int((state.LastSeq - state.FirstSeq + 1) - state.Msgs); numDeleted > 0 {
state.Deleted = make([]uint64, 0, numDeleted)
cur := fs.state.FirstSeq
for _, mb := range fs.blks {
mb.mu.Lock()
fseq := atomic.LoadUint64(&mb.first.seq)
// Account for messages missing from the head.
if fseq > cur {
for seq := cur; seq < fseq; seq++ {
state.Deleted = append(state.Deleted, seq)
}
}
// Only advance cur if we are increasing. We could have marker blocks with just tombstones.
if last := atomic.LoadUint64(&mb.last.seq); last >= cur {
cur = last + 1 // Expected next first.
}
// Add in deleted.
mb.dmap.Range(func(seq uint64) bool {
state.Deleted = append(state.Deleted, seq)
return true
})
mb.mu.Unlock()
}
}
fs.mu.RUnlock()
state.Lost = fs.lostData()
// Can not be guaranteed to be sorted.
if len(state.Deleted) > 0 {
slices.Sort(state.Deleted)
state.NumDeleted = len(state.Deleted)
}
return state
}
func (fs *fileStore) Utilization() (total, reported uint64, err error) {
fs.mu.RLock()
defer fs.mu.RUnlock()
for _, mb := range fs.blks {
mb.mu.RLock()
reported += mb.bytes
total += mb.rbytes
mb.mu.RUnlock()
}
return total, reported, nil
}
func fileStoreMsgSizeRaw(slen, hlen, mlen int) uint64 {
if hlen == 0 {
// length of the message record (4bytes) + seq(8) + ts(8) + subj_len(2) + subj + msg + hash(8)
return uint64(22 + slen + mlen + 8)
}
// length of the message record (4bytes) + seq(8) + ts(8) + subj_len(2) + subj + hdr_len(4) + hdr + msg + hash(8)
return uint64(22 + slen + 4 + hlen + mlen + 8)
}
func fileStoreMsgSize(subj string, hdr, msg []byte) uint64 {
return fileStoreMsgSizeRaw(len(subj), len(hdr), len(msg))
}
func fileStoreMsgSizeEstimate(slen, maxPayload int) uint64 {
return uint64(emptyRecordLen + slen + 4 + maxPayload)
}
// Determine time since any last activity, read/load, write or remove.
func (mb *msgBlock) sinceLastActivity() time.Duration {
if mb.closed {
return 0
}
last := mb.lwts
if mb.lrts > last {
last = mb.lrts
}
if mb.llts > last {
last = mb.llts
}
if mb.lsts > last {
last = mb.lsts
}
return time.Since(time.Unix(0, last).UTC())
}
// Determine time since last write or remove of a message.
// Read lock should be held.
func (mb *msgBlock) sinceLastWriteActivity() time.Duration {
if mb.closed {
return 0
}
last := mb.lwts
if mb.lrts > last {
last = mb.lrts
}
return time.Since(time.Unix(0, last).UTC())
}
func checkNewHeader(hdr []byte) error {
if len(hdr) < 2 || hdr[0] != magic ||
(hdr[1] != version && hdr[1] != newVersion) {
return errCorruptState
}
return nil
}
// readIndexInfo will read in the index information for the message block.
func (mb *msgBlock) readIndexInfo() error {
ifn := filepath.Join(mb.fs.fcfg.StoreDir, msgDir, fmt.Sprintf(indexScan, mb.index))
buf, err := os.ReadFile(ifn)
if err != nil {
return err
}
// Set if first time.
if mb.liwsz == 0 {
mb.liwsz = int64(len(buf))
}
// Decrypt if needed.
if mb.aek != nil {
buf, err = mb.aek.Open(buf[:0], mb.nonce, buf, nil)
if err != nil {
return err
}
}
if err := checkNewHeader(buf); err != nil {
defer os.Remove(ifn)
return fmt.Errorf("bad index file")
}
bi := hdrLen
// Helpers, will set i to -1 on error.
readSeq := func() uint64 {
if bi < 0 {
return 0
}
seq, n := binary.Uvarint(buf[bi:])
if n <= 0 {
bi = -1
return 0
}
bi += n
return seq &^ ebit
}
readCount := readSeq
readTimeStamp := func() int64 {
if bi < 0 {
return 0
}
ts, n := binary.Varint(buf[bi:])
if n <= 0 {
bi = -1
return -1
}
bi += n
return ts
}
mb.msgs = readCount()
mb.bytes = readCount()
atomic.StoreUint64(&mb.first.seq, readSeq())
mb.first.ts = readTimeStamp()
atomic.StoreUint64(&mb.last.seq, readSeq())
mb.last.ts = readTimeStamp()
dmapLen := readCount()
// Check if this is a short write index file.
if bi < 0 || bi+checksumSize > len(buf) {
os.Remove(ifn)
return fmt.Errorf("short index file")
}
// Check for consistency if accounting. If something is off bail and we will rebuild.
if mb.msgs != (atomic.LoadUint64(&mb.last.seq)-atomic.LoadUint64(&mb.first.seq)+1)-dmapLen {
os.Remove(ifn)
return fmt.Errorf("accounting inconsistent")
}
// Checksum
copy(mb.lchk[0:], buf[bi:bi+checksumSize])
bi += checksumSize
// Now check for presence of a delete map
if dmapLen > 0 {
// New version is encoded avl seqset.
if buf[1] == newVersion {
dmap, _, err := avl.Decode(buf[bi:])
if err != nil {
return fmt.Errorf("could not decode avl dmap: %v", err)
}
mb.dmap = *dmap
} else {
// This is the old version.
for i, fseq := 0, atomic.LoadUint64(&mb.first.seq); i < int(dmapLen); i++ {
seq := readSeq()
if seq == 0 {
break
}
mb.dmap.Insert(seq + fseq)
}
}
}
return nil
}
// Will return total number of cache loads.
func (fs *fileStore) cacheLoads() uint64 {
var tl uint64
fs.mu.RLock()
for _, mb := range fs.blks {
tl += mb.cloads
}
fs.mu.RUnlock()
return tl
}
// Will return total number of cached bytes.
func (fs *fileStore) cacheSize() uint64 {
var sz uint64
fs.mu.RLock()
for _, mb := range fs.blks {
mb.mu.RLock()
if mb.cache != nil {
sz += uint64(len(mb.cache.buf))
}
mb.mu.RUnlock()
}
fs.mu.RUnlock()
return sz
}
// Will return total number of dmapEntries for all msg blocks.
func (fs *fileStore) dmapEntries() int {
var total int
fs.mu.RLock()
for _, mb := range fs.blks {
total += mb.dmap.Size()
}
fs.mu.RUnlock()
return total
}
// Fixed helper for iterating.
func subjectsEqual(a, b string) bool {
return a == b
}
func subjectsAll(a, b string) bool {
return true
}
func compareFn(subject string) func(string, string) bool {
if subject == _EMPTY_ || subject == fwcs {
return subjectsAll
}
if subjectHasWildcard(subject) {
return subjectIsSubsetMatch
}
return subjectsEqual
}
// PurgeEx will remove messages based on subject filters, sequence and number of messages to keep.
// Will return the number of purged messages.
func (fs *fileStore) PurgeEx(subject string, sequence, keep uint64) (purged uint64, err error) {
if subject == _EMPTY_ || subject == fwcs {
if keep == 0 && sequence == 0 {
return fs.purge(0)
}
if sequence > 1 {
return fs.compact(sequence)
}
}
// Make sure to not leave subject if empty and we reach this spot.
if subject == _EMPTY_ {
subject = fwcs
}
eq, wc := compareFn(subject), subjectHasWildcard(subject)
var firstSeqNeedsUpdate bool
var bytes uint64
// If we have a "keep" designation need to get full filtered state so we know how many to purge.
var maxp uint64
if keep > 0 {
ss := fs.FilteredState(1, subject)
if keep >= ss.Msgs {
return 0, nil
}
maxp = ss.Msgs - keep
}
var smv StoreMsg
var tombs []msgId
var lowSeq uint64
fs.mu.Lock()
// We may remove blocks as we purge, so don't range directly on fs.blks
// otherwise we may jump over some (see https://github.com/nats-io/nats-server/issues/3528)
for i := 0; i < len(fs.blks); i++ {
mb := fs.blks[i]
mb.mu.Lock()
// If we do not have our fss, try to expire the cache if we have no items in this block.
shouldExpire := mb.fssNotLoaded()
t, f, l := mb.filteredPendingLocked(subject, wc, atomic.LoadUint64(&mb.first.seq))
if t == 0 {
// Expire if we were responsible for loading.
if shouldExpire {
// Expire this cache before moving on.
mb.tryForceExpireCacheLocked()
}
mb.mu.Unlock()
continue
}
if sequence > 1 && sequence <= l {
l = sequence - 1
}
if mb.cacheNotLoaded() {
mb.loadMsgsWithLock()
shouldExpire = true
}
var nrg uint64 // Number of remaining messages globally after removal from psim.
for seq, te := f, len(tombs); seq <= l; seq++ {
if sm, _ := mb.cacheLookupNoCopy(seq, &smv); sm != nil && eq(sm.subj, subject) {
rl := fileStoreMsgSize(sm.subj, sm.hdr, sm.msg)
// Do fast in place remove.
// Stats
if mb.msgs > 0 {
// Msgs
fs.state.Msgs--
mb.msgs--
// Bytes, make sure to not go negative.
if rl > fs.state.Bytes {
rl = fs.state.Bytes
}
if rl > mb.bytes {
rl = mb.bytes
}
fs.state.Bytes -= rl
mb.bytes -= rl
// Totals
purged++
bytes += rl
}
// PSIM and FSS updates.
nr := mb.removeSeqPerSubject(sm.subj, seq)
nrg = fs.removePerSubject(sm.subj)
// Track tombstones we need to write.
tombs = append(tombs, msgId{sm.seq, sm.ts})
if sm.seq < lowSeq || lowSeq == 0 {
lowSeq = sm.seq
}
// Check for first message.
if seq == atomic.LoadUint64(&mb.first.seq) {
mb.selectNextFirst()
if mb.isEmpty() {
// Since we are removing this block don't need to write tombstones.
tombs = tombs[:te]
fs.removeMsgBlock(mb)
i--
// keep flag set, if set previously
firstSeqNeedsUpdate = firstSeqNeedsUpdate || seq == fs.state.FirstSeq
} else if seq == fs.state.FirstSeq {
fs.state.FirstSeq = atomic.LoadUint64(&mb.first.seq) // new one.
if mb.first.ts == 0 {
fs.state.FirstTime = time.Time{}
} else {
fs.state.FirstTime = time.Unix(0, mb.first.ts).UTC()
}
}
} else {
// Out of order delete.
mb.dmap.Insert(seq)
}
// Break if we have emptied this block or if we set a maximum purge count.
if mb.isEmpty() || (maxp > 0 && purged >= maxp) {
break
}
// Also break if we know we have no more messages matching here.
// This is only applicable for non-wildcarded filters.
if !wc && nr == 0 {
break
}
}
}
// Expire if we were responsible for loading and we do not seem to be doing successive purgeEx calls.
// On successive calls - most likely from KV purge deletes, we want to keep the data loaded.
if shouldExpire && time.Since(fs.lpex) > time.Second {
// Expire this cache before moving on.
mb.tryForceExpireCacheLocked()
}
mb.mu.Unlock()
// Check if we should break out of top level too.
if maxp > 0 && purged >= maxp {
break
}
// Also check if not wildcarded and we have no remaining matches.
if !wc && nrg == 0 {
break
}
}
if firstSeqNeedsUpdate {
fs.selectNextFirst()
}
// Update the last purgeEx call time.
defer func() { fs.lpex = time.Now() }()
// Write any tombstones as needed.
// When writing multiple tombstones we will flush at the end.
if len(tombs) > 0 {
for _, tomb := range tombs {
if err := fs.writeTombstoneNoFlush(tomb.seq, tomb.ts); err != nil {
return purged, err
}
}
// Flush any pending. If we change blocks the newMsgBlockForWrite() will flush any pending for us.
if lmb := fs.lmb; lmb != nil {
lmb.flushPendingMsgs()
}
}
fs.dirty++
cb := fs.scb
fs.mu.Unlock()
if cb != nil {
if purged == 1 {
cb(-int64(purged), -int64(bytes), lowSeq, subject)
} else {
// FIXME(dlc) - Since we track lowSeq we could send to upper layer if they dealt with the condition properly.
cb(-int64(purged), -int64(bytes), 0, _EMPTY_)
}
}
return purged, nil
}
// Purge will remove all messages from this store.
// Will return the number of purged messages.
func (fs *fileStore) Purge() (uint64, error) {
return fs.purge(0)
}
func (fs *fileStore) purge(fseq uint64) (uint64, error) {
if fs.isClosed() {
return 0, ErrStoreClosed
}
fs.mu.Lock()
purged := fs.state.Msgs
rbytes := int64(fs.state.Bytes)
fs.state.FirstSeq = fs.state.LastSeq + 1
fs.state.FirstTime = time.Time{}
fs.state.Bytes = 0
fs.state.Msgs = 0
for _, mb := range fs.blks {
mb.dirtyClose()
}
fs.blks = nil
fs.lmb = nil
fs.bim = make(map[uint32]*msgBlock)
// Clear any per subject tracking.
fs.psim, fs.tsl = fs.psim.Empty(), 0
fs.sdm.empty()
// Mark dirty.
fs.dirty++
// Move the msgs directory out of the way, will delete out of band.
// FIXME(dlc) - These can error and we need to change api above to propagate?
mdir := filepath.Join(fs.fcfg.StoreDir, msgDir)
pdir := filepath.Join(fs.fcfg.StoreDir, purgeDir)
// If purge directory still exists then we need to wait
// in place and remove since rename would fail.
if _, err := os.Stat(pdir); err == nil {
<-dios
os.RemoveAll(pdir)
dios <- struct{}{}
}
<-dios
os.Rename(mdir, pdir)
dios <- struct{}{}
go func() {
<-dios
os.RemoveAll(pdir)
dios <- struct{}{}
}()
// Create new one.
<-dios
os.MkdirAll(mdir, defaultDirPerms)
dios <- struct{}{}
// Make sure we have a lmb to write to.
if _, err := fs.newMsgBlockForWrite(); err != nil {
fs.mu.Unlock()
return purged, err
}
// Check if we need to set the first seq to a new number.
if fseq > fs.state.FirstSeq {
fs.state.FirstSeq = fseq
fs.state.LastSeq = fseq - 1
}
lmb := fs.lmb
atomic.StoreUint64(&lmb.first.seq, fs.state.FirstSeq)
atomic.StoreUint64(&lmb.last.seq, fs.state.LastSeq)
lmb.last.ts = fs.state.LastTime.UnixNano()
if lseq := atomic.LoadUint64(&lmb.last.seq); lseq > 0 {
// Leave a tombstone so we can remember our starting sequence in case
// full state becomes corrupted.
fs.writeTombstone(lseq, lmb.last.ts)
}
cb := fs.scb
fs.mu.Unlock()
// Force a new index.db to be written.
if purged > 0 {
fs.forceWriteFullState()
}
if cb != nil {
cb(-int64(purged), -rbytes, 0, _EMPTY_)
}
return purged, nil
}
// Compact will remove all messages from this store up to
// but not including the seq parameter.
// Will return the number of purged messages.
func (fs *fileStore) Compact(seq uint64) (uint64, error) {
return fs.compact(seq)
}
func (fs *fileStore) compact(seq uint64) (uint64, error) {
if seq == 0 {
return fs.purge(seq)
}
var purged, bytes uint64
fs.mu.Lock()
// Same as purge all.
if lseq := fs.state.LastSeq; seq > lseq {
fs.mu.Unlock()
return fs.purge(seq)
}
// Short-circuit if the store was already compacted past this point.
if fs.state.FirstSeq > seq {
fs.mu.Unlock()
return purged, nil
}
// We have to delete interior messages.
smb := fs.selectMsgBlock(seq)
if smb == nil {
fs.mu.Unlock()
return 0, nil
}
// All msgblocks up to this one can be thrown away.
var deleted int
for _, mb := range fs.blks {
if mb == smb {
break
}
mb.mu.Lock()
purged += mb.msgs
bytes += mb.bytes
// Make sure we do subject cleanup as well.
mb.ensurePerSubjectInfoLoaded()
mb.fss.IterOrdered(func(bsubj []byte, ss *SimpleState) bool {
subj := bytesToString(bsubj)
for i := uint64(0); i < ss.Msgs; i++ {
fs.removePerSubject(subj)
}
return true
})
// Now close.
mb.dirtyCloseWithRemove(true)
mb.mu.Unlock()
deleted++
}
var smv StoreMsg
var err error
var tombs []msgId
smb.mu.Lock()
if atomic.LoadUint64(&smb.first.seq) == seq {
fs.state.FirstSeq = atomic.LoadUint64(&smb.first.seq)
fs.state.FirstTime = time.Unix(0, smb.first.ts).UTC()
goto SKIP
}
// Make sure we have the messages loaded.
if smb.cacheNotLoaded() {
if err = smb.loadMsgsWithLock(); err != nil {
goto SKIP
}
}
for mseq := atomic.LoadUint64(&smb.first.seq); mseq < seq; mseq++ {
sm, err := smb.cacheLookupNoCopy(mseq, &smv)
if err == errDeletedMsg {
// Update dmap.
if !smb.dmap.IsEmpty() {
smb.dmap.Delete(mseq)
}
} else if sm != nil {
sz := fileStoreMsgSize(sm.subj, sm.hdr, sm.msg)
if smb.msgs > 0 {
smb.msgs--
if sz > smb.bytes {
sz = smb.bytes
}
smb.bytes -= sz
bytes += sz
purged++
}
// Update fss
smb.removeSeqPerSubject(sm.subj, mseq)
fs.removePerSubject(sm.subj)
tombs = append(tombs, msgId{sm.seq, sm.ts})
}
}
// Check if empty after processing, could happen if tail of messages are all deleted.
if isEmpty := smb.msgs == 0; isEmpty {
// Only remove if not the last block.
if smb != fs.lmb {
smb.dirtyCloseWithRemove(true)
deleted++
} else {
// Make sure to sync changes.
smb.needSync = true
}
// Update fs first here as well.
fs.state.FirstSeq = atomic.LoadUint64(&smb.last.seq) + 1
fs.state.FirstTime = time.Time{}
} else {
// Make sure to sync changes.
smb.needSync = true
// Just for start condition for selectNextFirst.
if smb.first.seq < seq {
atomic.StoreUint64(&smb.first.seq, seq-1)
} else {
// selectNextFirst always adds 1, so need to subtract 1 here.
atomic.StoreUint64(&smb.first.seq, smb.first.seq-1)
}
smb.selectNextFirst()
fs.state.FirstSeq = atomic.LoadUint64(&smb.first.seq)
fs.state.FirstTime = time.Unix(0, smb.first.ts).UTC()
// Check if we should reclaim the head space from this block.
// This will be optimistic only, so don't continue if we encounter any errors here.
if smb.rbytes > compactMinimum && smb.bytes*2 < smb.rbytes {
var moff uint32
moff, _, _, err = smb.slotInfo(int(atomic.LoadUint64(&smb.first.seq) - smb.cache.fseq))
if err != nil || moff >= uint32(len(smb.cache.buf)) {
goto SKIP
}
buf := smb.cache.buf[moff:]
// Don't reuse, copy to new recycled buf.
nbuf := getMsgBlockBuf(len(buf))
nbuf = append(nbuf, buf...)
smb.closeFDsLockedNoCheck()
// Check for encryption.
if smb.bek != nil && len(nbuf) > 0 {
// Recreate to reset counter.
bek, err := genBlockEncryptionKey(smb.fs.fcfg.Cipher, smb.seed, smb.nonce)
if err != nil {
goto SKIP
}
// For future writes make sure to set smb.bek to keep counter correct.
smb.bek = bek
smb.bek.XORKeyStream(nbuf, nbuf)
}
// Recompress if necessary (smb.cmp contains the algorithm used when
// the block was loaded from disk, or defaults to NoCompression if not)
if nbuf, err = smb.cmp.Compress(nbuf); err != nil {
goto SKIP
}
// We will write to a new file and mv/rename it in case of failure.
mfn := filepath.Join(smb.fs.fcfg.StoreDir, msgDir, fmt.Sprintf(newScan, smb.index))
<-dios
err := os.WriteFile(mfn, nbuf, defaultFilePerms)
dios <- struct{}{}
if err != nil {
os.Remove(mfn)
goto SKIP
}
if err := os.Rename(mfn, smb.mfn); err != nil {
os.Remove(mfn)
goto SKIP
}
// Make sure to remove fss state.
smb.fss = nil
smb.clearCacheAndOffset()
smb.rbytes = uint64(len(nbuf))
// Make sure we don't write any additional tombstones.
tombs = nil
}
}
SKIP:
smb.mu.Unlock()
// Write any tombstones as needed.
// When writing multiple tombstones we will flush at the end.
if len(tombs) > 0 {
for _, tomb := range tombs {
if err := fs.writeTombstoneNoFlush(tomb.seq, tomb.ts); err != nil {
return purged, err
}
}
// Flush any pending. If we change blocks the newMsgBlockForWrite() will flush any pending for us.
if lmb := fs.lmb; lmb != nil {
lmb.flushPendingMsgs()
}
}
if deleted > 0 {
// Update block map.
if fs.bim != nil {
for _, mb := range fs.blks[:deleted] {
delete(fs.bim, mb.index)
}
}
// Update blks slice.
fs.blks = copyMsgBlocks(fs.blks[deleted:])
if lb := len(fs.blks); lb == 0 {
fs.lmb = nil
} else {
fs.lmb = fs.blks[lb-1]
}
}
// Update top level accounting.
if purged > fs.state.Msgs {
purged = fs.state.Msgs
}
fs.state.Msgs -= purged
if fs.state.Msgs == 0 {
fs.state.FirstSeq = fs.state.LastSeq + 1
fs.state.FirstTime = time.Time{}
}
if bytes > fs.state.Bytes {
bytes = fs.state.Bytes
}
fs.state.Bytes -= bytes
// Any existing state file no longer applicable. We will force write a new one
// after we release the lock.
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
fs.dirty++
cb := fs.scb
fs.mu.Unlock()
// Force a new index.db to be written.
if purged > 0 {
fs.forceWriteFullState()
}
if cb != nil && purged > 0 {
cb(-int64(purged), -int64(bytes), 0, _EMPTY_)
}
return purged, err
}
// Will completely reset our store.
func (fs *fileStore) reset() error {
if fs.isClosed() {
return ErrStoreClosed
}
fs.mu.Lock()
var purged, bytes uint64
cb := fs.scb
for _, mb := range fs.blks {
mb.mu.Lock()
purged += mb.msgs
bytes += mb.bytes
mb.dirtyCloseWithRemove(true)
mb.mu.Unlock()
}
// Reset
fs.state.FirstSeq = 0
fs.state.FirstTime = time.Time{}
fs.state.LastSeq = 0
fs.state.LastTime = time.Now().UTC()
// Update msgs and bytes.
fs.state.Msgs = 0
fs.state.Bytes = 0
// Reset blocks.
fs.blks, fs.lmb = nil, nil
// Reset subject mappings.
fs.psim, fs.tsl = fs.psim.Empty(), 0
fs.sdm.empty()
fs.bim = make(map[uint32]*msgBlock)
// If we purged anything, make sure we kick flush state loop.
if purged > 0 {
fs.dirty++
}
fs.mu.Unlock()
if cb != nil {
cb(-int64(purged), -int64(bytes), 0, _EMPTY_)
}
return nil
}
// Return all active tombstones in this msgBlock.
func (mb *msgBlock) tombs() []msgId {
mb.mu.Lock()
defer mb.mu.Unlock()
return mb.tombsLocked()
}
// Return all active tombstones in this msgBlock.
// Write lock should be held.
func (mb *msgBlock) tombsLocked() []msgId {
if mb.cacheNotLoaded() {
if err := mb.loadMsgsWithLock(); err != nil {
return nil
}
}
var tombs []msgId
var le = binary.LittleEndian
buf := mb.cache.buf
for index, lbuf := uint32(0), uint32(len(buf)); index < lbuf; {
if index+msgHdrSize > lbuf {
return tombs
}
hdr := buf[index : index+msgHdrSize]
rl, seq := le.Uint32(hdr[0:]), le.Uint64(hdr[4:])
// Clear any headers bit that could be set.
rl &^= hbit
// Check for tombstones.
if seq&tbit != 0 {
ts := int64(le.Uint64(hdr[12:]))
tombs = append(tombs, msgId{seq &^ tbit, ts})
}
// Advance to next record.
index += rl
}
return tombs
}
// Truncate will truncate a stream store up to seq. Sequence needs to be valid.
func (fs *fileStore) Truncate(seq uint64) error {
if fs.isClosed() {
return ErrStoreClosed
}
// Check for request to reset.
if seq == 0 {
return fs.reset()
}
fs.mu.Lock()
// Any existing state file will no longer be applicable. We will force write a new one
// at the end, after we release the lock.
os.Remove(filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile))
var lsm *StoreMsg
smb := fs.selectMsgBlock(seq)
if smb != nil {
lsm, _, _ = smb.fetchMsgNoCopy(seq, nil)
}
// Reset last so new block doesn't contain truncated sequences/timestamps.
var lastTime int64
if lsm != nil {
lastTime = lsm.ts
} else if smb != nil {
lastTime = smb.last.ts
} else {
lastTime = fs.state.LastTime.UnixNano()
}
fs.state.LastSeq = seq
fs.state.LastTime = time.Unix(0, lastTime).UTC()
// Always create a new write block for any tombstones.
// We'll truncate the selected message block as the last step, so can't write tombstones to it.
// If we end up not needing to write tombstones, this block will be cleaned up at the end.
tmb, err := fs.newMsgBlockForWrite()
if err != nil {
return err
}
// The selected message block needs to be removed if it needs to be fully truncated.
var removeSmb bool
if smb != nil {
removeSmb = atomic.LoadUint64(&smb.first.seq) > seq
}
// If the selected block is not found or the message was deleted, we'll need to write a tombstone
// at the truncated sequence so we don't roll backward on our last sequence and timestamp.
if lsm == nil || removeSmb {
fs.writeTombstone(seq, lastTime)
}
var purged, bytes uint64
// Remove any left over msg blocks.
getLastMsgBlock := func() *msgBlock {
// Start at one before last, tmb will be the last most of the time
// unless a new block gets added for tombstones.
for i := len(fs.blks) - 2; i >= 0; i-- {
if mb := fs.blks[i]; mb.index < tmb.index {
return mb
}
}
return nil
}
for mb := getLastMsgBlock(); mb != nil && mb != smb; mb = getLastMsgBlock() {
mb.mu.Lock()
purged += mb.msgs
bytes += mb.bytes
// We could have tombstones for messages before the truncated sequence.
// Need to store those for blocks we're about to remove.
if tombs := mb.tombsLocked(); len(tombs) > 0 {
// Temporarily unlock while we write tombstones.
mb.mu.Unlock()
for _, tomb := range tombs {
if tomb.seq < seq {
fs.writeTombstone(tomb.seq, tomb.ts)
}
}
mb.mu.Lock()
}
fs.forceRemoveMsgBlock(mb)
mb.mu.Unlock()
}
hasWrittenTombstones := len(tmb.tombs()) > 0
if smb != nil {
smb.mu.Lock()
if removeSmb {
purged += smb.msgs
bytes += smb.bytes
// We could have tombstones for messages before the truncated sequence.
if tombs := smb.tombsLocked(); len(tombs) > 0 {
// Temporarily unlock while we write tombstones.
smb.mu.Unlock()
for _, tomb := range tombs {
if tomb.seq < seq {
fs.writeTombstone(tomb.seq, tomb.ts)
}
}
smb.mu.Lock()
}
fs.forceRemoveMsgBlock(smb)
smb.mu.Unlock()
goto SKIP
}
// Make sure writeable.
if err := smb.enableForWriting(fs.fip); err != nil {
smb.mu.Unlock()
fs.mu.Unlock()
return err
}
// Truncate our selected message block.
nmsgs, nbytes, err := smb.truncate(seq, lastTime)
if err != nil {
smb.mu.Unlock()
fs.mu.Unlock()
return fmt.Errorf("smb.truncate: %w", err)
}
// Account for the truncated msgs and bytes.
purged += nmsgs
bytes += nbytes
// The selected message block is not the last anymore, need to close down resources.
if hasWrittenTombstones {
// Quit our loops.
if smb.qch != nil {
close(smb.qch)
smb.qch = nil
}
smb.closeFDsLockedNoCheck()
smb.recompressOnDiskIfNeeded()
}
smb.mu.Unlock()
}
SKIP:
// If no tombstones were written, we can remove the block and
// purely rely on the selected block as the last block.
if !hasWrittenTombstones {
fs.lmb = smb
tmb.mu.Lock()
fs.forceRemoveMsgBlock(tmb)
tmb.mu.Unlock()
}
// Reset last.
fs.state.LastSeq = seq
fs.state.LastTime = time.Unix(0, lastTime).UTC()
// Update msgs and bytes.
if purged > fs.state.Msgs {
purged = fs.state.Msgs
}
fs.state.Msgs -= purged
if bytes > fs.state.Bytes {
bytes = fs.state.Bytes
}
fs.state.Bytes -= bytes
// Reset our subject lookup info.
fs.resetGlobalPerSubjectInfo()
fs.dirty++
cb := fs.scb
fs.mu.Unlock()
// Force a new index.db to be written.
if purged > 0 {
fs.forceWriteFullState()
}
if cb != nil {
cb(-int64(purged), -int64(bytes), 0, _EMPTY_)
}
return nil
}
func (fs *fileStore) lastSeq() uint64 {
fs.mu.RLock()
seq := fs.state.LastSeq
fs.mu.RUnlock()
return seq
}
// Returns number of msg blks.
func (fs *fileStore) numMsgBlocks() int {
fs.mu.RLock()
defer fs.mu.RUnlock()
return len(fs.blks)
}
// Will add a new msgBlock.
// Lock should be held.
func (fs *fileStore) addMsgBlock(mb *msgBlock) {
fs.blks = append(fs.blks, mb)
fs.lmb = mb
fs.bim[mb.index] = mb
}
// Remove from our list of blks.
// Both locks should be held.
func (fs *fileStore) removeMsgBlockFromList(mb *msgBlock) {
// Remove from list.
for i, omb := range fs.blks {
if mb == omb {
fs.dirty++
blks := append(fs.blks[:i], fs.blks[i+1:]...)
fs.blks = copyMsgBlocks(blks)
if fs.bim != nil {
delete(fs.bim, mb.index)
}
break
}
}
}
// Removes the msgBlock
// Both locks should be held.
func (fs *fileStore) removeMsgBlock(mb *msgBlock) {
// Check for us being last message block
lseq, lts := atomic.LoadUint64(&mb.last.seq), mb.last.ts
if mb == fs.lmb {
// Creating a new message write block requires that the lmb lock is not held.
mb.mu.Unlock()
// Write the tombstone to remember since this was last block.
if lmb, _ := fs.newMsgBlockForWrite(); lmb != nil {
fs.writeTombstone(lseq, lts)
}
mb.mu.Lock()
} else if lseq == fs.state.LastSeq {
// Need to write a tombstone for the last sequence if we're removing the block containing it.
fs.writeTombstone(lseq, lts)
}
// Only delete message block after (potentially) writing a tombstone.
fs.forceRemoveMsgBlock(mb)
}
// Removes the msgBlock, without writing tombstones to ensure the last sequence is preserved.
// Both locks should be held.
func (fs *fileStore) forceRemoveMsgBlock(mb *msgBlock) {
mb.dirtyCloseWithRemove(true)
fs.removeMsgBlockFromList(mb)
}
// Purges and removes the msgBlock from the store.
// Lock should be held.
func (fs *fileStore) purgeMsgBlock(mb *msgBlock) {
mb.mu.Lock()
// Adjust per-subject tracking if present.
if err := mb.ensurePerSubjectInfoLoaded(); err == nil && mb.fss != nil {
mb.fss.IterFast(func(bsubj []byte, ss *SimpleState) bool {
subj := bytesToString(bsubj)
for range ss.Msgs {
fs.removePerSubject(subj)
}
return true
})
}
// Update top level accounting.
msgs, bytes := mb.msgs, mb.bytes
if msgs > fs.state.Msgs {
msgs = fs.state.Msgs
}
if bytes > fs.state.Bytes {
bytes = fs.state.Bytes
}
fs.state.Msgs -= msgs
fs.state.Bytes -= bytes
fs.removeMsgBlock(mb)
mb.tryForceExpireCacheLocked()
mb.mu.Unlock()
fs.selectNextFirst()
if cb := fs.scb; cb != nil {
// If we have a callback registered, we need to release lock regardless since consumers will recalculate pending.
fs.mu.Unlock()
// Storage updates.
cb(-int64(msgs), -int64(bytes), 0, _EMPTY_)
fs.mu.Lock()
}
}
// Called by purge to simply get rid of the cache and close our fds.
// Lock should not be held.
func (mb *msgBlock) dirtyClose() {
mb.mu.Lock()
defer mb.mu.Unlock()
mb.dirtyCloseWithRemove(false)
}
// Should be called with lock held.
func (mb *msgBlock) dirtyCloseWithRemove(remove bool) error {
if mb == nil {
return nil
}
// Stop cache expiration timer.
if mb.ctmr != nil {
mb.ctmr.Stop()
mb.ctmr = nil
}
// Close cache
mb.clearCacheAndOffset()
// Quit our loops.
if mb.qch != nil {
close(mb.qch)
mb.qch = nil
}
if mb.mfd != nil {
mb.mfd.Close()
mb.mfd = nil
}
if remove {
// Clear any tracking by subject if we are removing.
mb.fss = nil
if mb.mfn != _EMPTY_ {
err := os.Remove(mb.mfn)
if isPermissionError(err) {
return err
}
mb.mfn = _EMPTY_
}
if mb.kfn != _EMPTY_ {
err := os.Remove(mb.kfn)
if isPermissionError(err) {
return err
}
}
}
return nil
}
// Remove a seq from the fss and select new first.
// Lock should be held.
func (mb *msgBlock) removeSeqPerSubject(subj string, seq uint64) uint64 {
mb.ensurePerSubjectInfoLoaded()
if mb.fss == nil {
return 0
}
bsubj := stringToBytes(subj)
ss, ok := mb.fss.Find(bsubj)
if !ok || ss == nil {
return 0
}
mb.fs.sdm.removeSeqAndSubject(seq, subj)
if ss.Msgs == 1 {
mb.fss.Delete(bsubj)
return 0
}
ss.Msgs--
// Only one left.
if ss.Msgs == 1 {
if !ss.lastNeedsUpdate && seq != ss.Last {
ss.First = ss.Last
ss.firstNeedsUpdate = false
return 1
}
if !ss.firstNeedsUpdate && seq != ss.First {
ss.Last = ss.First
ss.lastNeedsUpdate = false
return 1
}
}
// We can lazily calculate the first/last sequence when needed.
ss.firstNeedsUpdate = seq == ss.First || ss.firstNeedsUpdate
ss.lastNeedsUpdate = seq == ss.Last || ss.lastNeedsUpdate
return ss.Msgs
}
// Will recalculate the first and/or last sequence for this subject in this block.
// Will avoid slower path message lookups and scan the cache directly instead.
func (mb *msgBlock) recalculateForSubj(subj string, ss *SimpleState) {
// Need to make sure messages are loaded.
if mb.cacheNotLoaded() {
if err := mb.loadMsgsWithLock(); err != nil {
return
}
}
startSlot := int(ss.First - mb.cache.fseq)
if startSlot < 0 {
startSlot = 0
}
if startSlot >= len(mb.cache.idx) {
ss.First = ss.Last
ss.firstNeedsUpdate = false
ss.lastNeedsUpdate = false
return
}
endSlot := int(ss.Last - mb.cache.fseq)
if endSlot < 0 {
endSlot = 0
}
if endSlot >= len(mb.cache.idx) || startSlot > endSlot {
return
}
var le = binary.LittleEndian
if ss.firstNeedsUpdate {
// Mark first as updated.
ss.firstNeedsUpdate = false
fseq := ss.First + 1
if mbFseq := atomic.LoadUint64(&mb.first.seq); fseq < mbFseq {
fseq = mbFseq
}
for slot := startSlot; slot < len(mb.cache.idx); slot++ {
bi := mb.cache.idx[slot] &^ cbit
if bi == dbit {
// delete marker so skip.
continue
}
li := int(bi) - mb.cache.off
if li >= len(mb.cache.buf) {
ss.First = ss.Last
// Only need to reset ss.lastNeedsUpdate, ss.firstNeedsUpdate is already reset above.
ss.lastNeedsUpdate = false
return
}
buf := mb.cache.buf[li:]
hdr := buf[:msgHdrSize]
slen := int(le.Uint16(hdr[20:]))
if subj == bytesToString(buf[msgHdrSize:msgHdrSize+slen]) {
seq := le.Uint64(hdr[4:])
if seq < fseq || seq&ebit != 0 || mb.dmap.Exists(seq) {
continue
}
ss.First = seq
if ss.Msgs == 1 {
ss.Last = seq
ss.lastNeedsUpdate = false
return
}
// Skip the start slot ahead, if we need to recalculate last we can stop early.
startSlot = slot
break
}
}
}
if ss.lastNeedsUpdate {
// Mark last as updated.
ss.lastNeedsUpdate = false
lseq := ss.Last - 1
if mbLseq := atomic.LoadUint64(&mb.last.seq); lseq > mbLseq {
lseq = mbLseq
}
for slot := endSlot; slot >= startSlot; slot-- {
bi := mb.cache.idx[slot] &^ cbit
if bi == dbit {
// delete marker so skip.
continue
}
li := int(bi) - mb.cache.off
if li >= len(mb.cache.buf) {
// Can't overwrite ss.Last, just skip.
return
}
buf := mb.cache.buf[li:]
hdr := buf[:msgHdrSize]
slen := int(le.Uint16(hdr[20:]))
if subj == bytesToString(buf[msgHdrSize:msgHdrSize+slen]) {
seq := le.Uint64(hdr[4:])
if seq > lseq || seq&ebit != 0 || mb.dmap.Exists(seq) {
continue
}
// Sequence should never be lower, but guard against it nonetheless.
if seq < ss.First {
seq = ss.First
}
ss.Last = seq
if ss.Msgs == 1 {
ss.First = seq
ss.firstNeedsUpdate = false
}
return
}
}
}
}
// Lock should be held.
func (fs *fileStore) resetGlobalPerSubjectInfo() {
// Clear any global subject state.
fs.psim, fs.tsl = fs.psim.Empty(), 0
if fs.noTrackSubjects() {
return
}
for _, mb := range fs.blks {
fs.populateGlobalPerSubjectInfo(mb)
}
}
// Lock should be held.
func (mb *msgBlock) resetPerSubjectInfo() error {
mb.fss = nil
return mb.generatePerSubjectInfo()
}
// generatePerSubjectInfo will generate the per subject info via the raw msg block.
// Lock should be held.
func (mb *msgBlock) generatePerSubjectInfo() error {
// Check if this mb is empty. This can happen when its the last one and we are holding onto it for seq and timestamp info.
if mb.msgs == 0 {
return nil
}
if mb.cacheNotLoaded() {
if err := mb.loadMsgsWithLock(); err != nil {
return err
}
// indexCacheBuf can produce fss now, so if non-nil we are good.
if mb.fss != nil {
return nil
}
}
// Create new one regardless.
mb.fss = mb.fss.Empty()
var smv StoreMsg
fseq, lseq := atomic.LoadUint64(&mb.first.seq), atomic.LoadUint64(&mb.last.seq)
for seq := fseq; seq <= lseq; seq++ {
if mb.dmap.Exists(seq) {
// Optimisation to avoid calling cacheLookup which hits time.Now().
// It gets set later on if the fss is non-empty anyway.
continue
}
sm, err := mb.cacheLookupNoCopy(seq, &smv)
if err != nil {
// Since we are walking by sequence we can ignore some errors that are benign to rebuilding our state.
if err == ErrStoreMsgNotFound || err == errDeletedMsg {
continue
}
if err == errNoCache {
return nil
}
return err
}
if sm != nil && len(sm.subj) > 0 {
if ss, ok := mb.fss.Find(stringToBytes(sm.subj)); ok && ss != nil {
ss.Msgs++
ss.Last = seq
ss.lastNeedsUpdate = false
} else {
mb.fss.Insert(stringToBytes(sm.subj), SimpleState{Msgs: 1, First: seq, Last: seq})
}
}
}
if mb.fss.Size() > 0 {
// Make sure we run the cache expire timer.
mb.llts = ats.AccessTime()
// Mark fss activity same as load time.
mb.lsts = mb.llts
mb.startCacheExpireTimer()
}
return nil
}
// Helper to make sure fss loaded if we are tracking.
// Lock should be held
func (mb *msgBlock) ensurePerSubjectInfoLoaded() error {
if mb.fss != nil || mb.noTrack {
if mb.fss != nil {
// Mark fss activity.
mb.lsts = ats.AccessTime()
}
return nil
}
if mb.msgs == 0 {
mb.fss = stree.NewSubjectTree[SimpleState]()
return nil
}
return mb.generatePerSubjectInfo()
}
// Called on recovery to populate the global psim state.
// Lock should be held.
func (fs *fileStore) populateGlobalPerSubjectInfo(mb *msgBlock) {
mb.mu.Lock()
defer mb.mu.Unlock()
if err := mb.ensurePerSubjectInfoLoaded(); err != nil {
return
}
// Now populate psim.
mb.fss.IterFast(func(bsubj []byte, ss *SimpleState) bool {
if len(bsubj) > 0 {
if info, ok := fs.psim.Find(bsubj); ok {
info.total += ss.Msgs
if mb.index > info.lblk {
info.lblk = mb.index
}
} else {
fs.psim.Insert(bsubj, psi{total: ss.Msgs, fblk: mb.index, lblk: mb.index})
fs.tsl += len(bsubj)
}
}
return true
})
}
// Close the message block.
func (mb *msgBlock) close(sync bool) {
if mb == nil {
return
}
mb.mu.Lock()
defer mb.mu.Unlock()
if mb.closed {
return
}
// Stop cache expiration timer.
if mb.ctmr != nil {
mb.ctmr.Stop()
mb.ctmr = nil
}
// Clear fss.
mb.fss = nil
// Close cache
mb.clearCacheAndOffset()
// Quit our loops.
if mb.qch != nil {
close(mb.qch)
mb.qch = nil
}
if mb.mfd != nil {
if sync {
mb.mfd.Sync()
}
mb.mfd.Close()
}
mb.mfd = nil
// Mark as closed.
mb.closed = true
}
func (fs *fileStore) closeAllMsgBlocks(sync bool) {
for _, mb := range fs.blks {
mb.close(sync)
}
}
func (fs *fileStore) Delete() error {
if fs.isClosed() {
// Always attempt to remove since we could have been closed beforehand.
os.RemoveAll(fs.fcfg.StoreDir)
// Since we did remove, if we did have anything remaining make sure to
// call into any storage updates that had been registered.
fs.mu.Lock()
cb, msgs, bytes := fs.scb, int64(fs.state.Msgs), int64(fs.state.Bytes)
// Guard against double accounting if called twice.
fs.state.Msgs, fs.state.Bytes = 0, 0
fs.mu.Unlock()
if msgs > 0 && cb != nil {
cb(-msgs, -bytes, 0, _EMPTY_)
}
return ErrStoreClosed
}
pdir := filepath.Join(fs.fcfg.StoreDir, purgeDir)
// If purge directory still exists then we need to wait
// in place and remove since rename would fail.
if _, err := os.Stat(pdir); err == nil {
os.RemoveAll(pdir)
}
// Quickly close all blocks and simulate a purge w/o overhead an new write block.
fs.mu.Lock()
for _, mb := range fs.blks {
mb.dirtyClose()
}
dmsgs := fs.state.Msgs
dbytes := int64(fs.state.Bytes)
fs.state.Msgs, fs.state.Bytes = 0, 0
fs.blks = nil
cb := fs.scb
fs.mu.Unlock()
if cb != nil {
cb(-int64(dmsgs), -dbytes, 0, _EMPTY_)
}
if err := fs.stop(true, false); err != nil {
return err
}
// Make sure we will not try to recover if killed before removal below completes.
if err := os.Remove(filepath.Join(fs.fcfg.StoreDir, JetStreamMetaFile)); err != nil {
return err
}
// Now move into different directory with "." prefix.
ndir := filepath.Join(filepath.Dir(fs.fcfg.StoreDir), tsep+filepath.Base(fs.fcfg.StoreDir))
if err := os.Rename(fs.fcfg.StoreDir, ndir); err != nil {
return err
}
// Do this in separate Go routine in case lots of blocks.
// Purge above protects us as does the removal of meta artifacts above.
go func() {
<-dios
err := os.RemoveAll(ndir)
dios <- struct{}{}
if err == nil {
return
}
ttl := time.Now().Add(time.Second)
for time.Now().Before(ttl) {
time.Sleep(10 * time.Millisecond)
<-dios
err = os.RemoveAll(ndir)
dios <- struct{}{}
if err == nil {
return
}
}
}()
return nil
}
// Lock should be held.
func (fs *fileStore) setSyncTimer() {
if fs.syncTmr != nil {
fs.syncTmr.Reset(fs.fcfg.SyncInterval)
} else {
// First time this fires will be between SyncInterval/2 and SyncInterval,
// so that different stores are spread out, rather than having many of
// them trying to all sync at once, causing blips and contending dios.
start := (fs.fcfg.SyncInterval / 2) + (time.Duration(mrand.Int63n(int64(fs.fcfg.SyncInterval / 2))))
fs.syncTmr = time.AfterFunc(start, fs.syncBlocks)
}
}
// Lock should be held.
func (fs *fileStore) cancelSyncTimer() {
if fs.syncTmr != nil {
fs.syncTmr.Stop()
fs.syncTmr = nil
}
}
// The full state file is versioned.
// - 0x1: original binary index.db format
// - 0x2: adds support for TTL count field after num deleted
const (
fullStateMagic = uint8(11)
fullStateMinVersion = uint8(1) // What is the minimum version we know how to parse?
fullStateVersion = uint8(2) // What is the current version written out to index.db?
)
// This go routine periodically writes out our full stream state index.
func (fs *fileStore) flushStreamStateLoop(qch, done chan struct{}) {
// Signal we are done on exit.
defer close(done)
// Make sure we do not try to write these out too fast.
// Spread these out for large numbers on a server restart.
const writeThreshold = 2 * time.Minute
writeJitter := time.Duration(mrand.Int63n(int64(30 * time.Second)))
t := time.NewTicker(writeThreshold + writeJitter)
defer t.Stop()
for {
select {
case <-t.C:
err := fs.writeFullState()
if isPermissionError(err) && fs.srv != nil {
fs.warn("File system permission denied when flushing stream state, disabling JetStream: %v", err)
// messages in block cache could be lost in the worst case.
// In the clustered mode it is very highly unlikely as a result of replication.
fs.srv.DisableJetStream()
return
}
case <-qch:
return
}
}
}
// Helper since unixnano of zero time undefined.
func timestampNormalized(t time.Time) int64 {
if t.IsZero() {
return 0
}
return t.UnixNano()
}
// writeFullState will proceed to write the full meta state iff not complex and time consuming.
// Since this is for quick recovery it is optional and should not block/stall normal operations.
func (fs *fileStore) writeFullState() error {
return fs._writeFullState(false, true)
}
// forceWriteFullState will proceed to write the full meta state.
func (fs *fileStore) forceWriteFullState() error {
return fs._writeFullState(true, true)
}
// forceWriteFullStateLocked will proceed to write the full meta state. This should only be called by stop()
func (fs *fileStore) forceWriteFullStateLocked() error {
return fs._writeFullState(true, false)
}
// This will write the full binary state for the stream.
// This plus everything new since last hash will be the total recovered state.
// This state dump will have the following.
// 1. Stream summary - Msgs, Bytes, First and Last (Sequence and Timestamp)
// 2. PSIM - Per Subject Index Map - Tracks first and last blocks with subjects present.
// 3. MBs - Index, Bytes, First and Last Sequence and Timestamps, and the deleted map (avl.seqset).
// 4. Last block index and hash of record inclusive to this stream state.
func (fs *fileStore) _writeFullState(force, needLock bool) error {
if fs.isClosed() {
return nil
}
fsLock := func() {
if needLock {
fs.mu.Lock()
}
}
fsUnlock := func() {
if needLock {
fs.mu.Unlock()
}
}
start := time.Now()
fsLock()
if fs.dirty == 0 {
fsUnlock()
return nil
}
// For calculating size and checking time costs for non forced calls.
numSubjects := fs.numSubjects()
// If we are not being forced to write out our state, check the complexity for time costs as to not
// block or stall normal operations.
// We will base off of number of subjects and interior deletes. A very large number of msg blocks could also
// be used, but for next server version will redo all meta handling to be disk based. So this is temporary.
if !force {
const numThreshold = 1_000_000
// Calculate interior deletes.
var numDeleted int
if fs.state.LastSeq > fs.state.FirstSeq {
numDeleted = int((fs.state.LastSeq - fs.state.FirstSeq + 1) - fs.state.Msgs)
}
if numSubjects > numThreshold || numDeleted > numThreshold {
fsUnlock()
return errStateTooBig
}
}
// We track this through subsequent runs to get an avg per blk used for subsequent runs.
avgDmapLen := fs.adml
// If first time through could be 0
if avgDmapLen == 0 && ((fs.state.LastSeq-fs.state.FirstSeq+1)-fs.state.Msgs) > 0 {
avgDmapLen = 1024
}
// Calculate and estimate of the uper bound on the size to avoid multiple allocations.
sz := hdrLen + // Magic and Version
(binary.MaxVarintLen64 * 6) + // FS data
binary.MaxVarintLen64 + fs.tsl + // NumSubjects + total subject length
numSubjects*(binary.MaxVarintLen64*4) + // psi record
binary.MaxVarintLen64 + // Num blocks.
len(fs.blks)*((binary.MaxVarintLen64*8)+avgDmapLen) + // msg blocks, avgDmapLen is est for dmaps
binary.MaxVarintLen64 + 8 + 8 // last index + record checksum + full state checksum
// Do 4k on stack if possible.
const ssz = 4 * 1024
var buf []byte
if sz <= ssz {
var _buf [ssz]byte
buf, sz = _buf[0:hdrLen:ssz], ssz
} else {
buf = make([]byte, hdrLen, sz)
}
buf[0], buf[1] = fullStateMagic, fullStateVersion
buf = binary.AppendUvarint(buf, fs.state.Msgs)
buf = binary.AppendUvarint(buf, fs.state.Bytes)
buf = binary.AppendUvarint(buf, fs.state.FirstSeq)
buf = binary.AppendVarint(buf, timestampNormalized(fs.state.FirstTime))
buf = binary.AppendUvarint(buf, fs.state.LastSeq)
buf = binary.AppendVarint(buf, timestampNormalized(fs.state.LastTime))
// Do per subject information map if applicable.
buf = binary.AppendUvarint(buf, uint64(numSubjects))
if numSubjects > 0 {
fs.psim.Match([]byte(fwcs), func(subj []byte, psi *psi) {
buf = binary.AppendUvarint(buf, uint64(len(subj)))
buf = append(buf, subj...)
buf = binary.AppendUvarint(buf, psi.total)
buf = binary.AppendUvarint(buf, uint64(psi.fblk))
if psi.total > 1 {
buf = binary.AppendUvarint(buf, uint64(psi.lblk))
}
})
}
// Now walk all blocks and write out first and last and optional dmap encoding.
var lbi uint32
var lchk [8]byte
nb := len(fs.blks)
buf = binary.AppendUvarint(buf, uint64(nb))
// Use basetime to save some space.
baseTime := timestampNormalized(fs.state.FirstTime)
var scratch [8 * 1024]byte
// Track the state as represented by the mbs.
var mstate StreamState
var dmapTotalLen int
for _, mb := range fs.blks {
mb.mu.RLock()
buf = binary.AppendUvarint(buf, uint64(mb.index))
buf = binary.AppendUvarint(buf, mb.bytes)
buf = binary.AppendUvarint(buf, atomic.LoadUint64(&mb.first.seq))
buf = binary.AppendVarint(buf, mb.first.ts-baseTime)
buf = binary.AppendUvarint(buf, atomic.LoadUint64(&mb.last.seq))
buf = binary.AppendVarint(buf, mb.last.ts-baseTime)
numDeleted := mb.dmap.Size()
buf = binary.AppendUvarint(buf, uint64(numDeleted))
buf = binary.AppendUvarint(buf, mb.ttls) // Field is new in version 2
if numDeleted > 0 {
dmap, _ := mb.dmap.Encode(scratch[:0])
dmapTotalLen += len(dmap)
buf = append(buf, dmap...)
}
// If this is the last one grab the last checksum and the block index, e.g. 22.blk, 22 is the block index.
// We use this to quickly open this file on recovery.
if mb == fs.lmb {
lbi = mb.index
mb.ensureLastChecksumLoaded()
copy(lchk[0:], mb.lchk[:])
}
updateTrackingState(&mstate, mb)
mb.mu.RUnlock()
}
if dmapTotalLen > 0 {
fs.adml = dmapTotalLen / len(fs.blks)
}
// Place block index and hash onto the end.
buf = binary.AppendUvarint(buf, uint64(lbi))
buf = append(buf, lchk[:]...)
// Encrypt if needed.
if fs.prf != nil {
if err := fs.setupAEK(); err != nil {
fsUnlock()
return err
}
nonce := make([]byte, fs.aek.NonceSize(), fs.aek.NonceSize()+len(buf)+fs.aek.Overhead())
if n, err := rand.Read(nonce); err != nil {
fsUnlock()
return err
} else if n != len(nonce) {
fsUnlock()
return fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce))
}
buf = fs.aek.Seal(nonce, nonce, buf, nil)
}
fn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)
fs.hh.Reset()
fs.hh.Write(buf)
buf = fs.hh.Sum(buf)
// Snapshot prior dirty count.
priorDirty := fs.dirty
statesEqual := trackingStatesEqual(&fs.state, &mstate)
// Release lock.
fsUnlock()
// Check consistency here.
if !statesEqual {
fs.warn("Stream state encountered internal inconsistency on write")
// Rebuild our fs state from the mb state.
if needLock {
fs.rebuildState(nil)
} else {
fs.rebuildStateLocked(nil)
}
return errCorruptState
}
if cap(buf) > sz {
fs.debug("WriteFullState reallocated from %d to %d", sz, cap(buf))
}
// Only warn about construction time since file write not holding any locks.
if took := time.Since(start); took > time.Minute {
fs.warn("WriteFullState took %v (%d bytes)", took.Round(time.Millisecond), len(buf))
}
// Write our update index.db
// Protect with dios.
<-dios
err := os.WriteFile(fn, buf, defaultFilePerms)
// if file system is not writable isPermissionError is set to true
dios <- struct{}{}
if isPermissionError(err) {
return err
}
// Update dirty if successful.
if err == nil {
fsLock()
fs.dirty -= priorDirty
fsUnlock()
}
return fs.writeTTLState(needLock)
}
func (fs *fileStore) writeTTLState(needLock bool) error {
if needLock {
fs.mu.RLock()
}
if fs.ttls == nil {
if needLock {
fs.mu.RUnlock()
}
return nil
}
fn := filepath.Join(fs.fcfg.StoreDir, msgDir, ttlStreamStateFile)
// Must be lseq+1 to identify up to which sequence the TTLs are valid.
buf := fs.ttls.Encode(fs.state.LastSeq + 1)
if needLock {
fs.mu.RUnlock()
}
return fs.writeFileWithOptionalSync(fn, buf, defaultFilePerms)
}
// Stop the current filestore.
func (fs *fileStore) Stop() error {
return fs.stop(false, true)
}
// Stop the current filestore.
func (fs *fileStore) stop(delete, writeState bool) error {
if fs.isClosed() {
return ErrStoreClosed
}
fs.mu.Lock()
if fs.closing {
fs.mu.Unlock()
return ErrStoreClosed
}
// Mark as closing. Do before releasing the lock to wait on the state flush loop
// so we don't end up with this function running more than once.
fs.closing = true
// Release the state flusher loop.
if fs.qch != nil {
close(fs.qch)
fs.qch = nil
}
if writeState {
// Wait for the state flush loop to exit.
fsld := fs.fsld
fs.mu.Unlock()
<-fsld
fs.mu.Lock()
}
fs.closeAllMsgBlocks(false)
fs.cancelSyncTimer()
fs.cancelAgeChk()
if writeState {
// Write full state if needed. If not dirty this is a no-op.
fs.forceWriteFullStateLocked()
}
// Mark as closed. Last message block needs to be cleared after
// writeFullState has completed.
fs.closed.Store(true)
fs.lmb = nil
// We should update the upper usage layer on a stop.
cb, bytes := fs.scb, int64(fs.state.Bytes)
fs.mu.Unlock()
fs.cmu.Lock()
var _cfs [256]ConsumerStore
cfs := append(_cfs[:0], fs.cfs...)
fs.cfs = nil
fs.cmu.Unlock()
for _, o := range cfs {
if delete {
o.StreamDelete()
} else {
o.Stop()
}
}
if bytes > 0 && cb != nil {
cb(0, -bytes, 0, _EMPTY_)
}
// Unregister from the access time service.
ats.Unregister()
return nil
}
const errFile = "errors.txt"
// Stream our snapshot through S2 compression and tar.
func (fs *fileStore) streamSnapshot(w io.WriteCloser, includeConsumers bool, errCh chan string) {
defer close(errCh)
defer w.Close()
enc := s2.NewWriter(w)
defer enc.Close()
tw := tar.NewWriter(enc)
defer tw.Close()
defer func() {
fs.mu.Lock()
fs.sips--
fs.mu.Unlock()
}()
modTime := time.Now().UTC()
writeFile := func(name string, buf []byte) error {
hdr := &tar.Header{
Name: name,
Mode: 0600,
ModTime: modTime,
Uname: "nats",
Gname: "nats",
Size: int64(len(buf)),
Format: tar.FormatPAX,
}
if err := tw.WriteHeader(hdr); err != nil {
return err
}
if _, err := tw.Write(buf); err != nil {
return err
}
return nil
}
writeErr := func(err string) {
writeFile(errFile, []byte(err))
errCh <- err
}
fs.mu.Lock()
blks := fs.blks
// Grab our general meta data.
// We do this now instead of pulling from files since they could be encrypted.
meta, err := json.Marshal(fs.cfg)
if err != nil {
fs.mu.Unlock()
writeErr(fmt.Sprintf("Could not gather stream meta file: %v", err))
return
}
hh := fs.hh
hh.Reset()
hh.Write(meta)
var hb [highwayhash.Size64]byte
sum := []byte(hex.EncodeToString(fs.hh.Sum(hb[:0])))
fs.mu.Unlock()
// Meta first.
if writeFile(JetStreamMetaFile, meta) != nil {
return
}
if writeFile(JetStreamMetaFileSum, sum) != nil {
return
}
// Can't use join path here, tar only recognizes relative paths with forward slashes.
msgPre := msgDir + "/"
var bbuf []byte
// Now do messages themselves.
for _, mb := range blks {
if mb.pendingWriteSize() > 0 {
mb.flushPendingMsgs()
}
mb.mu.Lock()
// We could stream but don't want to hold the lock and prevent changes, so just read in and
// release the lock for now.
bbuf, err = mb.loadBlock(bbuf)
if err != nil {
mb.mu.Unlock()
writeErr(fmt.Sprintf("Could not read message block [%d]: %v", mb.index, err))
return
}
// Check for encryption.
if mb.bek != nil && len(bbuf) > 0 {
rbek, err := genBlockEncryptionKey(fs.fcfg.Cipher, mb.seed, mb.nonce)
if err != nil {
mb.mu.Unlock()
writeErr(fmt.Sprintf("Could not create encryption key for message block [%d]: %v", mb.index, err))
return
}
rbek.XORKeyStream(bbuf, bbuf)
}
// Check for compression.
if bbuf, err = mb.decompressIfNeeded(bbuf); err != nil {
mb.mu.Unlock()
writeErr(fmt.Sprintf("Could not decompress message block [%d]: %v", mb.index, err))
return
}
mb.mu.Unlock()
// Do this one unlocked.
if writeFile(msgPre+fmt.Sprintf(blkScan, mb.index), bbuf) != nil {
return
}
}
// Do index.db last. We will force a write as well.
// Write out full state as well before proceeding.
if err := fs.forceWriteFullState(); err == nil {
const minLen = 32
sfn := filepath.Join(fs.fcfg.StoreDir, msgDir, streamStreamStateFile)
if buf, err := os.ReadFile(sfn); err == nil && len(buf) >= minLen {
if fs.aek != nil {
ns := fs.aek.NonceSize()
buf, err = fs.aek.Open(nil, buf[:ns], buf[ns:len(buf)-highwayhash.Size64], nil)
if err == nil {
// Redo hash checksum at end on plaintext.
fs.mu.Lock()
hh.Reset()
hh.Write(buf)
buf = fs.hh.Sum(buf)
fs.mu.Unlock()
}
}
if err == nil && writeFile(msgPre+streamStreamStateFile, buf) != nil {
return
}
}
}
// Bail if no consumers requested.
if !includeConsumers {
return
}
// Do consumers' state last.
fs.cmu.RLock()
cfs := fs.cfs
fs.cmu.RUnlock()
for _, cs := range cfs {
o, ok := cs.(*consumerFileStore)
if !ok {
continue
}
o.mu.Lock()
// Grab our general meta data.
// We do this now instead of pulling from files since they could be encrypted.
meta, err := json.Marshal(o.cfg)
if err != nil {
o.mu.Unlock()
writeErr(fmt.Sprintf("Could not gather consumer meta file for %q: %v", o.name, err))
return
}
o.hh.Reset()
o.hh.Write(meta)
var hb [highwayhash.Size64]byte
sum := []byte(hex.EncodeToString(o.hh.Sum(hb[:0])))
// We can have the running state directly encoded now.
state, err := o.encodeState()
if err != nil {
o.mu.Unlock()
writeErr(fmt.Sprintf("Could not encode consumer state for %q: %v", o.name, err))
return
}
odirPre := filepath.Join(consumerDir, o.name)
o.mu.Unlock()
// Write all the consumer files.
if writeFile(filepath.Join(odirPre, JetStreamMetaFile), meta) != nil {
return
}
if writeFile(filepath.Join(odirPre, JetStreamMetaFileSum), sum) != nil {
return
}
writeFile(filepath.Join(odirPre, consumerState), state)
}
}
// Create a snapshot of this stream and its consumer's state along with messages.
func (fs *fileStore) Snapshot(deadline time.Duration, checkMsgs, includeConsumers bool) (*SnapshotResult, error) {
if fs.isClosed() {
return nil, ErrStoreClosed
}
fs.mu.Lock()
// Only allow one at a time.
if fs.sips > 0 {
fs.mu.Unlock()
return nil, ErrStoreSnapshotInProgress
}
// Mark us as snapshotting
fs.sips += 1
fs.mu.Unlock()
if checkMsgs {
ld := fs.checkMsgs()
if ld != nil && len(ld.Msgs) > 0 {
return nil, fmt.Errorf("snapshot check detected %d bad messages", len(ld.Msgs))
}
}
pr, pw := net.Pipe()
// Set a write deadline here to protect ourselves.
if deadline > 0 {
pw.SetWriteDeadline(time.Now().Add(deadline))
}
// We can add to our stream while snapshotting but not "user" delete anything.
var state StreamState
fs.FastState(&state)
// Stream in separate Go routine.
errCh := make(chan string, 1)
go fs.streamSnapshot(pw, includeConsumers, errCh)
return &SnapshotResult{pr, state, errCh}, nil
}
// Helper to return the config.
func (fs *fileStore) fileStoreConfig() FileStoreConfig {
fs.mu.RLock()
defer fs.mu.RUnlock()
return fs.fcfg
}
// Read lock all existing message blocks.
// Lock held on entry.
func (fs *fileStore) readLockAllMsgBlocks() {
for _, mb := range fs.blks {
mb.mu.RLock()
}
}
// Read unlock all existing message blocks.
// Lock held on entry.
func (fs *fileStore) readUnlockAllMsgBlocks() {
for _, mb := range fs.blks {
mb.mu.RUnlock()
}
}
// Binary encoded state snapshot, >= v2.10 server.
func (fs *fileStore) EncodedStreamState(failed uint64) ([]byte, error) {
fs.mu.RLock()
defer fs.mu.RUnlock()
// Calculate deleted.
var numDeleted int64
if fs.state.LastSeq > fs.state.FirstSeq {
numDeleted = int64(fs.state.LastSeq-fs.state.FirstSeq+1) - int64(fs.state.Msgs)
if numDeleted < 0 {
numDeleted = 0
}
}
// Encoded is Msgs, Bytes, FirstSeq, LastSeq, Failed, NumDeleted and optional DeletedBlocks
var buf [1024]byte
buf[0], buf[1] = streamStateMagic, streamStateVersion
n := hdrLen
n += binary.PutUvarint(buf[n:], fs.state.Msgs)
n += binary.PutUvarint(buf[n:], fs.state.Bytes)
n += binary.PutUvarint(buf[n:], fs.state.FirstSeq)
n += binary.PutUvarint(buf[n:], fs.state.LastSeq)
n += binary.PutUvarint(buf[n:], failed)
n += binary.PutUvarint(buf[n:], uint64(numDeleted))
b := buf[0:n]
if numDeleted > 0 {
var scratch [4 * 1024]byte
fs.readLockAllMsgBlocks()
defer fs.readUnlockAllMsgBlocks()
for _, db := range fs.deleteBlocks() {
switch db := db.(type) {
case *DeleteRange:
first, _, num := db.State()
scratch[0] = runLengthMagic
i := 1
i += binary.PutUvarint(scratch[i:], first)
i += binary.PutUvarint(scratch[i:], num)
b = append(b, scratch[0:i]...)
case *avl.SequenceSet:
buf, err := db.Encode(scratch[:0])
if err != nil {
return nil, err
}
b = append(b, buf...)
default:
return nil, errors.New("no impl")
}
}
}
return b, nil
}
// We used to be more sophisticated to save memory, but speed is more important.
// All blocks should be at least read locked.
func (fs *fileStore) deleteBlocks() DeleteBlocks {
var dbs DeleteBlocks
var prevLast uint64
for _, mb := range fs.blks {
// Detect if we have a gap between these blocks.
fseq := atomic.LoadUint64(&mb.first.seq)
if prevLast > 0 && prevLast+1 != fseq {
dbs = append(dbs, &DeleteRange{First: prevLast + 1, Num: fseq - prevLast - 1})
}
if mb.dmap.Size() > 0 {
dbs = append(dbs, &mb.dmap)
}
prevLast = atomic.LoadUint64(&mb.last.seq)
}
return dbs
}
// SyncDeleted will make sure this stream has same deleted state as dbs.
// This will only process deleted state within our current state.
func (fs *fileStore) SyncDeleted(dbs DeleteBlocks) {
if len(dbs) == 0 {
return
}
fs.mu.Lock()
defer fs.mu.Unlock()
lseq := fs.state.LastSeq
var needsCheck DeleteBlocks
fs.readLockAllMsgBlocks()
mdbs := fs.deleteBlocks()
for i, db := range dbs {
first, last, num := db.State()
// If the block is same as what we have we can skip.
if i < len(mdbs) {
eFirst, eLast, eNum := mdbs[i].State()
if first == eFirst && last == eLast && num == eNum {
continue
}
} else if first > lseq {
// Skip blocks not applicable to our current state.
continue
}
// Need to insert these.
needsCheck = append(needsCheck, db)
}
fs.readUnlockAllMsgBlocks()
for _, db := range needsCheck {
db.Range(func(dseq uint64) bool {
fs.removeMsg(dseq, false, true, false)
return true
})
}
}
////////////////////////////////////////////////////////////////////////////////
// Consumers
////////////////////////////////////////////////////////////////////////////////
type consumerFileStore struct {
mu sync.Mutex
fs *fileStore
cfg *FileConsumerInfo
prf keyGen
aek cipher.AEAD
name string
odir string
ifn string
hh *highwayhash.Digest64
state ConsumerState
fch chan struct{}
qch chan struct{}
flusher bool
writing bool
dirty bool
closed bool
}
func (fs *fileStore) ConsumerStore(name string, cfg *ConsumerConfig) (ConsumerStore, error) {
if fs == nil {
return nil, fmt.Errorf("filestore is nil")
}
if fs.isClosed() {
return nil, ErrStoreClosed
}
if cfg == nil || name == _EMPTY_ {
return nil, fmt.Errorf("bad consumer config")
}
// We now allow overrides from a stream being a filestore type and forcing a consumer to be memory store.
if cfg.MemoryStorage {
// Create directly here.
o := &consumerMemStore{ms: fs, cfg: *cfg}
fs.AddConsumer(o)
return o, nil
}
odir := filepath.Join(fs.fcfg.StoreDir, consumerDir, name)
if err := os.MkdirAll(odir, defaultDirPerms); err != nil {
return nil, fmt.Errorf("could not create consumer directory - %v", err)
}
csi := &FileConsumerInfo{Name: name, Created: time.Now().UTC(), ConsumerConfig: *cfg}
o := &consumerFileStore{
fs: fs,
cfg: csi,
prf: fs.prf,
name: name,
odir: odir,
ifn: filepath.Join(odir, consumerState),
}
key := sha256.Sum256([]byte(fs.cfg.Name + "/" + name))
hh, err := highwayhash.NewDigest64(key[:])
if err != nil {
return nil, fmt.Errorf("could not create hash: %v", err)
}
o.hh = hh
// Check for encryption.
if o.prf != nil {
if ekey, err := os.ReadFile(filepath.Join(odir, JetStreamMetaFileKey)); err == nil {
if len(ekey) < minBlkKeySize {
return nil, errBadKeySize
}
// Recover key encryption key.
rb, err := fs.prf([]byte(fs.cfg.Name + tsep + o.name))
if err != nil {
return nil, err
}
sc := fs.fcfg.Cipher
kek, err := genEncryptionKey(sc, rb)
if err != nil {
return nil, err
}
ns := kek.NonceSize()
nonce := ekey[:ns]
seed, err := kek.Open(nil, nonce, ekey[ns:], nil)
if err != nil {
// We may be here on a cipher conversion, so attempt to convert.
if err = o.convertCipher(); err != nil {
return nil, err
}
} else {
o.aek, err = genEncryptionKey(sc, seed)
}
if err != nil {
return nil, err
}
}
}
// Track if we are creating the directory so that we can clean up if we encounter an error.
var didCreate bool
// Write our meta data iff does not exist.
meta := filepath.Join(odir, JetStreamMetaFile)
if _, err := os.Stat(meta); err != nil && os.IsNotExist(err) {
didCreate = true
csi.Created = time.Now().UTC()
if err := o.writeConsumerMeta(); err != nil {
os.RemoveAll(odir)
return nil, err
}
}
// If we expect to be encrypted check that what we are restoring is not plaintext.
// This can happen on snapshot restores or conversions.
if o.prf != nil {
keyFile := filepath.Join(odir, JetStreamMetaFileKey)
if _, err := os.Stat(keyFile); err != nil && os.IsNotExist(err) {
if err := o.writeConsumerMeta(); err != nil {
if didCreate {
os.RemoveAll(odir)
}
return nil, err
}
// Redo the state file as well here if we have one and we can tell it was plaintext.
if buf, err := os.ReadFile(o.ifn); err == nil {
if _, err := decodeConsumerState(buf); err == nil {
state, err := o.encryptState(buf)
if err != nil {
return nil, err
}
err = fs.writeFileWithOptionalSync(o.ifn, state, defaultFilePerms)
if err != nil {
if didCreate {
os.RemoveAll(odir)
}
return nil, err
}
}
}
}
}
// Create channels to control our flush go routine.
o.fch = make(chan struct{}, 1)
o.qch = make(chan struct{})
go o.flushLoop(o.fch, o.qch)
// Make sure to load in our state from disk if needed.
if err = o.loadState(); err != nil {
return nil, err
}
// Assign to filestore.
fs.AddConsumer(o)
return o, nil
}
func (o *consumerFileStore) convertCipher() error {
fs := o.fs
odir := filepath.Join(fs.fcfg.StoreDir, consumerDir, o.name)
ekey, err := os.ReadFile(filepath.Join(odir, JetStreamMetaFileKey))
if err != nil {
return err
}
if len(ekey) < minBlkKeySize {
return errBadKeySize
}
// Recover key encryption key.
rb, err := fs.prf([]byte(fs.cfg.Name + tsep + o.name))
if err != nil {
return err
}
// Do these in reverse since converting.
sc := fs.fcfg.Cipher
osc := AES
if sc == AES {
osc = ChaCha
}
kek, err := genEncryptionKey(osc, rb)
if err != nil {
return err
}
ns := kek.NonceSize()
nonce := ekey[:ns]
seed, err := kek.Open(nil, nonce, ekey[ns:], nil)
if err != nil {
return err
}
aek, err := genEncryptionKey(osc, seed)
if err != nil {
return err
}
// Now read in and decode our state using the old cipher.
buf, err := os.ReadFile(o.ifn)
if err != nil {
return err
}
buf, err = aek.Open(nil, buf[:ns], buf[ns:], nil)
if err != nil {
return err
}
// Since we are here we recovered our old state.
// Now write our meta, which will generate the new keys with the new cipher.
if err := o.writeConsumerMeta(); err != nil {
return err
}
// Now write out or state with the new cipher.
return o.writeState(buf)
}
// Kick flusher for this consumer.
// Lock should be held.
func (o *consumerFileStore) kickFlusher() {
if o.fch != nil {
select {
case o.fch <- struct{}{}:
default:
}
}
o.dirty = true
}
// Set in flusher status
func (o *consumerFileStore) setInFlusher() {
o.mu.Lock()
o.flusher = true
o.mu.Unlock()
}
// Clear in flusher status
func (o *consumerFileStore) clearInFlusher() {
o.mu.Lock()
o.flusher = false
o.mu.Unlock()
}
// Report in flusher status
func (o *consumerFileStore) inFlusher() bool {
o.mu.Lock()
defer o.mu.Unlock()
return o.flusher
}
// flushLoop watches for consumer updates and the quit channel.
func (o *consumerFileStore) flushLoop(fch, qch chan struct{}) {
o.setInFlusher()
defer o.clearInFlusher()
// Maintain approximately 10 updates per second per consumer under load.
const minTime = 100 * time.Millisecond
var lastWrite time.Time
var dt *time.Timer
setDelayTimer := func(addWait time.Duration) {
if dt == nil {
dt = time.NewTimer(addWait)
return
}
if !dt.Stop() {
select {
case <-dt.C:
default:
}
}
dt.Reset(addWait)
}
for {
select {
case <-fch:
if ts := time.Since(lastWrite); ts < minTime {
setDelayTimer(minTime - ts)
select {
case <-dt.C:
case <-qch:
return
}
}
o.mu.Lock()
if o.closed {
o.mu.Unlock()
return
}
buf, err := o.encodeState()
o.mu.Unlock()
if err != nil {
return
}
// TODO(dlc) - if we error should start failing upwards.
if err := o.writeState(buf); err == nil {
lastWrite = time.Now()
}
case <-qch:
return
}
}
}
// SetStarting sets our starting stream sequence.
func (o *consumerFileStore) SetStarting(sseq uint64) error {
o.mu.Lock()
o.state.Delivered.Stream = sseq
buf, err := o.encodeState()
o.mu.Unlock()
if err != nil {
return err
}
return o.writeState(buf)
}
// UpdateStarting updates our starting stream sequence.
func (o *consumerFileStore) UpdateStarting(sseq uint64) {
o.mu.Lock()
defer o.mu.Unlock()
if sseq > o.state.Delivered.Stream {
o.state.Delivered.Stream = sseq
// For AckNone just update delivered and ackfloor at the same time.
if o.cfg.AckPolicy == AckNone {
o.state.AckFloor.Stream = sseq
}
}
// Make sure we flush to disk.
o.kickFlusher()
}
// HasState returns if this store has a recorded state.
func (o *consumerFileStore) HasState() bool {
o.mu.Lock()
// We have a running state, or stored on disk but not yet initialized.
if o.state.Delivered.Consumer != 0 || o.state.Delivered.Stream != 0 {
o.mu.Unlock()
return true
}
_, err := os.Stat(o.ifn)
o.mu.Unlock()
return err == nil
}
// UpdateDelivered is called whenever a new message has been delivered.
func (o *consumerFileStore) UpdateDelivered(dseq, sseq, dc uint64, ts int64) error {
o.mu.Lock()
defer o.mu.Unlock()
if dc != 1 && o.cfg.AckPolicy == AckNone {
return ErrNoAckPolicy
}
// On restarts the old leader may get a replay from the raft logs that are old.
if dseq <= o.state.AckFloor.Consumer {
return nil
}
// See if we expect an ack for this.
if o.cfg.AckPolicy != AckNone {
// Need to create pending records here.
if o.state.Pending == nil {
o.state.Pending = make(map[uint64]*Pending)
}
var p *Pending
// Check for an update to a message already delivered.
if sseq <= o.state.Delivered.Stream {
if p = o.state.Pending[sseq]; p != nil {
// Do not update p.Sequence, that should be the original delivery sequence.
p.Timestamp = ts
}
} else {
// Add to pending.
o.state.Pending[sseq] = &Pending{dseq, ts}
}
// Update delivered as needed.
if dseq > o.state.Delivered.Consumer {
o.state.Delivered.Consumer = dseq
}
if sseq > o.state.Delivered.Stream {
o.state.Delivered.Stream = sseq
}
if dc > 1 {
if maxdc := uint64(o.cfg.MaxDeliver); maxdc > 0 && dc > maxdc {
// Make sure to remove from pending.
delete(o.state.Pending, sseq)
}
if o.state.Redelivered == nil {
o.state.Redelivered = make(map[uint64]uint64)
}
// Only update if greater than what we already have.
if o.state.Redelivered[sseq] < dc-1 {
o.state.Redelivered[sseq] = dc - 1
}
}
} else {
// For AckNone just update delivered and ackfloor at the same time.
if dseq > o.state.Delivered.Consumer {
o.state.Delivered.Consumer = dseq
o.state.AckFloor.Consumer = dseq
}
if sseq > o.state.Delivered.Stream {
o.state.Delivered.Stream = sseq
o.state.AckFloor.Stream = sseq
}
}
// Make sure we flush to disk.
o.kickFlusher()
return nil
}
// UpdateAcks is called whenever a consumer with explicit ack or ack all acks a message.
func (o *consumerFileStore) UpdateAcks(dseq, sseq uint64) error {
o.mu.Lock()
defer o.mu.Unlock()
if o.cfg.AckPolicy == AckNone {
return ErrNoAckPolicy
}
// On restarts the old leader may get a replay from the raft logs that are old.
if dseq <= o.state.AckFloor.Consumer {
return nil
}
if len(o.state.Pending) == 0 || o.state.Pending[sseq] == nil {
delete(o.state.Redelivered, sseq)
return ErrStoreMsgNotFound
}
// Check for AckAll here.
if o.cfg.AckPolicy == AckAll {
sgap := sseq - o.state.AckFloor.Stream
o.state.AckFloor.Consumer = dseq
o.state.AckFloor.Stream = sseq
if sgap > uint64(len(o.state.Pending)) {
for seq := range o.state.Pending {
if seq <= sseq {
delete(o.state.Pending, seq)
delete(o.state.Redelivered, seq)
}
}
} else {
for seq := sseq; seq > sseq-sgap && len(o.state.Pending) > 0; seq-- {
delete(o.state.Pending, seq)
delete(o.state.Redelivered, seq)
}
}
o.kickFlusher()
return nil
}
// AckExplicit
// First delete from our pending state.
if p, ok := o.state.Pending[sseq]; ok {
delete(o.state.Pending, sseq)
if dseq > p.Sequence && p.Sequence > 0 {
dseq = p.Sequence // Use the original.
}
}
if len(o.state.Pending) == 0 {
o.state.AckFloor.Consumer = o.state.Delivered.Consumer
o.state.AckFloor.Stream = o.state.Delivered.Stream
} else if dseq == o.state.AckFloor.Consumer+1 {
o.state.AckFloor.Consumer = dseq
o.state.AckFloor.Stream = sseq
if o.state.Delivered.Consumer > dseq {
for ss := sseq + 1; ss <= o.state.Delivered.Stream; ss++ {
if p, ok := o.state.Pending[ss]; ok {
if p.Sequence > 0 {
o.state.AckFloor.Consumer = p.Sequence - 1
o.state.AckFloor.Stream = ss - 1
}
break
}
}
}
}
// We do these regardless.
delete(o.state.Redelivered, sseq)
o.kickFlusher()
return nil
}
const seqsHdrSize = 6*binary.MaxVarintLen64 + hdrLen
// Encode our consumer state, version 2.
// Lock should be held.
func (o *consumerFileStore) EncodedState() ([]byte, error) {
o.mu.Lock()
defer o.mu.Unlock()
return o.encodeState()
}
func (o *consumerFileStore) encodeState() ([]byte, error) {
// Grab reference to state, but make sure we load in if needed, so do not reference o.state directly.
state, err := o.stateWithCopyLocked(false)
if err != nil {
return nil, err
}
return encodeConsumerState(state), nil
}
func (o *consumerFileStore) UpdateConfig(cfg *ConsumerConfig) error {
o.mu.Lock()
defer o.mu.Unlock()
// This is mostly unchecked here. We are assuming the upper layers have done sanity checking.
csi := o.cfg
csi.ConsumerConfig = *cfg
return o.writeConsumerMeta()
}
func (o *consumerFileStore) Update(state *ConsumerState) error {
// Sanity checks.
if state.AckFloor.Consumer > state.Delivered.Consumer {
return fmt.Errorf("bad ack floor for consumer")
}
if state.AckFloor.Stream > state.Delivered.Stream {
return fmt.Errorf("bad ack floor for stream")
}
// Copy to our state.
var pending map[uint64]*Pending
var redelivered map[uint64]uint64
if len(state.Pending) > 0 {
pending = make(map[uint64]*Pending, len(state.Pending))
for seq, p := range state.Pending {
pending[seq] = &Pending{p.Sequence, p.Timestamp}
if seq <= state.AckFloor.Stream || seq > state.Delivered.Stream {
return fmt.Errorf("bad pending entry, sequence [%d] out of range", seq)
}
}
}
if len(state.Redelivered) > 0 {
redelivered = make(map[uint64]uint64, len(state.Redelivered))
for seq, dc := range state.Redelivered {
redelivered[seq] = dc
}
}
// Replace our state.
o.mu.Lock()
defer o.mu.Unlock()
// Check to see if this is an outdated update.
if state.Delivered.Consumer < o.state.Delivered.Consumer || state.AckFloor.Stream < o.state.AckFloor.Stream {
return fmt.Errorf("old update ignored")
}
o.state.Delivered = state.Delivered
o.state.AckFloor = state.AckFloor
o.state.Pending = pending
o.state.Redelivered = redelivered
o.kickFlusher()
return nil
}
// Will encrypt the state with our asset key. Will be a no-op if encryption not enabled.
// Lock should be held.
func (o *consumerFileStore) encryptState(buf []byte) ([]byte, error) {
if o.aek == nil {
return buf, nil
}
// TODO(dlc) - Optimize on space usage a bit?
nonce := make([]byte, o.aek.NonceSize(), o.aek.NonceSize()+len(buf)+o.aek.Overhead())
if n, err := rand.Read(nonce); err != nil {
return nil, err
} else if n != len(nonce) {
return nil, fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce))
}
return o.aek.Seal(nonce, nonce, buf, nil), nil
}
// Used to limit number of disk IO calls in flight since they could all be blocking an OS thread.
// https://github.com/nats-io/nats-server/issues/2742
var dios chan struct{}
// Used to setup our simplistic counting semaphore using buffered channels.
// golang.org's semaphore seemed a bit heavy.
func init() {
// Limit ourselves to a sensible number of blocking I/O calls. Range between
// 4-16 concurrent disk I/Os based on CPU cores, or 50% of cores if greater
// than 32 cores.
mp := runtime.GOMAXPROCS(-1)
nIO := min(16, max(4, mp))
if mp > 32 {
// If the system has more than 32 cores then limit dios to 50% of cores.
nIO = max(16, min(mp, mp/2))
}
dios = make(chan struct{}, nIO)
// Fill it up to start.
for i := 0; i < nIO; i++ {
dios <- struct{}{}
}
}
func (o *consumerFileStore) writeState(buf []byte) error {
// Check if we have the index file open.
o.mu.Lock()
if o.writing || len(buf) == 0 {
o.mu.Unlock()
return nil
}
// Check on encryption.
if o.aek != nil {
var err error
if buf, err = o.encryptState(buf); err != nil {
return err
}
}
o.writing = true
o.dirty = false
ifn := o.ifn
o.mu.Unlock()
// Lock not held here but we do limit number of outstanding calls that could block OS threads.
err := o.fs.writeFileWithOptionalSync(ifn, buf, defaultFilePerms)
o.mu.Lock()
if err != nil {
o.dirty = true
}
o.writing = false
o.mu.Unlock()
return err
}
// Will upodate the config. Only used when recovering ephemerals.
func (o *consumerFileStore) updateConfig(cfg ConsumerConfig) error {
o.mu.Lock()
defer o.mu.Unlock()
o.cfg = &FileConsumerInfo{ConsumerConfig: cfg}
return o.writeConsumerMeta()
}
// Write out the consumer meta data, i.e. state.
// Lock should be held.
func (cfs *consumerFileStore) writeConsumerMeta() error {
meta := filepath.Join(cfs.odir, JetStreamMetaFile)
if _, err := os.Stat(meta); err != nil && !os.IsNotExist(err) {
return err
}
if cfs.prf != nil && cfs.aek == nil {
fs := cfs.fs
key, _, _, encrypted, err := fs.genEncryptionKeys(fs.cfg.Name + tsep + cfs.name)
if err != nil {
return err
}
cfs.aek = key
keyFile := filepath.Join(cfs.odir, JetStreamMetaFileKey)
if _, err := os.Stat(keyFile); err != nil && !os.IsNotExist(err) {
return err
}
err = cfs.fs.writeFileWithOptionalSync(keyFile, encrypted, defaultFilePerms)
if err != nil {
return err
}
}
b, err := json.Marshal(cfs.cfg)
if err != nil {
return err
}
// Encrypt if needed.
if cfs.aek != nil {
nonce := make([]byte, cfs.aek.NonceSize(), cfs.aek.NonceSize()+len(b)+cfs.aek.Overhead())
if n, err := rand.Read(nonce); err != nil {
return err
} else if n != len(nonce) {
return fmt.Errorf("not enough nonce bytes read (%d != %d)", n, len(nonce))
}
b = cfs.aek.Seal(nonce, nonce, b, nil)
}
err = cfs.fs.writeFileWithOptionalSync(meta, b, defaultFilePerms)
if err != nil {
return err
}
cfs.hh.Reset()
cfs.hh.Write(b)
var hb [highwayhash.Size64]byte
checksum := hex.EncodeToString(cfs.hh.Sum(hb[:0]))
sum := filepath.Join(cfs.odir, JetStreamMetaFileSum)
err = cfs.fs.writeFileWithOptionalSync(sum, []byte(checksum), defaultFilePerms)
if err != nil {
return err
}
return nil
}
// Consumer version.
func checkConsumerHeader(hdr []byte) (uint8, error) {
if len(hdr) < 2 || hdr[0] != magic {
return 0, errCorruptState
}
version := hdr[1]
switch version {
case 1, 2:
return version, nil
}
return 0, fmt.Errorf("unsupported version: %d", version)
}
func (o *consumerFileStore) copyPending() map[uint64]*Pending {
pending := make(map[uint64]*Pending, len(o.state.Pending))
for seq, p := range o.state.Pending {
pending[seq] = &Pending{p.Sequence, p.Timestamp}
}
return pending
}
func (o *consumerFileStore) copyRedelivered() map[uint64]uint64 {
redelivered := make(map[uint64]uint64, len(o.state.Redelivered))
for seq, dc := range o.state.Redelivered {
redelivered[seq] = dc
}
return redelivered
}
// Type returns the type of the underlying store.
func (o *consumerFileStore) Type() StorageType { return FileStorage }
// State retrieves the state from the state file.
// This is not expected to be called in high performance code, only on startup.
func (o *consumerFileStore) State() (*ConsumerState, error) {
return o.stateWithCopy(true)
}
// This will not copy pending or redelivered, so should only be done under the
// consumer owner's lock.
func (o *consumerFileStore) BorrowState() (*ConsumerState, error) {
return o.stateWithCopy(false)
}
func (o *consumerFileStore) stateWithCopy(doCopy bool) (*ConsumerState, error) {
o.mu.Lock()
defer o.mu.Unlock()
return o.stateWithCopyLocked(doCopy)
}
// Lock should be held.
func (o *consumerFileStore) stateWithCopyLocked(doCopy bool) (*ConsumerState, error) {
if o.closed {
return nil, ErrStoreClosed
}
state := &ConsumerState{}
// See if we have a running state or if we need to read in from disk.
if o.state.Delivered.Consumer != 0 || o.state.Delivered.Stream != 0 {
state.Delivered = o.state.Delivered
state.AckFloor = o.state.AckFloor
if len(o.state.Pending) > 0 {
if doCopy {
state.Pending = o.copyPending()
} else {
state.Pending = o.state.Pending
}
}
if len(o.state.Redelivered) > 0 {
if doCopy {
state.Redelivered = o.copyRedelivered()
} else {
state.Redelivered = o.state.Redelivered
}
}
return state, nil
}
// Read the state in here from disk..
<-dios
buf, err := os.ReadFile(o.ifn)
dios <- struct{}{}
if err != nil && !os.IsNotExist(err) {
return nil, err
}
if len(buf) == 0 {
return state, nil
}
// Check on encryption.
if o.aek != nil {
ns := o.aek.NonceSize()
buf, err = o.aek.Open(nil, buf[:ns], buf[ns:], nil)
if err != nil {
return nil, err
}
}
state, err = decodeConsumerState(buf)
if err != nil {
return nil, err
}
// Copy this state into our own.
o.state.Delivered = state.Delivered
o.state.AckFloor = state.AckFloor
if len(state.Pending) > 0 {
if doCopy {
o.state.Pending = make(map[uint64]*Pending, len(state.Pending))
for seq, p := range state.Pending {
o.state.Pending[seq] = &Pending{p.Sequence, p.Timestamp}
}
} else {
o.state.Pending = state.Pending
}
}
if len(state.Redelivered) > 0 {
if doCopy {
o.state.Redelivered = make(map[uint64]uint64, len(state.Redelivered))
for seq, dc := range state.Redelivered {
o.state.Redelivered[seq] = dc
}
} else {
o.state.Redelivered = state.Redelivered
}
}
return state, nil
}
// Lock should be held. Called at startup.
func (o *consumerFileStore) loadState() error {
if _, err := os.Stat(o.ifn); err == nil {
// This will load our state in from disk.
_, err = o.stateWithCopyLocked(false)
return err
} else if os.IsNotExist(err) {
return nil
} else {
return err
}
}
// Decode consumer state.
func decodeConsumerState(buf []byte) (*ConsumerState, error) {
version, err := checkConsumerHeader(buf)
if err != nil {
return nil, err
}
bi := hdrLen
// Helpers, will set i to -1 on error.
readSeq := func() uint64 {
if bi < 0 {
return 0
}
seq, n := binary.Uvarint(buf[bi:])
if n <= 0 {
bi = -1
return 0
}
bi += n
return seq
}
readTimeStamp := func() int64 {
if bi < 0 {
return 0
}
ts, n := binary.Varint(buf[bi:])
if n <= 0 {
bi = -1
return -1
}
bi += n
return ts
}
// Just for clarity below.
readLen := readSeq
readCount := readSeq
state := &ConsumerState{}
state.AckFloor.Consumer = readSeq()
state.AckFloor.Stream = readSeq()
state.Delivered.Consumer = readSeq()
state.Delivered.Stream = readSeq()
if bi == -1 {
return nil, errCorruptState
}
if version == 1 {
// Adjust back. Version 1 also stored delivered as next to be delivered,
// so adjust that back down here.
if state.AckFloor.Consumer > 1 {
state.Delivered.Consumer += state.AckFloor.Consumer - 1
}
if state.AckFloor.Stream > 1 {
state.Delivered.Stream += state.AckFloor.Stream - 1
}
}
// Protect ourselves against rolling backwards.
const hbit = 1 << 63
if state.AckFloor.Stream&hbit != 0 || state.Delivered.Stream&hbit != 0 {
return nil, errCorruptState
}
// We have additional stuff.
if numPending := readLen(); numPending > 0 {
mints := readTimeStamp()
state.Pending = make(map[uint64]*Pending, numPending)
for i := 0; i < int(numPending); i++ {
sseq := readSeq()
var dseq uint64
if version == 2 {
dseq = readSeq()
}
ts := readTimeStamp()
// Check the state machine for corruption, not the value which could be -1.
if bi == -1 {
return nil, errCorruptState
}
// Adjust seq back.
sseq += state.AckFloor.Stream
if sseq == 0 {
return nil, errCorruptState
}
if version == 2 {
dseq += state.AckFloor.Consumer
}
// Adjust the timestamp back.
if version == 1 {
ts = (ts + mints) * int64(time.Second)
} else {
ts = (mints - ts) * int64(time.Second)
}
// Store in pending.
state.Pending[sseq] = &Pending{dseq, ts}
}
}
// We have redelivered entries here.
if numRedelivered := readLen(); numRedelivered > 0 {
state.Redelivered = make(map[uint64]uint64, numRedelivered)
for i := 0; i < int(numRedelivered); i++ {
if seq, n := readSeq(), readCount(); seq > 0 && n > 0 {
// Adjust seq back.
seq += state.AckFloor.Stream
state.Redelivered[seq] = n
}
}
}
return state, nil
}
// Stop the processing of the consumers's state.
func (o *consumerFileStore) Stop() error {
o.mu.Lock()
if o.closed {
o.mu.Unlock()
return nil
}
if o.qch != nil {
close(o.qch)
o.qch = nil
}
var err error
var buf []byte
if o.dirty {
// Make sure to write this out..
if buf, err = o.encodeState(); err == nil && len(buf) > 0 {
if o.aek != nil {
if buf, err = o.encryptState(buf); err != nil {
return err
}
}
}
}
o.odir = _EMPTY_
o.closed = true
ifn, fs := o.ifn, o.fs
o.mu.Unlock()
fs.RemoveConsumer(o)
if len(buf) > 0 {
o.waitOnFlusher()
err = o.fs.writeFileWithOptionalSync(ifn, buf, defaultFilePerms)
}
return err
}
func (o *consumerFileStore) waitOnFlusher() {
if !o.inFlusher() {
return
}
timeout := time.Now().Add(100 * time.Millisecond)
for time.Now().Before(timeout) {
if !o.inFlusher() {
return
}
time.Sleep(10 * time.Millisecond)
}
}
// Delete the consumer.
func (o *consumerFileStore) Delete() error {
return o.delete(false)
}
func (o *consumerFileStore) StreamDelete() error {
return o.delete(true)
}
func (o *consumerFileStore) delete(streamDeleted bool) error {
o.mu.Lock()
if o.closed {
o.mu.Unlock()
return nil
}
if o.qch != nil {
close(o.qch)
o.qch = nil
}
var err error
odir := o.odir
o.odir = _EMPTY_
o.closed = true
fs := o.fs
o.mu.Unlock()
// If our stream was not deleted this will remove the directories.
if odir != _EMPTY_ && !streamDeleted {
<-dios
err = os.RemoveAll(odir)
dios <- struct{}{}
}
if !streamDeleted {
fs.RemoveConsumer(o)
}
return err
}
func (fs *fileStore) AddConsumer(o ConsumerStore) error {
fs.cmu.Lock()
defer fs.cmu.Unlock()
fs.cfs = append(fs.cfs, o)
return nil
}
func (fs *fileStore) RemoveConsumer(o ConsumerStore) error {
fs.cmu.Lock()
defer fs.cmu.Unlock()
for i, cfs := range fs.cfs {
if o == cfs {
fs.cfs = append(fs.cfs[:i], fs.cfs[i+1:]...)
break
}
}
return nil
}
////////////////////////////////////////////////////////////////////////////////
// Templates
////////////////////////////////////////////////////////////////////////////////
type templateFileStore struct {
dir string
hh *highwayhash.Digest64
}
func newTemplateFileStore(storeDir string) *templateFileStore {
tdir := filepath.Join(storeDir, tmplsDir)
key := sha256.Sum256([]byte("templates"))
hh, err := highwayhash.NewDigest64(key[:])
if err != nil {
return nil
}
return &templateFileStore{dir: tdir, hh: hh}
}
func (ts *templateFileStore) Store(t *streamTemplate) error {
dir := filepath.Join(ts.dir, t.Name)
if err := os.MkdirAll(dir, defaultDirPerms); err != nil {
return fmt.Errorf("could not create templates storage directory for %q- %v", t.Name, err)
}
meta := filepath.Join(dir, JetStreamMetaFile)
if _, err := os.Stat(meta); (err != nil && !os.IsNotExist(err)) || err == nil {
return err
}
t.mu.Lock()
b, err := json.Marshal(t)
t.mu.Unlock()
if err != nil {
return err
}
if err := os.WriteFile(meta, b, defaultFilePerms); err != nil {
return err
}
// FIXME(dlc) - Do checksum
ts.hh.Reset()
ts.hh.Write(b)
var hb [highwayhash.Size64]byte
checksum := hex.EncodeToString(ts.hh.Sum(hb[:0]))
sum := filepath.Join(dir, JetStreamMetaFileSum)
if err := os.WriteFile(sum, []byte(checksum), defaultFilePerms); err != nil {
return err
}
return nil
}
func (ts *templateFileStore) Delete(t *streamTemplate) error {
return os.RemoveAll(filepath.Join(ts.dir, t.Name))
}
////////////////////////////////////////////////////////////////////////////////
// Compression
////////////////////////////////////////////////////////////////////////////////
type CompressionInfo struct {
Algorithm StoreCompression
OriginalSize uint64
}
func (c *CompressionInfo) MarshalMetadata() []byte {
b := make([]byte, 14) // 4 + potentially up to 10 for uint64
b[0], b[1], b[2] = 'c', 'm', 'p'
b[3] = byte(c.Algorithm)
n := binary.PutUvarint(b[4:], c.OriginalSize)
return b[:4+n]
}
func (c *CompressionInfo) UnmarshalMetadata(b []byte) (int, error) {
c.Algorithm = NoCompression
c.OriginalSize = 0
if len(b) < 5 { // 4 + min 1 for uvarint uint64
return 0, nil
}
if b[0] != 'c' || b[1] != 'm' || b[2] != 'p' {
return 0, nil
}
var n int
c.Algorithm = StoreCompression(b[3])
c.OriginalSize, n = binary.Uvarint(b[4:])
if n <= 0 {
return 0, fmt.Errorf("metadata incomplete")
}
return 4 + n, nil
}
func (alg StoreCompression) Compress(buf []byte) ([]byte, error) {
if len(buf) < checksumSize {
return nil, fmt.Errorf("uncompressed buffer is too short")
}
bodyLen := int64(len(buf) - checksumSize)
var output bytes.Buffer
var writer io.WriteCloser
switch alg {
case NoCompression:
return buf, nil
case S2Compression:
writer = s2.NewWriter(&output)
default:
return nil, fmt.Errorf("compression algorithm not known")
}
input := bytes.NewReader(buf[:bodyLen])
checksum := buf[bodyLen:]
// Compress the block content, but don't compress the checksum.
// We will preserve it at the end of the block as-is.
if n, err := io.CopyN(writer, input, bodyLen); err != nil {
return nil, fmt.Errorf("error writing to compression writer: %w", err)
} else if n != bodyLen {
return nil, fmt.Errorf("short write on body (%d != %d)", n, bodyLen)
}
if err := writer.Close(); err != nil {
return nil, fmt.Errorf("error closing compression writer: %w", err)
}
// Now add the checksum back onto the end of the block.
if n, err := output.Write(checksum); err != nil {
return nil, fmt.Errorf("error writing checksum: %w", err)
} else if n != checksumSize {
return nil, fmt.Errorf("short write on checksum (%d != %d)", n, checksumSize)
}
return output.Bytes(), nil
}
func (alg StoreCompression) Decompress(buf []byte) ([]byte, error) {
if len(buf) < checksumSize {
return nil, fmt.Errorf("compressed buffer is too short")
}
bodyLen := int64(len(buf) - checksumSize)
input := bytes.NewReader(buf[:bodyLen])
var reader io.ReadCloser
switch alg {
case NoCompression:
return buf, nil
case S2Compression:
reader = io.NopCloser(s2.NewReader(input))
default:
return nil, fmt.Errorf("compression algorithm not known")
}
// Decompress the block content. The checksum isn't compressed so
// we can preserve it from the end of the block as-is.
checksum := buf[bodyLen:]
output, err := io.ReadAll(reader)
if err != nil {
return nil, fmt.Errorf("error reading compression reader: %w", err)
}
output = append(output, checksum...)
return output, reader.Close()
}
// writeFileWithOptionalSync is equivalent to os.WriteFile() but optionally
// sets O_SYNC on the open file if SyncAlways is set. The dios semaphore is
// handled automatically by this function, so don't wrap calls to it in dios.
func (fs *fileStore) writeFileWithOptionalSync(name string, data []byte, perm fs.FileMode) error {
return writeAtomically(name, data, perm, fs.fcfg.SyncAlways)
}
func writeFileWithSync(name string, data []byte, perm fs.FileMode) error {
return writeAtomically(name, data, perm, true)
}
func writeAtomically(name string, data []byte, perm fs.FileMode, sync bool) error {
tmp := name + ".tmp"
flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
if sync {
flags = flags | os.O_SYNC
}
<-dios
defer func() {
dios <- struct{}{}
}()
f, err := os.OpenFile(tmp, flags, perm)
if err != nil {
return err
}
if _, err := f.Write(data); err != nil {
_ = f.Close()
_ = os.Remove(tmp)
return err
}
if err := f.Close(); err != nil {
_ = os.Remove(tmp)
return err
}
if err := os.Rename(tmp, name); err != nil {
_ = os.Remove(tmp)
return err
}
if sync {
// To ensure that the file rename was persisted on all filesystems,
// also try to flush the directory metadata.
if d, err := os.Open(filepath.Dir(name)); err == nil {
_ = d.Sync()
_ = d.Close()
}
}
return nil
}