1090 lines
38 KiB
Go
1090 lines
38 KiB
Go
// Copyright 2025 The NATS Authors
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
package server
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"math"
|
|
"math/big"
|
|
"path/filepath"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
)
|
|
|
|
var (
|
|
// Tracks the total inflight batches, across all streams and accounts that enable batching.
|
|
globalInflightAtomicBatches atomic.Int64
|
|
globalInflightFastBatches atomic.Int64
|
|
)
|
|
|
|
type batching struct {
|
|
mu sync.Mutex
|
|
atomic map[string]*atomicBatch
|
|
fast map[string]*fastBatch
|
|
}
|
|
|
|
type atomicBatch struct {
|
|
timer *time.Timer // Inactivity timer for the batch.
|
|
lseq uint64 // The highest sequence for this batch.
|
|
store StreamStore // Where the batch is staged before committing.
|
|
}
|
|
|
|
type fastBatch struct {
|
|
timer *time.Timer // Inactivity timer for the batch.
|
|
lseq uint64 // The highest sequence for this batch.
|
|
sseq uint64 // Last persisted stream sequence.
|
|
pseq uint64 // Last persisted batch sequence (is always lower or equal to lseq).
|
|
fseq uint64 // Sequence of when we last sent a flow message (is always lower or equal to pseq).
|
|
pending uint32 // Number of pending messages in the batch waiting to be persisted.
|
|
ackMessages uint16 // Ack will be sent every N messages.
|
|
maxAckMessages uint16 // Maximum ackMessages value the client allows.
|
|
reply string // The last reply subject seen when persisting a message.
|
|
gapOk bool // Whether a gap is okay, if not, the batch would be rejected.
|
|
commit bool // If the batch is committed.
|
|
}
|
|
|
|
// newAtomicBatch creates an atomic batch publish object.
|
|
// Lock should be held.
|
|
func (batches *batching) newAtomicBatch(mset *stream, batchId string, replicas int, storage StorageType, storeDir, streamName string) (*atomicBatch, error) {
|
|
store, err := newBatchStore(mset, batchId, replicas, storage, storeDir, streamName)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b := &atomicBatch{store: store}
|
|
b.setupCleanupTimer(mset, batchId, batches)
|
|
return b, nil
|
|
}
|
|
|
|
// setupCleanupTimer sets up a timer to clean up the batch after a timeout.
|
|
func (b *atomicBatch) setupCleanupTimer(mset *stream, batchId string, batches *batching) {
|
|
// Create a timer to clean up after timeout.
|
|
timeout := getCleanupTimeout(mset)
|
|
b.timer = time.AfterFunc(timeout, func() {
|
|
b.cleanup(batchId, batches)
|
|
mset.sendStreamBatchAbandonedAdvisory(batchId, BatchTimeout)
|
|
})
|
|
}
|
|
|
|
// resetCleanupTimer resets the cleanup timer, allowing to extend the lifetime of the batch.
|
|
// Returns whether the timer was reset without it having expired before.
|
|
func (b *atomicBatch) resetCleanupTimer(mset *stream) bool {
|
|
timeout := getCleanupTimeout(mset)
|
|
return b.timer.Reset(timeout)
|
|
}
|
|
|
|
// cleanup deletes underlying resources associated with the batch and unregisters it from the stream's batches.
|
|
func (b *atomicBatch) cleanup(batchId string, batches *batching) {
|
|
batches.mu.Lock()
|
|
defer batches.mu.Unlock()
|
|
b.cleanupLocked(batchId, batches)
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (b *atomicBatch) cleanupLocked(batchId string, batches *batching) {
|
|
if b.timer == nil {
|
|
return
|
|
}
|
|
globalInflightAtomicBatches.Add(-1)
|
|
b.timer.Stop()
|
|
b.store.Delete(true)
|
|
delete(batches.atomic, batchId)
|
|
// Reset so that another invocation doesn't double-account.
|
|
b.timer = nil
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (b *atomicBatch) stopLocked() {
|
|
if b.timer == nil {
|
|
return
|
|
}
|
|
globalInflightAtomicBatches.Add(-1)
|
|
b.timer.Stop()
|
|
b.store.Stop()
|
|
// Reset so that another invocation doesn't double-account.
|
|
b.timer = nil
|
|
}
|
|
|
|
func getBatchStoreDir(storeDir, streamName, batchId string) (string, string) {
|
|
bname := getHash(batchId)
|
|
return bname, filepath.Join(storeDir, streamsDir, streamName, batchesDir, bname)
|
|
}
|
|
|
|
func newBatchStore(mset *stream, batchId string, replicas int, storage StorageType, storeDir, streamName string) (StreamStore, error) {
|
|
if replicas == 1 && storage == FileStorage {
|
|
bname, storeDir := getBatchStoreDir(storeDir, streamName, batchId)
|
|
fcfg := FileStoreConfig{AsyncFlush: true, BlockSize: defaultLargeBlockSize, StoreDir: storeDir}
|
|
s := mset.srv
|
|
prf := s.jsKeyGen(s.getOpts().JetStreamKey, mset.acc.Name)
|
|
if prf != nil {
|
|
// We are encrypted here, fill in correct cipher selection.
|
|
fcfg.Cipher = s.getOpts().JetStreamCipher
|
|
}
|
|
oldprf := s.jsKeyGen(s.getOpts().JetStreamOldKey, mset.acc.Name)
|
|
cfg := StreamConfig{Name: bname, Storage: FileStorage}
|
|
return newFileStoreWithCreated(fcfg, cfg, time.Time{}, prf, oldprf)
|
|
}
|
|
return newMemStore(&StreamConfig{Name: _EMPTY_, Storage: MemoryStorage})
|
|
}
|
|
|
|
// readyForCommit indicates the batch is ready to be committed.
|
|
// If the timer has already cleaned up the batch, we can't commit.
|
|
// Otherwise, we ensure the timer does not clean up the batch in the meantime.
|
|
// Lock should be held.
|
|
func (b *atomicBatch) readyForCommit() *BatchAbandonReason {
|
|
if !b.timer.Stop() {
|
|
return &BatchTimeout
|
|
}
|
|
if b.store.FlushAllPending() != nil {
|
|
return &BatchIncomplete
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// newFastBatch creates a fast batch publish object and registers it in batches.fast.
|
|
// Lock should be held.
|
|
func (batches *batching) newFastBatch(mset *stream, batchId string, gapOk bool, maxAckMessages uint16) *fastBatch {
|
|
b := &fastBatch{gapOk: gapOk, maxAckMessages: maxAckMessages}
|
|
if batches.fast == nil {
|
|
batches.fast = make(map[string]*fastBatch, 1)
|
|
}
|
|
batches.fast[batchId] = b
|
|
batches.fastBatchInit(b)
|
|
b.setupCleanupTimer(mset, batchId, batches)
|
|
return b
|
|
}
|
|
|
|
// fastBatchInit (re)initializes the ackMessages field for a fast batch.
|
|
// The batch must already be registered in batches.fast.
|
|
// Lock should be held.
|
|
func (batches *batching) fastBatchInit(b *fastBatch) {
|
|
// If it's the only batch, just allow what the client wants, otherwise we'll
|
|
// need to coordinate and slowly ramp up this publisher.
|
|
// TODO(mvv): fast ingest's initial flow value improvements?
|
|
ackMessages := min(500, b.maxAckMessages)
|
|
if len(batches.fast) > 1 {
|
|
ackMessages = 1
|
|
}
|
|
b.ackMessages = ackMessages
|
|
}
|
|
|
|
// fastBatchReset resets the fast batch to an empty state and sends a flow control message.
|
|
// Lock should be held.
|
|
func (batches *batching) fastBatchReset(mset *stream, batchId string, b *fastBatch) {
|
|
// If the timer already stopped before we could commit, we clean it up.
|
|
if b.timer == nil || (!b.commit && !b.timer.Stop()) {
|
|
b.cleanupLocked(batchId, batches)
|
|
return
|
|
}
|
|
// Otherwise, reset the state.
|
|
batches.fastBatchInit(b)
|
|
b.timer.Reset(getCleanupTimeout(mset))
|
|
b.commit = false
|
|
b.pending = 0
|
|
b.fseq, b.lseq = b.pseq, b.pseq
|
|
b.sendFlowControl(b.fseq, mset, b.reply)
|
|
}
|
|
|
|
// fastBatchRegisterSequences registers the highest stored batch and stream sequence and returns
|
|
// whether a PubAck should be sent if the batch has been committed.
|
|
// If this is called on a follower, it only registers the highest stream and persisted batch sequences.
|
|
// Lock should be held.
|
|
func (batches *batching) fastBatchRegisterSequences(mset *stream, reply string, streamSeq uint64, isLeader bool, batch *FastBatch) bool {
|
|
b, ok := batches.fast[batch.id]
|
|
if !ok || !isLeader {
|
|
// If this batch has committed, we can clean it up.
|
|
if batch.commit {
|
|
if b != nil {
|
|
b.cleanupLocked(batch.id, batches)
|
|
}
|
|
return false
|
|
}
|
|
// Otherwise, even as a follower, we record the latest state of this batch.
|
|
if b == nil || !b.resetCleanupTimer(mset) {
|
|
if b != nil {
|
|
// The timer couldn't be reset, this means the timer already runs and is likely
|
|
// waiting to acquire the lock. We reset the timer here so it doesn't clean up
|
|
// this batch that we're about to overwrite.
|
|
b.timer = nil
|
|
} else {
|
|
// If this is a new batch for us, even though we're a follower, we still need
|
|
// to account toward the global inflight limit.
|
|
globalInflightFastBatches.Add(1)
|
|
}
|
|
// We'll need a copy as we'll use it as a key and later for cleanup.
|
|
batchId := copyString(batch.id)
|
|
b = batches.newFastBatch(mset, batchId, batch.gapOk, batch.flow)
|
|
}
|
|
b.sseq = streamSeq
|
|
b.pseq, b.lseq = batch.seq, batch.seq
|
|
b.reply = reply
|
|
return false
|
|
}
|
|
b.reply = reply
|
|
if b.pending > 0 {
|
|
b.pending--
|
|
}
|
|
b.sseq = streamSeq
|
|
// Store last persisted batch sequence.
|
|
// If we have no remaining pending writes, we might have had duplicate messages
|
|
// and need to send additional flow control messages.
|
|
var skipped bool
|
|
if b.pending == 0 {
|
|
skipped = true
|
|
b.pseq = b.lseq
|
|
} else {
|
|
b.pseq = batch.seq
|
|
}
|
|
// If the PubAck needs to be sent now as a result of a commit.
|
|
if b.lseq == b.pseq && b.commit {
|
|
b.cleanupLocked(batch.id, batches)
|
|
// If we skipped ahead due to duplicate messages, send the PubAck with the highest sequence.
|
|
if skipped {
|
|
var buf [256]byte
|
|
pubAck := append(buf[:0], mset.pubAck...)
|
|
response := append(pubAck, strconv.FormatUint(b.sseq, 10)...)
|
|
response = append(response, fmt.Sprintf(",\"batch\":%q,\"count\":%d}", batch.id, b.lseq)...)
|
|
if len(reply) > 0 {
|
|
mset.outq.sendMsg(reply, response)
|
|
}
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
b.checkFlowControl(mset, reply, batches)
|
|
return false
|
|
}
|
|
|
|
// checkFlowControl checks whether a flow control message should be sent.
|
|
// If so, it updates the flow values to speed up or slow down the publisher if needed.
|
|
// Returns whether a flow control message was sent.
|
|
// Lock should be held.
|
|
func (b *fastBatch) checkFlowControl(mset *stream, reply string, batches *batching) bool {
|
|
am := uint64(b.ackMessages)
|
|
if b.pseq < b.fseq+am {
|
|
return false
|
|
}
|
|
// Instead of sending multiple flow control messages, skip ahead to only send the last.
|
|
steps := (b.pseq - b.fseq) / am
|
|
b.fseq += steps * am
|
|
|
|
// TODO(mvv): fast ingest's dynamic flow value improvements?
|
|
// This is currently just a simple value to have a working version. Should take average
|
|
// message sizes into account and compare how much this client is contributing to the
|
|
// ingest IPQ total size and messages and have publishers share based on that.
|
|
maxAckMessages := uint16(500 / len(batches.fast))
|
|
if maxAckMessages < 1 {
|
|
maxAckMessages = 1
|
|
}
|
|
// Limit to the client's allowed maximum.
|
|
if maxAckMessages > b.maxAckMessages {
|
|
maxAckMessages = b.maxAckMessages
|
|
}
|
|
|
|
if b.ackMessages < maxAckMessages {
|
|
// Ramp up.
|
|
b.ackMessages *= 2
|
|
if b.ackMessages > maxAckMessages {
|
|
b.ackMessages = maxAckMessages
|
|
}
|
|
} else if b.ackMessages > maxAckMessages {
|
|
// Slow down.
|
|
b.ackMessages /= 2
|
|
if b.ackMessages <= maxAckMessages {
|
|
b.ackMessages = maxAckMessages
|
|
}
|
|
}
|
|
|
|
// Finally, send the flow control message.
|
|
b.sendFlowControl(b.fseq, mset, reply)
|
|
return true
|
|
}
|
|
|
|
// sendFlowControl sends a fast batch flow control message for the current highest sequence.
|
|
// Lock should be held.
|
|
func (b *fastBatch) sendFlowControl(batchSeq uint64, mset *stream, reply string) {
|
|
if len(reply) == 0 {
|
|
return
|
|
}
|
|
response, _ := BatchFlowAck{Sequence: batchSeq, Messages: b.ackMessages}.MarshalJSON()
|
|
mset.outq.sendMsg(reply, response)
|
|
}
|
|
|
|
// fastBatchCommit ends the batch and commits the data up to that point. If all messages
|
|
// have already been persisted, a PubAck is sent immediately. Otherwise, it will be sent
|
|
// after the last message has been persisted.
|
|
// Lock should be held.
|
|
func (batches *batching) fastBatchCommit(b *fastBatch, batchId string, mset *stream, reply string) bool {
|
|
// Either we commit now, or we clean up later, so stop the timer.
|
|
if b.timer == nil || (!b.commit && !b.timer.Stop()) {
|
|
// Shouldn't be possible for the timer to already be stopped if we haven't committed yet,
|
|
// since we pre-check being able to reset the timer. But guard against it anyhow.
|
|
return true
|
|
}
|
|
// Mark that this batch commits.
|
|
b.commit = true
|
|
// If the whole batch has been persisted, we can respond with the PubAck now.
|
|
if b.lseq == b.pseq {
|
|
b.cleanupLocked(batchId, batches)
|
|
var buf [256]byte
|
|
pubAck := append(buf[:0], mset.pubAck...)
|
|
response := append(pubAck, strconv.FormatUint(b.sseq, 10)...)
|
|
response = append(response, fmt.Sprintf(",\"batch\":%q,\"count\":%d}", batchId, b.lseq)...)
|
|
if len(reply) > 0 {
|
|
mset.outq.sendMsg(reply, response)
|
|
}
|
|
return true
|
|
}
|
|
// Otherwise, we need to wait and the PubAck will be sent when the last message is persisted.
|
|
return false
|
|
}
|
|
|
|
// setupCleanupTimer sets up a timer to clean up the batch after a timeout.
|
|
func (b *fastBatch) setupCleanupTimer(mset *stream, batchId string, batches *batching) {
|
|
// Create a timer to clean up after timeout.
|
|
timeout := getCleanupTimeout(mset)
|
|
b.timer = time.AfterFunc(timeout, func() {
|
|
b.cleanup(batchId, batches)
|
|
})
|
|
}
|
|
|
|
// resetCleanupTimer resets the cleanup timer, allowing to extend the lifetime of the batch.
|
|
// Returns whether the timer was reset without it having expired before.
|
|
func (b *fastBatch) resetCleanupTimer(mset *stream) bool {
|
|
if b.commit {
|
|
return true
|
|
}
|
|
if b.timer == nil {
|
|
return false
|
|
}
|
|
timeout := getCleanupTimeout(mset)
|
|
return b.timer.Reset(timeout)
|
|
}
|
|
|
|
// cleanup deletes underlying resources associated with the batch and unregisters it from the stream's batches.
|
|
func (b *fastBatch) cleanup(batchId string, batches *batching) {
|
|
batches.mu.Lock()
|
|
defer batches.mu.Unlock()
|
|
b.cleanupLocked(batchId, batches)
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (b *fastBatch) cleanupLocked(batchId string, batches *batching) {
|
|
// If the timer is nil, it means this batch has been replaced with a new one.
|
|
// This can happen on a follower depending on timing.
|
|
if b.timer == nil {
|
|
return
|
|
}
|
|
globalInflightFastBatches.Add(-1)
|
|
b.timer.Stop()
|
|
delete(batches.fast, batchId)
|
|
// Reset so that another invocation doesn't double-account.
|
|
b.timer = nil
|
|
}
|
|
|
|
// getCleanupTimeout returns the timeout for the batch, taking into account the server's limits.
|
|
func getCleanupTimeout(mset *stream) time.Duration {
|
|
timeout := streamMaxBatchTimeout
|
|
if maxBatchTimeout := mset.srv.getOpts().JetStreamLimits.MaxBatchTimeout; maxBatchTimeout > 0 {
|
|
timeout = maxBatchTimeout
|
|
}
|
|
return timeout
|
|
}
|
|
|
|
// batchStagedDiff stages all changes for consistency checks until commit.
|
|
type batchStagedDiff struct {
|
|
msgIds map[string]struct{}
|
|
counter map[string]*msgCounterRunningTotal
|
|
inflight map[string]*inflightSubjectRunningTotal
|
|
inflightTransform map[uint64]string
|
|
expectedPerSubject map[string]*batchExpectedPerSubject
|
|
}
|
|
|
|
type batchExpectedPerSubject struct {
|
|
sseq uint64 // Stream sequence.
|
|
clseq uint64 // Clustered proposal sequence.
|
|
}
|
|
|
|
func (diff *batchStagedDiff) commit(mset *stream) {
|
|
if len(diff.msgIds) > 0 {
|
|
ts := time.Now().UnixNano()
|
|
mset.ddMu.Lock()
|
|
for msgId := range diff.msgIds {
|
|
// We stage with zero, and will update in processJetStreamMsg once we know the sequence.
|
|
mset.storeMsgIdLocked(&ddentry{msgId, 0, ts})
|
|
}
|
|
mset.ddMu.Unlock()
|
|
}
|
|
|
|
// Store running totals for counters, we could have multiple counter increments proposed, but not applied yet.
|
|
if len(diff.counter) > 0 {
|
|
if mset.clusteredCounterTotal == nil {
|
|
mset.clusteredCounterTotal = make(map[string]*msgCounterRunningTotal, len(diff.counter))
|
|
}
|
|
for k, c := range diff.counter {
|
|
mset.clusteredCounterTotal[k] = c
|
|
}
|
|
}
|
|
|
|
// Track inflight.
|
|
if len(diff.inflight) > 0 {
|
|
if mset.inflight == nil {
|
|
mset.inflight = make(map[string]*inflightSubjectRunningTotal, len(diff.inflight))
|
|
}
|
|
for subj, i := range diff.inflight {
|
|
if c, ok := mset.inflight[subj]; ok {
|
|
c.bytes += i.bytes
|
|
c.ops += i.ops
|
|
c.schedule = i.schedule
|
|
} else {
|
|
mset.inflight[subj] = i
|
|
}
|
|
}
|
|
}
|
|
|
|
// Track inflight subject transforms.
|
|
if len(diff.inflightTransform) > 0 {
|
|
if mset.inflightTransform == nil {
|
|
mset.inflightTransform = make(map[uint64]string, len(diff.inflightTransform))
|
|
}
|
|
for clseq, subj := range diff.inflightTransform {
|
|
mset.inflightTransform[clseq] = subj
|
|
}
|
|
}
|
|
|
|
// Track sequence and subject.
|
|
if len(diff.expectedPerSubject) > 0 {
|
|
if mset.expectedPerSubjectSequence == nil {
|
|
mset.expectedPerSubjectSequence = make(map[uint64]string, len(diff.expectedPerSubject))
|
|
}
|
|
if mset.expectedPerSubjectInProcess == nil {
|
|
mset.expectedPerSubjectInProcess = make(map[string]struct{}, len(diff.expectedPerSubject))
|
|
}
|
|
for subj, e := range diff.expectedPerSubject {
|
|
mset.expectedPerSubjectSequence[e.clseq] = subj
|
|
mset.expectedPerSubjectInProcess[subj] = struct{}{}
|
|
}
|
|
}
|
|
}
|
|
|
|
type batchApply struct {
|
|
mu sync.Mutex
|
|
id string // ID of the current batch.
|
|
count uint64 // Number of entries in the batch, for consistency checks.
|
|
entries []*CommittedEntry // Previous entries that are part of this batch.
|
|
entryStart int // The index into an entry indicating the first message of the batch.
|
|
maxApplied uint64 // Applied value before the entry containing the first message of the batch.
|
|
}
|
|
|
|
// clearBatchStateLocked clears in-memory apply-batch-related state.
|
|
// batch.mu lock should be held.
|
|
func (batch *batchApply) clearBatchStateLocked() {
|
|
batch.id = _EMPTY_
|
|
batch.count = 0
|
|
batch.entries = nil
|
|
batch.entryStart = 0
|
|
batch.maxApplied = 0
|
|
}
|
|
|
|
// rejectBatchStateLocked rejects the batch and clears in-memory apply-batch-related state.
|
|
// Corrects mset.clfs to take the failed batch into account.
|
|
// batch.mu lock should be held.
|
|
func (batch *batchApply) rejectBatchStateLocked(mset *stream) {
|
|
mset.clMu.Lock()
|
|
mset.clfs += batch.count
|
|
mset.clMu.Unlock()
|
|
// We're rejecting the batch, so all entries need to be returned to the pool.
|
|
for _, bce := range batch.entries {
|
|
bce.ReturnToPool()
|
|
}
|
|
batch.clearBatchStateLocked()
|
|
}
|
|
|
|
func (batch *batchApply) rejectBatchState(mset *stream) {
|
|
batch.mu.Lock()
|
|
defer batch.mu.Unlock()
|
|
batch.rejectBatchStateLocked(mset)
|
|
}
|
|
|
|
// checkMsgHeadersPreClusteredProposal checks the message for expected/consistency headers.
|
|
// mset.mu lock must NOT be held or used.
|
|
// mset.clMu lock must be held.
|
|
func checkMsgHeadersPreClusteredProposal(
|
|
diff *batchStagedDiff, mset *stream, subject, rsubject string, hdr []byte, msg []byte, sourced bool, name string,
|
|
jsa *jsAccount, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules bool,
|
|
discard DiscardPolicy, discardNewPer bool, maxMsgSize int, maxMsgs int64, maxMsgsPer int64, maxBytes int64,
|
|
) ([]byte, []byte, uint64, *ApiError, error) {
|
|
var incr *big.Int
|
|
var hasSchedule bool
|
|
|
|
// Some header checks must be checked pre proposal.
|
|
if len(hdr) > 0 {
|
|
// Since we encode header len as u16 make sure we do not exceed.
|
|
// Again this works if it goes through but better to be pre-emptive.
|
|
if len(hdr) > math.MaxUint16 {
|
|
err := fmt.Errorf("JetStream header size exceeds limits for '%s > %s'", jsa.acc().Name, name)
|
|
return hdr, msg, 0, NewJSStreamHeaderExceedsMaximumError(), err
|
|
}
|
|
// Counter increments.
|
|
// Only supported on counter streams, and payload must be empty (if not coming from a source).
|
|
var ok bool
|
|
if incr, ok = getMessageIncr(hdr); !ok {
|
|
apiErr := NewJSMessageIncrInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
} else if incr != nil && !sourced {
|
|
// Only do checks if the message isn't sourced. Otherwise, we need to store verbatim.
|
|
if !allowMsgCounter {
|
|
apiErr := NewJSMessageIncrDisabledError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
} else if len(msg) > 0 {
|
|
apiErr := NewJSMessageIncrPayloadError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
} else {
|
|
// Check for incompatible headers.
|
|
var doErr bool
|
|
if getRollup(hdr) != _EMPTY_ ||
|
|
getExpectedStream(hdr) != _EMPTY_ ||
|
|
getExpectedLastMsgId(hdr) != _EMPTY_ ||
|
|
getExpectedLastSeqPerSubjectForSubject(hdr) != _EMPTY_ {
|
|
doErr = true
|
|
} else if _, ok = getExpectedLastSeq(hdr); ok {
|
|
doErr = true
|
|
} else if _, ok = getExpectedLastSeqPerSubject(hdr); ok {
|
|
doErr = true
|
|
}
|
|
|
|
if doErr {
|
|
apiErr := NewJSMessageIncrInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
}
|
|
}
|
|
}
|
|
// Expected stream name can also be pre-checked.
|
|
if sname := getExpectedStream(hdr); sname != _EMPTY_ && sname != name {
|
|
return hdr, msg, 0, NewJSStreamNotMatchError(), errStreamMismatch
|
|
}
|
|
// TTL'd messages are rejected entirely if TTLs are not enabled on the stream, or if the TTL is invalid.
|
|
if ttl, err := getMessageTTL(hdr); !sourced && (ttl != 0 || err != nil) {
|
|
if !allowTTL {
|
|
return hdr, msg, 0, NewJSMessageTTLDisabledError(), errMsgTTLDisabled
|
|
} else if err != nil {
|
|
return hdr, msg, 0, NewJSMessageTTLInvalidError(), err
|
|
}
|
|
}
|
|
// Check for MsgIds here at the cluster level to avoid excessive CLFS accounting.
|
|
// Will help during restarts.
|
|
if msgId := getMsgId(hdr); msgId != _EMPTY_ {
|
|
// Dedupe if staged.
|
|
if _, ok = diff.msgIds[msgId]; ok {
|
|
return hdr, msg, 0, NewJSAtomicPublishContainsDuplicateMessageError(), errMsgIdDuplicate
|
|
}
|
|
mset.ddMu.Lock()
|
|
if dde := mset.checkMsgId(msgId); dde != nil {
|
|
seq := dde.seq
|
|
mset.ddMu.Unlock()
|
|
// Should not return an invalid sequence, in that case error.
|
|
if seq > 0 {
|
|
return hdr, msg, seq, NewJSAtomicPublishContainsDuplicateMessageError(), errMsgIdDuplicate
|
|
} else {
|
|
return hdr, msg, 0, NewJSStreamDuplicateMessageConflictError(), errMsgIdDuplicate
|
|
}
|
|
}
|
|
if diff.msgIds == nil {
|
|
diff.msgIds = map[string]struct{}{msgId: {}}
|
|
} else {
|
|
diff.msgIds[msgId] = struct{}{}
|
|
}
|
|
mset.ddMu.Unlock()
|
|
}
|
|
}
|
|
|
|
// Apply increment for counter.
|
|
// But only if it's allowed for this stream. This can happen when we store verbatim for a sourced stream.
|
|
if incr == nil && allowMsgCounter {
|
|
apiErr := NewJSMessageIncrMissingError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
}
|
|
if incr != nil && allowMsgCounter {
|
|
var initial big.Int
|
|
var sources CounterSources
|
|
|
|
// If we've got a running total, update that, since we have inflight proposals updating the same counter.
|
|
var ok bool
|
|
var counter *msgCounterRunningTotal
|
|
if counter, ok = diff.counter[subject]; ok {
|
|
initial = *counter.total
|
|
sources = counter.sources
|
|
} else if counter, ok = mset.clusteredCounterTotal[subject]; ok {
|
|
initial = *counter.total
|
|
sources = counter.sources
|
|
// Make an explicit copy to separate the staged data from what's committed.
|
|
// Don't need to initialize all values, they'll be overwritten later.
|
|
counter = &msgCounterRunningTotal{ops: counter.ops}
|
|
} else {
|
|
// Load last message, and store as inflight running total.
|
|
var smv StoreMsg
|
|
sm, err := mset.store.LoadLastMsg(subject, &smv)
|
|
if err == nil && sm != nil {
|
|
var val CounterValue
|
|
// Return an error if the counter is broken somehow.
|
|
if json.Unmarshal(sm.msg, &val) != nil {
|
|
apiErr := NewJSMessageCounterBrokenError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
}
|
|
if ncs := sliceHeader(JSMessageCounterSources, sm.hdr); len(ncs) > 0 {
|
|
if err := json.Unmarshal(ncs, &sources); err != nil {
|
|
apiErr := NewJSMessageCounterBrokenError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
}
|
|
}
|
|
initial.SetString(val.Value, 10)
|
|
}
|
|
}
|
|
srchdr := sliceHeader(JSStreamSource, hdr)
|
|
if len(srchdr) > 0 {
|
|
// This is a sourced message, so we can't apply Nats-Incr but
|
|
// instead should just update the source count header.
|
|
fields := strings.Split(string(srchdr), " ")
|
|
origStream := fields[0]
|
|
origSubj := subject
|
|
if len(fields) >= 5 {
|
|
origSubj = fields[4]
|
|
}
|
|
var val CounterValue
|
|
if json.Unmarshal(msg, &val) != nil {
|
|
apiErr := NewJSMessageCounterBrokenError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
}
|
|
var sourced big.Int
|
|
sourced.SetString(val.Value, 10)
|
|
if sources == nil {
|
|
sources = map[string]map[string]string{}
|
|
}
|
|
if _, ok = sources[origStream]; !ok {
|
|
sources[origStream] = map[string]string{}
|
|
}
|
|
prevVal := sources[origStream][origSubj]
|
|
sources[origStream][origSubj] = sourced.String()
|
|
// We will also replace the Nats-Incr header with the diff
|
|
// between our last value from this source and this one, so
|
|
// that the arithmetic is always correct.
|
|
var previous big.Int
|
|
previous.SetString(prevVal, 10)
|
|
incr.Sub(&sourced, &previous)
|
|
hdr = setHeader(JSMessageIncr, incr.String(), hdr)
|
|
}
|
|
// Now make the change.
|
|
initial.Add(&initial, incr)
|
|
// Generate the new payload.
|
|
var _msg [128]byte
|
|
msg = fmt.Appendf(_msg[:0], "{%q:%q}", "val", initial.String())
|
|
// Write the updated source count headers.
|
|
if len(sources) > 0 {
|
|
nhdr, err := json.Marshal(sources)
|
|
if err != nil {
|
|
return hdr, msg, 0, NewJSMessageCounterBrokenError(), err
|
|
}
|
|
hdr = setHeader(JSMessageCounterSources, string(nhdr), hdr)
|
|
}
|
|
|
|
// Check to see if we are over the max msg size.
|
|
maxSize := int64(mset.srv.getOpts().MaxPayload)
|
|
if maxMsgSize >= 0 && int64(maxMsgSize) < maxSize {
|
|
maxSize = int64(maxMsgSize)
|
|
}
|
|
hdrLen, msgLen := int64(len(hdr)), int64(len(msg))
|
|
// Subtract to prevent against overflows.
|
|
if hdrLen > maxSize || msgLen > maxSize-hdrLen {
|
|
return hdr, msg, 0, NewJSStreamMessageExceedsMaximumError(), ErrMaxPayload
|
|
}
|
|
|
|
// Keep the in-memory counters up-to-date.
|
|
if counter == nil {
|
|
counter = &msgCounterRunningTotal{}
|
|
}
|
|
counter.total = &initial
|
|
counter.sources = sources
|
|
counter.ops++
|
|
if diff.counter == nil {
|
|
diff.counter = map[string]*msgCounterRunningTotal{subject: counter}
|
|
} else {
|
|
diff.counter[subject] = counter
|
|
}
|
|
}
|
|
|
|
if len(hdr) > 0 {
|
|
// Expected last sequence.
|
|
if seq, exists := getExpectedLastSeq(hdr); exists && seq != mset.clseq-mset.clfs {
|
|
mlseq := mset.clseq - mset.clfs
|
|
err := fmt.Errorf("last sequence mismatch: %d vs %d", seq, mlseq)
|
|
return hdr, msg, 0, NewJSStreamWrongLastSequenceError(mlseq), err
|
|
} else if exists && len(diff.inflight) > 0 {
|
|
// Only the first message in a batch can contain an expected last sequence.
|
|
err := fmt.Errorf("last sequence mismatch")
|
|
return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err
|
|
}
|
|
|
|
// Expected last sequence per subject.
|
|
if seq, exists := getExpectedLastSeqPerSubject(hdr); exists {
|
|
// Allow override of the subject used for the check.
|
|
seqSubj := subject
|
|
if optSubj := getExpectedLastSeqPerSubjectForSubject(hdr); optSubj != _EMPTY_ {
|
|
seqSubj = optSubj
|
|
}
|
|
|
|
// The subject is already written to in this batch, we can't allow
|
|
// expected checks since they would be incorrect.
|
|
if _, ok := diff.inflight[seqSubj]; ok {
|
|
err := errors.New("last sequence by subject mismatch")
|
|
return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err
|
|
}
|
|
|
|
// If the subject is already in process, block as otherwise we could have
|
|
// multiple messages inflight with the same subject.
|
|
if _, found := mset.expectedPerSubjectInProcess[seqSubj]; found {
|
|
err := errors.New("last sequence by subject mismatch")
|
|
return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err
|
|
}
|
|
|
|
// If the subject is already in process but without expected headers, block as we would have
|
|
// multiple messages inflight with the same subject.
|
|
if _, ok := mset.inflight[seqSubj]; ok {
|
|
err := errors.New("last sequence by subject mismatch")
|
|
return hdr, msg, 0, NewJSStreamWrongLastSequenceConstantError(), err
|
|
}
|
|
|
|
// If we've already done an expected-check on this subject, use the cached result.
|
|
if e, ok := diff.expectedPerSubject[seqSubj]; ok {
|
|
if e.sseq != seq {
|
|
err := fmt.Errorf("last sequence by subject mismatch: %d vs %d", seq, e.sseq)
|
|
return hdr, msg, 0, NewJSStreamWrongLastSequenceError(e.sseq), err
|
|
}
|
|
e.clseq = mset.clseq
|
|
} else {
|
|
var smv StoreMsg
|
|
var fseq uint64
|
|
sm, err := mset.store.LoadLastMsg(seqSubj, &smv)
|
|
if sm != nil {
|
|
fseq = sm.seq
|
|
}
|
|
if err == ErrStoreMsgNotFound && seq == 0 {
|
|
fseq, err = 0, nil
|
|
}
|
|
if err != nil || fseq != seq {
|
|
err = fmt.Errorf("last sequence by subject mismatch: %d vs %d", seq, fseq)
|
|
return hdr, msg, 0, NewJSStreamWrongLastSequenceError(fseq), err
|
|
}
|
|
|
|
e = &batchExpectedPerSubject{sseq: fseq, clseq: mset.clseq}
|
|
if diff.expectedPerSubject == nil {
|
|
diff.expectedPerSubject = map[string]*batchExpectedPerSubject{seqSubj: e}
|
|
} else {
|
|
diff.expectedPerSubject[seqSubj] = e
|
|
}
|
|
}
|
|
} else if getExpectedLastSeqPerSubjectForSubject(hdr) != _EMPTY_ {
|
|
apiErr := NewJSStreamExpectedLastSeqPerSubjectInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
}
|
|
|
|
// Message scheduling.
|
|
if sourced {
|
|
// noop, sourced messages were already validated by the origin stream.
|
|
} else if schedule, apiErr := getMessageSchedule(hdr); apiErr != nil {
|
|
if !allowMsgSchedules {
|
|
apiErr = NewJSMessageSchedulesDisabledError()
|
|
}
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
} else if !schedule.IsZero() {
|
|
hasSchedule = true
|
|
if !allowMsgSchedules {
|
|
apiErr := NewJSMessageSchedulesDisabledError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
} else if scheduleTtl, ok := getMessageScheduleTTL(hdr); !ok {
|
|
apiErr := NewJSMessageSchedulesTTLInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
} else if scheduleRollup := getMessageScheduleRollup(hdr); scheduleRollup != _EMPTY_ && scheduleRollup != JSMsgRollupSubject {
|
|
apiErr := NewJSMessageSchedulesRollupInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
} else if scheduleTtl != _EMPTY_ && !allowTTL {
|
|
return hdr, msg, 0, NewJSMessageTTLDisabledError(), errMsgTTLDisabled
|
|
} else if scheduleTarget := getMessageScheduleTarget(hdr); scheduleTarget == _EMPTY_ ||
|
|
!IsValidPublishSubject(scheduleTarget) || scheduleTarget == subject {
|
|
apiErr := NewJSMessageSchedulesTargetInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
} else if scheduleSource := getMessageScheduleSource(hdr); scheduleSource != _EMPTY_ &&
|
|
(scheduleSource == scheduleTarget || scheduleSource == subject || !IsValidPublishSubject(scheduleSource)) {
|
|
apiErr := NewJSMessageSchedulesSourceInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
} else {
|
|
mset.cfgMu.RLock()
|
|
match := slices.ContainsFunc(mset.cfg.Subjects, func(subj string) bool {
|
|
return SubjectsCollide(subj, scheduleTarget)
|
|
})
|
|
if !match {
|
|
mset.cfgMu.RUnlock()
|
|
apiErr := NewJSMessageSchedulesTargetInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
}
|
|
if scheduleSource != _EMPTY_ {
|
|
match = slices.ContainsFunc(mset.cfg.Subjects, func(subj string) bool {
|
|
return SubjectsCollide(subj, scheduleSource)
|
|
})
|
|
if !match {
|
|
mset.cfgMu.RUnlock()
|
|
apiErr := NewJSMessageSchedulesSourceInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
}
|
|
}
|
|
mset.cfgMu.RUnlock()
|
|
|
|
// Add a rollup sub header if it doesn't already exist.
|
|
// Otherwise, it must exist already as a rollup on the subject.
|
|
if rollup := getRollup(hdr); rollup == _EMPTY_ {
|
|
hdr = genHeader(hdr, JSMsgRollup, JSMsgRollupSubject)
|
|
} else if rollup != JSMsgRollupSubject {
|
|
apiErr := NewJSMessageSchedulesRollupInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
}
|
|
}
|
|
}
|
|
if scheduleNext := sliceHeader(JSScheduleNext, hdr); len(scheduleNext) > 0 && !sourced {
|
|
// Clients may only use Nats-Schedule-Next to purge a schedule.
|
|
if bytesToString(scheduleNext) != JSScheduleNextPurge {
|
|
apiErr := NewJSMessageSchedulesSchedulerInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
}
|
|
// Nats-Scheduler must accompany the purge and:
|
|
// - it must NOT be empty.
|
|
// - it must NOT match the publish subject.
|
|
if scheduler := sliceHeader(JSScheduler, hdr); len(scheduler) == 0 ||
|
|
bytesToString(scheduler) == subject || !IsValidPublishSubject(bytesToString(scheduler)) {
|
|
apiErr := NewJSMessageSchedulesSchedulerInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
} else if !allowMsgSchedules {
|
|
apiErr := NewJSMessageSchedulesDisabledError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
} else {
|
|
// Check that the to-be-purged subject is a schedule message.
|
|
// We still allow this message through if there exists no message for this subject,
|
|
// to remain backward-compatible. An "expected at sequence" check can still be
|
|
// performed to make this stricter.
|
|
schedSubj := bytesToString(scheduler)
|
|
var invalid bool
|
|
if i, ok := diff.inflight[schedSubj]; ok {
|
|
invalid = !i.schedule
|
|
} else if i, ok = mset.inflight[schedSubj]; ok {
|
|
invalid = !i.schedule
|
|
} else {
|
|
var smv StoreMsg
|
|
sm, _ := mset.store.LoadLastMsg(schedSubj, &smv)
|
|
invalid = sm != nil && len(sliceHeader(JSSchedulePattern, sm.hdr)) == 0
|
|
}
|
|
if invalid {
|
|
apiErr := NewJSMessageSchedulesSchedulerInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
}
|
|
}
|
|
} else if !sourced && len(sliceHeader(JSScheduler, hdr)) > 0 {
|
|
// Clients may only use Nats-Scheduler alongside Nats-Schedule-Next.
|
|
apiErr := NewJSMessageSchedulesSchedulerInvalidError()
|
|
return hdr, msg, 0, apiErr, apiErr
|
|
}
|
|
|
|
// Check for any rollups.
|
|
if rollup := getRollup(hdr); rollup != _EMPTY_ {
|
|
if (!allowRollup || denyPurge) && !sourced {
|
|
err := errors.New("rollup not permitted")
|
|
return hdr, msg, 0, NewJSStreamRollupFailedError(err), err
|
|
}
|
|
switch rollup {
|
|
case JSMsgRollupSubject:
|
|
// Rolling up the subject is only allowed if the first occurrence of this subject in the batch.
|
|
if _, ok := diff.inflight[subject]; ok {
|
|
err := errors.New("batch rollup sub invalid")
|
|
return hdr, msg, 0, NewJSStreamRollupFailedError(err), err
|
|
}
|
|
case JSMsgRollupAll:
|
|
// Rolling up the whole stream is only allowed if this is the first message of the batch.
|
|
if len(diff.inflight) > 0 {
|
|
err := errors.New("batch rollup all invalid")
|
|
return hdr, msg, 0, NewJSStreamRollupFailedError(err), err
|
|
}
|
|
default:
|
|
err := fmt.Errorf("rollup value invalid: %q", rollup)
|
|
return hdr, msg, 0, NewJSStreamRollupFailedError(err), err
|
|
}
|
|
}
|
|
}
|
|
|
|
// Track inflight.
|
|
// Store the subject to ensure other messages in this batch using
|
|
// an expected check or rollup on the same subject fail.
|
|
if diff.inflight == nil {
|
|
diff.inflight = make(map[string]*inflightSubjectRunningTotal, 1)
|
|
}
|
|
var sz uint64
|
|
if mset.store.Type() == FileStorage {
|
|
sz = fileStoreMsgSizeRaw(len(subject), len(hdr), len(msg))
|
|
} else {
|
|
sz = memStoreMsgSizeRaw(len(subject), len(hdr), len(msg))
|
|
}
|
|
var (
|
|
i *inflightSubjectRunningTotal
|
|
ok bool
|
|
err error
|
|
)
|
|
if i, ok = diff.inflight[subject]; ok {
|
|
i.bytes += sz
|
|
i.ops++
|
|
i.schedule = hasSchedule
|
|
} else {
|
|
i = &inflightSubjectRunningTotal{bytes: sz, ops: 1, schedule: hasSchedule}
|
|
diff.inflight[subject] = i
|
|
}
|
|
|
|
// Subject transform.
|
|
if subject != rsubject {
|
|
// The 'subject' is a transformed subject used for consistency checks.
|
|
// But since we propose the original (raw) subject to our peers, we need
|
|
// to store the transformed subject separately for when we apply.
|
|
// TODO(mvv): since subject transforms are handled by each replica individually, this has a
|
|
// potential for desync given out-of-order stream subject transform updates.
|
|
if diff.inflightTransform == nil {
|
|
diff.inflightTransform = make(map[uint64]string, 1)
|
|
}
|
|
diff.inflightTransform[mset.clseq] = subject
|
|
}
|
|
|
|
// Check if we have discard new with max msgs or bytes.
|
|
// We need to deny here otherwise we'd need to bump CLFS, and it could succeed on some
|
|
// peers and not others depending on consumer ack state (if interest policy).
|
|
// So we deny here, if we allow that means we know it would succeed on every peer.
|
|
if discard == DiscardNew {
|
|
if maxMsgs > 0 || maxBytes > 0 {
|
|
// Track usual max msgs/bytes thresholds for DiscardNew.
|
|
var state StreamState
|
|
mset.store.FastState(&state)
|
|
|
|
totalMsgs := state.Msgs
|
|
totalBytes := state.Bytes
|
|
for _, i = range mset.inflight {
|
|
totalMsgs += i.ops
|
|
totalBytes += i.bytes
|
|
}
|
|
for _, i = range diff.inflight {
|
|
totalMsgs += i.ops
|
|
totalBytes += i.bytes
|
|
}
|
|
|
|
if maxMsgs > 0 && totalMsgs > uint64(maxMsgs) {
|
|
err = ErrMaxMsgs
|
|
} else if maxBytes > 0 && totalBytes > uint64(maxBytes) {
|
|
err = ErrMaxBytes
|
|
}
|
|
if err != nil {
|
|
return hdr, msg, 0, NewJSStreamStoreFailedError(err, Unless(err)), err
|
|
}
|
|
}
|
|
|
|
// Similarly, check DiscardNew per-subject threshold to not need to bump CLFS.
|
|
// Allow rollup messages through since they will purge after storing.
|
|
if discardNewPer && maxMsgsPer > 0 && len(sliceHeader(JSMsgRollup, hdr)) == 0 {
|
|
// Get the current total for this subject.
|
|
totalMsgsForSubject := mset.store.SubjectsTotals(subject)[subject]
|
|
// Add inflight count in this batch and for this stream.
|
|
totalMsgsForSubject += i.ops
|
|
if i, ok = mset.inflight[subject]; ok {
|
|
totalMsgsForSubject += i.ops
|
|
}
|
|
if totalMsgsForSubject > uint64(maxMsgsPer) {
|
|
err = ErrMaxMsgsPerSubject
|
|
return hdr, msg, 0, NewJSStreamStoreFailedError(err, Unless(err)), err
|
|
}
|
|
}
|
|
}
|
|
|
|
return hdr, msg, 0, nil, nil
|
|
}
|
|
|
|
// recalculateClusteredSeq initializes or updates mset.clseq, for example after a leader change.
|
|
// This is reused for normal clustered publishing into a stream, and for atomic and fast batch publishing.
|
|
// mset.clMu lock must be held.
|
|
func recalculateClusteredSeq(mset *stream, needStreamLock bool) (lseq uint64) {
|
|
// Need to unlock and re-acquire the locks in the proper order.
|
|
mset.clMu.Unlock()
|
|
// Locking order is stream -> batchMu -> clMu
|
|
if needStreamLock {
|
|
mset.mu.RLock()
|
|
}
|
|
batch := mset.batchApply
|
|
var batchCount uint64
|
|
if batch != nil {
|
|
batch.mu.Lock()
|
|
batchCount = batch.count
|
|
}
|
|
mset.clMu.Lock()
|
|
// Re-capture
|
|
lseq = mset.lseq
|
|
mset.clseq = lseq + mset.clfs + batchCount
|
|
// Keep hold of the mset.clMu, but unlock the others.
|
|
if batch != nil {
|
|
batch.mu.Unlock()
|
|
}
|
|
if needStreamLock {
|
|
mset.mu.RUnlock()
|
|
}
|
|
return lseq
|
|
}
|
|
|
|
// commitSingleMsg commits and proposes a single message to the node.
|
|
// This is reused both for normal publishing into a stream, and for fast batch publishing.
|
|
// mset.clMu lock must be held.
|
|
func commitSingleMsg(
|
|
diff *batchStagedDiff, mset *stream, subject string, reply string, hdr []byte, msg []byte, name string,
|
|
jsa *jsAccount, mt *msgTrace, node RaftNode, replicas int, lseq uint64,
|
|
) error {
|
|
// Do proposal.
|
|
esm := encodeStreamMsgAllowCompress(subject, reply, hdr, msg, mset.clseq, time.Now().UnixNano(), false)
|
|
if err := node.Propose(esm); err != nil {
|
|
return err
|
|
}
|
|
|
|
var mtKey uint64
|
|
if mt != nil {
|
|
mtKey = mset.clseq
|
|
if mset.mt == nil {
|
|
mset.mt = make(map[uint64]*msgTrace)
|
|
}
|
|
mset.mt[mtKey] = mt
|
|
}
|
|
|
|
diff.commit(mset)
|
|
mset.clseq++
|
|
mset.trackReplicationTraffic(node, len(esm), replicas)
|
|
|
|
// Check to see if we are being overrun.
|
|
// TODO(dlc) - Make this a limit where we drop messages to protect ourselves, but allow to be configured.
|
|
if mset.clseq-(lseq+mset.clfs) > streamLagWarnThreshold {
|
|
lerr := fmt.Errorf("JetStream stream '%s > %s' has high message lag", jsa.acc().Name, name)
|
|
mset.srv.RateLimitWarnf("%s", lerr.Error())
|
|
}
|
|
return nil
|
|
}
|