9261 lines
281 KiB
Go
9261 lines
281 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"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math"
|
|
"math/big"
|
|
"math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/antithesishq/antithesis-sdk-go/assert"
|
|
"github.com/klauspost/compress/s2"
|
|
"github.com/nats-io/nats-server/v2/server/gsl"
|
|
"github.com/nats-io/nuid"
|
|
)
|
|
|
|
// StreamConfigRequest is used to create or update a stream.
|
|
type StreamConfigRequest struct {
|
|
StreamConfig
|
|
// This is not part of the StreamConfig, because its scoped to request,
|
|
// and not to the stream itself.
|
|
Pedantic bool `json:"pedantic,omitempty"`
|
|
}
|
|
|
|
// StreamConfig will determine the name, subjects and retention policy
|
|
// for a given stream. If subjects is empty the name will be used.
|
|
type StreamConfig struct {
|
|
Name string `json:"name"`
|
|
Description string `json:"description,omitempty"`
|
|
Subjects []string `json:"subjects,omitempty"`
|
|
Retention RetentionPolicy `json:"retention"`
|
|
MaxConsumers int `json:"max_consumers"`
|
|
MaxMsgs int64 `json:"max_msgs"`
|
|
MaxBytes int64 `json:"max_bytes"`
|
|
MaxAge time.Duration `json:"max_age"`
|
|
MaxMsgsPer int64 `json:"max_msgs_per_subject"`
|
|
MaxMsgSize int32 `json:"max_msg_size,omitempty"`
|
|
Discard DiscardPolicy `json:"discard"`
|
|
Storage StorageType `json:"storage"`
|
|
Replicas int `json:"num_replicas"`
|
|
NoAck bool `json:"no_ack,omitempty"`
|
|
Duplicates time.Duration `json:"duplicate_window,omitempty"`
|
|
Placement *Placement `json:"placement,omitempty"`
|
|
Mirror *StreamSource `json:"mirror,omitempty"`
|
|
Sources []*StreamSource `json:"sources,omitempty"`
|
|
Compression StoreCompression `json:"compression"`
|
|
FirstSeq uint64 `json:"first_seq,omitempty"`
|
|
|
|
// Allow applying a subject transform to incoming messages before doing anything else
|
|
SubjectTransform *SubjectTransformConfig `json:"subject_transform,omitempty"`
|
|
|
|
// Allow republish of the message after being sequenced and stored.
|
|
RePublish *RePublish `json:"republish,omitempty"`
|
|
|
|
// Allow higher performance, direct access to get individual messages. E.g. KeyValue
|
|
AllowDirect bool `json:"allow_direct"`
|
|
// Allow higher performance and unified direct access for mirrors as well.
|
|
MirrorDirect bool `json:"mirror_direct"`
|
|
|
|
// Allow KV like semantics to also discard new on a per subject basis
|
|
DiscardNewPer bool `json:"discard_new_per_subject,omitempty"`
|
|
|
|
// Optional qualifiers. These can not be modified after set to true.
|
|
|
|
// Sealed will seal a stream so no messages can get out or in.
|
|
Sealed bool `json:"sealed"`
|
|
// DenyDelete will restrict the ability to delete messages.
|
|
DenyDelete bool `json:"deny_delete"`
|
|
// DenyPurge will restrict the ability to purge messages.
|
|
DenyPurge bool `json:"deny_purge"`
|
|
// AllowRollup allows messages to be placed into the system and purge
|
|
// all older messages using a special msg header.
|
|
AllowRollup bool `json:"allow_rollup_hdrs"`
|
|
|
|
// The following defaults will apply to consumers when created against
|
|
// this stream, unless overridden manually.
|
|
// TODO(nat): Can/should we name these better?
|
|
ConsumerLimits StreamConsumerLimits `json:"consumer_limits"`
|
|
|
|
// AllowMsgTTL allows header initiated per-message TTLs. If disabled,
|
|
// then the `NATS-TTL` header will be ignored.
|
|
AllowMsgTTL bool `json:"allow_msg_ttl"`
|
|
|
|
// SubjectDeleteMarkerTTL sets the TTL of delete marker messages left behind by
|
|
// subject delete markers.
|
|
SubjectDeleteMarkerTTL time.Duration `json:"subject_delete_marker_ttl,omitempty"`
|
|
|
|
// AllowMsgCounter allows a stream to use (only) counter CRDTs.
|
|
AllowMsgCounter bool `json:"allow_msg_counter,omitempty"`
|
|
|
|
// AllowAtomicPublish allows atomic batch publishing into the stream.
|
|
AllowAtomicPublish bool `json:"allow_atomic,omitempty"`
|
|
|
|
// AllowMsgSchedules allows the scheduling of messages.
|
|
AllowMsgSchedules bool `json:"allow_msg_schedules,omitempty"`
|
|
|
|
// PersistMode allows to opt-in to different persistence mode settings.
|
|
PersistMode PersistModeType `json:"persist_mode,omitempty"`
|
|
|
|
// AllowBatchPublish allows fast batch publishing into the stream.
|
|
AllowBatchPublish bool `json:"allow_batched,omitempty"`
|
|
|
|
// Metadata is additional metadata for the Stream.
|
|
Metadata map[string]string `json:"metadata,omitempty"`
|
|
}
|
|
|
|
// clone performs a deep copy of the StreamConfig struct, returning a new clone with
|
|
// all values copied.
|
|
func (cfg *StreamConfig) clone() *StreamConfig {
|
|
clone := *cfg
|
|
if cfg.Placement != nil {
|
|
placement := *cfg.Placement
|
|
clone.Placement = &placement
|
|
}
|
|
if cfg.Mirror != nil {
|
|
mirror := *cfg.Mirror
|
|
clone.Mirror = &mirror
|
|
}
|
|
if len(cfg.Sources) > 0 {
|
|
clone.Sources = make([]*StreamSource, len(cfg.Sources))
|
|
for i, cfgSource := range cfg.Sources {
|
|
source := *cfgSource
|
|
clone.Sources[i] = &source
|
|
}
|
|
}
|
|
if cfg.SubjectTransform != nil {
|
|
transform := *cfg.SubjectTransform
|
|
clone.SubjectTransform = &transform
|
|
}
|
|
if cfg.RePublish != nil {
|
|
rePublish := *cfg.RePublish
|
|
clone.RePublish = &rePublish
|
|
}
|
|
if cfg.Metadata != nil {
|
|
clone.Metadata = make(map[string]string, len(cfg.Metadata))
|
|
for k, v := range cfg.Metadata {
|
|
clone.Metadata[k] = v
|
|
}
|
|
}
|
|
return &clone
|
|
}
|
|
|
|
type StreamConsumerLimits struct {
|
|
InactiveThreshold time.Duration `json:"inactive_threshold,omitempty"`
|
|
MaxAckPending int `json:"max_ack_pending,omitempty"`
|
|
}
|
|
|
|
// SubjectTransformConfig is for applying a subject transform (to matching messages) before doing anything else when a new message is received
|
|
type SubjectTransformConfig struct {
|
|
Source string `json:"src"`
|
|
Destination string `json:"dest"`
|
|
}
|
|
|
|
// RePublish is for republishing messages once committed to a stream.
|
|
type RePublish struct {
|
|
Source string `json:"src,omitempty"`
|
|
Destination string `json:"dest"`
|
|
HeadersOnly bool `json:"headers_only,omitempty"`
|
|
}
|
|
|
|
// PersistModeType determines what persistence mode the stream uses.
|
|
type PersistModeType int
|
|
|
|
const (
|
|
// DefaultPersistMode specifies the default persist mode. Writes to the stream will immediately be flushed.
|
|
// The publish acknowledgement will be sent after the persisting completes.
|
|
DefaultPersistMode = PersistModeType(iota)
|
|
// AsyncPersistMode specifies writes to the stream will be flushed asynchronously.
|
|
// The publish acknowledgement may be sent before the persisting completes.
|
|
// This means writes could be lost if they weren't flushed prior to a hard kill of the server.
|
|
AsyncPersistMode
|
|
)
|
|
|
|
const (
|
|
defaultPersistModeJSONString = `"default"`
|
|
asyncPersistModeJSONString = `"async"`
|
|
)
|
|
|
|
var (
|
|
defaultPersistModeJSONBytes = []byte(defaultPersistModeJSONString)
|
|
asyncPersistModeJSONBytes = []byte(asyncPersistModeJSONString)
|
|
)
|
|
|
|
func (wc PersistModeType) String() string {
|
|
switch wc {
|
|
case DefaultPersistMode:
|
|
return "Default"
|
|
case AsyncPersistMode:
|
|
return "Async"
|
|
default:
|
|
return "Unknown Persist Mode Type"
|
|
}
|
|
}
|
|
|
|
func (wc PersistModeType) MarshalJSON() ([]byte, error) {
|
|
switch wc {
|
|
case DefaultPersistMode:
|
|
return defaultPersistModeJSONBytes, nil
|
|
case AsyncPersistMode:
|
|
return asyncPersistModeJSONBytes, nil
|
|
default:
|
|
return nil, fmt.Errorf("can not marshal %v", wc)
|
|
}
|
|
}
|
|
|
|
func (wc *PersistModeType) UnmarshalJSON(data []byte) error {
|
|
switch string(data) {
|
|
case defaultPersistModeJSONString, `""`:
|
|
*wc = DefaultPersistMode
|
|
case asyncPersistModeJSONString:
|
|
*wc = AsyncPersistMode
|
|
default:
|
|
return fmt.Errorf("can not unmarshal %q", data)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// JSPubAckResponse is a formal response to a publish operation.
|
|
type JSPubAckResponse struct {
|
|
Error *ApiError `json:"error,omitempty"`
|
|
*PubAck
|
|
}
|
|
|
|
// ToError checks if the response has a error and if it does converts it to an error
|
|
// avoiding the pitfalls described by https://yourbasic.org/golang/gotcha-why-nil-error-not-equal-nil/
|
|
func (r *JSPubAckResponse) ToError() error {
|
|
if r.Error == nil {
|
|
return nil
|
|
}
|
|
return r.Error
|
|
}
|
|
|
|
// PubAck is the detail you get back from a publish to a stream that was successful.
|
|
// e.g. +OK {"stream": "Orders", "seq": 22}
|
|
type PubAck struct {
|
|
Stream string `json:"stream"`
|
|
Sequence uint64 `json:"seq"`
|
|
Domain string `json:"domain,omitempty"`
|
|
Duplicate bool `json:"duplicate,omitempty"`
|
|
Value string `json:"val,omitempty"`
|
|
BatchId string `json:"batch,omitempty"`
|
|
BatchSize int `json:"count,omitempty"`
|
|
}
|
|
|
|
// CounterValue is the body of a message when used as a counter.
|
|
// e.g. {"val":"123"}
|
|
type CounterValue struct {
|
|
Value string `json:"val"`
|
|
}
|
|
|
|
// CounterSources is the body of the Nats-Counter-Sources header.
|
|
// e.g. {"stream":{"subject":"123"}}
|
|
type CounterSources map[string]map[string]string
|
|
|
|
// BatchFlowAck is used for flow control when fast batch publishing into a stream.
|
|
// This message is vital to handling acknowledgements and flow control.
|
|
// These may technically be lost without the client receiving it. The client can retrieve
|
|
// these by using the "ping" operation if it's expecting acks but not receiving any.
|
|
type BatchFlowAck struct {
|
|
// Type: "ack"
|
|
Type string `json:"type"`
|
|
// Sequence is the sequence of the message that triggered the ack.
|
|
// If "gap: fail" this means the messages up to and including Sequence were persisted.
|
|
// If "gap: ok" this means _some_ of the messages up to and including Sequence were persisted.
|
|
// But there could have been gaps.
|
|
Sequence uint64 `json:"seq"`
|
|
// Messages indicates acknowledgements will be sent every N messages.
|
|
Messages uint16 `json:"msgs"`
|
|
}
|
|
|
|
func (ack BatchFlowAck) MarshalJSON() ([]byte, error) {
|
|
type Alias BatchFlowAck
|
|
a := Alias(ack)
|
|
a.Type = "ack"
|
|
return json.Marshal(a)
|
|
}
|
|
|
|
// BatchFlowGap is used for reporting gaps when fast batch publishing into a stream.
|
|
// This message is purely informational and could technically be lost without the client receiving it.
|
|
type BatchFlowGap struct {
|
|
// Type: "gap"
|
|
Type string `json:"type"`
|
|
// ExpectedLastSequence is the sequence expected to be received next.
|
|
// Messages starting from ExpectedLastSequence up to (but not including) CurrentSequence were lost.
|
|
ExpectedLastSequence uint64 `json:"last_seq"`
|
|
// CurrentSequence is the sequence of the message that just came in and detected the gap.
|
|
CurrentSequence uint64 `json:"seq"`
|
|
}
|
|
|
|
func (gap BatchFlowGap) MarshalJSON() ([]byte, error) {
|
|
type Alias BatchFlowGap
|
|
a := Alias(gap)
|
|
a.Type = "gap"
|
|
return json.Marshal(a)
|
|
}
|
|
|
|
// BatchFlowErr is used for reporting errors when fast batch publishing into a stream.
|
|
// This message is purely informational and could technically be lost without the client receiving it.
|
|
type BatchFlowErr struct {
|
|
// Type: "err"
|
|
Type string `json:"type"`
|
|
// Sequence is the sequence of the message that triggered the error.
|
|
// There are no (relative) guarantees whatsoever about whether the messages up to this sequence were persisted.
|
|
// Such guarantees require the use of "gap: fail" and listening for BatchFlowAck and PubAck.
|
|
Sequence uint64 `json:"seq"`
|
|
// Error is used to return the error for the Sequence.
|
|
Error *ApiError `json:"error"`
|
|
}
|
|
|
|
func (err BatchFlowErr) MarshalJSON() ([]byte, error) {
|
|
type Alias BatchFlowErr
|
|
a := Alias(err)
|
|
a.Type = "err"
|
|
return json.Marshal(a)
|
|
}
|
|
|
|
// StreamInfo shows config and current state for this stream.
|
|
type StreamInfo struct {
|
|
Config StreamConfig `json:"config"`
|
|
Created time.Time `json:"created"`
|
|
State StreamState `json:"state"`
|
|
Domain string `json:"domain,omitempty"`
|
|
Cluster *ClusterInfo `json:"cluster,omitempty"`
|
|
Mirror *StreamSourceInfo `json:"mirror,omitempty"`
|
|
Sources []*StreamSourceInfo `json:"sources,omitempty"`
|
|
Alternates []StreamAlternate `json:"alternates,omitempty"`
|
|
// TimeStamp indicates when the info was gathered
|
|
TimeStamp time.Time `json:"ts"`
|
|
}
|
|
|
|
// streamInfoClusterResponse is a response used in a cluster to communicate the stream info
|
|
// back to the meta leader as part of a stream list request.
|
|
type streamInfoClusterResponse struct {
|
|
StreamInfo
|
|
OfflineReason string `json:"offline_reason,omitempty"` // Reporting when a stream is offline.
|
|
}
|
|
|
|
type StreamAlternate struct {
|
|
Name string `json:"name"`
|
|
Domain string `json:"domain,omitempty"`
|
|
Cluster string `json:"cluster"`
|
|
}
|
|
|
|
// ClusterInfo shows information about the underlying set of servers
|
|
// that make up the stream or consumer.
|
|
type ClusterInfo struct {
|
|
Name string `json:"name,omitempty"`
|
|
RaftGroup string `json:"raft_group,omitempty"`
|
|
Leader string `json:"leader,omitempty"`
|
|
LeaderSince *time.Time `json:"leader_since,omitempty"`
|
|
SystemAcc bool `json:"system_account,omitempty"`
|
|
TrafficAcc string `json:"traffic_account,omitempty"`
|
|
Replicas []*PeerInfo `json:"replicas,omitempty"`
|
|
}
|
|
|
|
// PeerInfo shows information about all the peers in the cluster that
|
|
// are supporting the stream or consumer.
|
|
type PeerInfo struct {
|
|
Name string `json:"name"` // Name is the unique name for the peer
|
|
Current bool `json:"current"` // Current indicates if it was seen recently and fully caught up
|
|
Offline bool `json:"offline,omitempty"` // Offline indicates if it has not been seen recently
|
|
Active time.Duration `json:"active"` // Active is the timestamp it was last active
|
|
Lag uint64 `json:"lag,omitempty"` // Lag is how many operations behind it is
|
|
Peer string `json:"peer"` // Peer is the unique ID for the peer
|
|
// For migrations.
|
|
cluster string
|
|
}
|
|
|
|
// StreamSourceInfo shows information about an upstream stream source.
|
|
type StreamSourceInfo struct {
|
|
Name string `json:"name"`
|
|
External *ExternalStream `json:"external,omitempty"`
|
|
Lag uint64 `json:"lag"`
|
|
Active time.Duration `json:"active"`
|
|
Error *ApiError `json:"error,omitempty"`
|
|
FilterSubject string `json:"filter_subject,omitempty"`
|
|
SubjectTransforms []SubjectTransformConfig `json:"subject_transforms,omitempty"`
|
|
}
|
|
|
|
// StreamSource dictates how streams can source from other streams.
|
|
type StreamSource struct {
|
|
Name string `json:"name"`
|
|
OptStartSeq uint64 `json:"opt_start_seq,omitempty"`
|
|
OptStartTime *time.Time `json:"opt_start_time,omitempty"`
|
|
FilterSubject string `json:"filter_subject,omitempty"`
|
|
SubjectTransforms []SubjectTransformConfig `json:"subject_transforms,omitempty"`
|
|
External *ExternalStream `json:"external,omitempty"`
|
|
Consumer *StreamConsumerSource `json:"consumer,omitempty"`
|
|
|
|
// Internal
|
|
iname string // For indexing when stream names are the same for multiple sources.
|
|
}
|
|
|
|
// StreamConsumerSource dictates a durable consumer with a specific name is used for sourcing.
|
|
type StreamConsumerSource struct {
|
|
Name string `json:"name,omitempty"`
|
|
DeliverSubject string `json:"deliver_subject,omitempty"`
|
|
}
|
|
|
|
// ExternalStream allows you to qualify access to a stream source in another account or domain.
|
|
type ExternalStream struct {
|
|
ApiPrefix string `json:"api"`
|
|
DeliverPrefix string `json:"deliver"`
|
|
}
|
|
|
|
// Will return the domain for this external stream.
|
|
func (ext *ExternalStream) Domain() string {
|
|
if ext == nil || ext.ApiPrefix == _EMPTY_ {
|
|
return _EMPTY_
|
|
}
|
|
return tokenAt(ext.ApiPrefix, 2)
|
|
}
|
|
|
|
// For managing stream ingest.
|
|
const (
|
|
streamDefaultMaxQueueMsgs = 100_000
|
|
streamDefaultMaxQueueBytes = 128 * 1024 * 1024
|
|
)
|
|
|
|
// For managing stream batches.
|
|
const (
|
|
streamDefaultMaxBatchTimeout = 10 * time.Second
|
|
// Atomic batches.
|
|
streamDefaultMaxAtomicBatchInflightPerStream = 50
|
|
streamDefaultMaxAtomicBatchInflightTotal = 1000
|
|
streamDefaultMaxAtomicBatchSize = 1000
|
|
// Fast batches.
|
|
streamDefaultMaxFastBatchInflightPerStream = 1000
|
|
streamDefaultMaxFastBatchInflightTotal = 50_000
|
|
)
|
|
|
|
var (
|
|
streamMaxBatchTimeout = streamDefaultMaxBatchTimeout
|
|
// Atomic batches.
|
|
streamMaxAtomicBatchInflightPerStream = streamDefaultMaxAtomicBatchInflightPerStream
|
|
streamMaxAtomicBatchInflightTotal = streamDefaultMaxAtomicBatchInflightTotal
|
|
streamMaxAtomicBatchSize = streamDefaultMaxAtomicBatchSize
|
|
// Fast batches.
|
|
streamMaxFastBatchInflightPerStream = streamDefaultMaxFastBatchInflightPerStream
|
|
streamMaxFastBatchInflightTotal = streamDefaultMaxFastBatchInflightTotal
|
|
)
|
|
|
|
// Stream is a jetstream stream of messages. When we receive a message internally destined
|
|
// for a Stream we will direct link from the client to this structure.
|
|
type stream struct {
|
|
mu sync.RWMutex // Read/write lock for the stream.
|
|
js *jetStream // The internal *jetStream for the account.
|
|
jsa *jsAccount // The JetStream account-level information.
|
|
acc *Account // The account this stream is defined in.
|
|
srv *Server // The server we are running in.
|
|
client *client // The internal JetStream client.
|
|
sysc *client // The internal JetStream system client.
|
|
|
|
// The current last subscription ID for the subscriptions through `client`.
|
|
// Those subscriptions are for the subjects filters being listened to and captured by the stream.
|
|
sid atomic.Uint64
|
|
|
|
pubAck []byte // The template (prefix) to generate the pubAck responses for this stream quickly.
|
|
outq *jsOutQ // Queue of *jsPubMsg for sending messages.
|
|
msgs *ipQueue[*inMsg] // Intra-process queue for the ingress of messages.
|
|
gets *ipQueue[*directGetReq] // Intra-process queue for the direct get requests.
|
|
store StreamStore // The storage for this stream.
|
|
ackq *ipQueue[uint64] // Intra-process queue for acks.
|
|
lseq uint64 // The sequence number of the last message stored in the stream.
|
|
lmsgId string // The de-duplication message ID of the last message stored in the stream.
|
|
consumers map[string]*consumer // The consumers for this stream.
|
|
numFilter int // The number of filtered consumers.
|
|
cfg StreamConfig // The stream's config.
|
|
cfgMu sync.RWMutex // Config mutex used to solve some races with consumer code
|
|
created time.Time // Time the stream was created.
|
|
stype StorageType // The storage type.
|
|
tier string // The tier is the number of replicas for the stream (e.g. "R1" or "R3").
|
|
ddMu sync.Mutex // Lock for dedupe state.
|
|
ddmap map[string]*ddentry // The dedupe map.
|
|
ddarr []*ddentry // The dedupe array.
|
|
ddindex int // The dedupe index.
|
|
ddtmr *time.Timer // The dedupe timer.
|
|
qch chan struct{} // The quit channel.
|
|
mqch chan struct{} // The monitor's quit channel.
|
|
active bool // Indicates that there are active internal subscriptions (for the subject filters)
|
|
// and/or mirror/sources consumers are scheduled to be established or already started.
|
|
closed atomic.Bool // Set to true when stop() is called on the stream.
|
|
cisrun atomic.Bool // Indicates one checkInterestState is already running.
|
|
|
|
// Mirror
|
|
mirror *sourceInfo
|
|
mirrorConsumerSetup *time.Timer
|
|
|
|
// Sources
|
|
sources map[string]*sourceInfo
|
|
sourceSetupSchedules map[string]*time.Timer
|
|
sourcesConsumerSetup *time.Timer
|
|
smsgs *ipQueue[*inMsg] // Intra-process queue for all incoming sourced messages.
|
|
|
|
// Indicates we have direct/sourcing consumers.
|
|
sourcingConsumers int
|
|
|
|
// For input subject transform.
|
|
itr *subjectTransform
|
|
|
|
// For republishing.
|
|
tr *subjectTransform
|
|
|
|
// For processing consumers without main stream lock.
|
|
clsMu sync.RWMutex
|
|
cList []*consumer // Consumer list.
|
|
sch chan struct{} // Channel to signal consumers.
|
|
sigq *ipQueue[*cMsg] // Intra-process queue for the messages to signal to the consumers.
|
|
csl *gsl.GenericSublist[*consumer] // Consumer subscription list.
|
|
|
|
// Leader will store seq/msgTrace in clustering mode. Used in applyStreamEntries
|
|
// to know if trace event should be sent after processing.
|
|
mt map[uint64]*msgTrace
|
|
|
|
// For non limits policy streams when they process an ack before the actual msg.
|
|
// Can happen in stretch clusters, multi-cloud, or during catchup for a restarted server.
|
|
preAcks map[uint64]map[*consumer]struct{}
|
|
|
|
// TODO(dlc) - Hide everything below behind two pointers.
|
|
// Clustered mode.
|
|
sa *streamAssignment // What the meta controller uses to assign streams to peers.
|
|
node RaftNode // Our RAFT node for the stream's group.
|
|
catchup atomic.Bool // Used to signal we are in catchup mode.
|
|
catchups map[string]uint64 // The number of messages that need to be caught per peer.
|
|
syncSub *subscription // Internal subscription for sync messages (on "$JSC.SYNC").
|
|
infoSub *subscription // Internal subscription for stream info requests.
|
|
clMu sync.Mutex // The mutex for clseq and clfs.
|
|
clseq uint64 // The current last seq being proposed to the NRG layer.
|
|
clfs uint64 // The count (offset) of the number of failed NRG sequences used to compute clseq.
|
|
lqsent time.Time // The time at which the last lost quorum advisory was sent. Used to rate limit.
|
|
uch chan struct{} // The channel to signal updates to the monitor routine.
|
|
inMonitor bool // True if the monitor routine has been started.
|
|
werr error // If a write error was encountered, and if so what error.
|
|
|
|
inflight map[string]*inflightSubjectRunningTotal // Inflight message sizes per subject.
|
|
inflightTransform map[uint64]string // Inflight message's optional transformed subject.
|
|
clusteredCounterTotal map[string]*msgCounterRunningTotal // Inflight counter totals.
|
|
expectedPerSubjectSequence map[uint64]string // Inflight 'expected per subject' subjects per clseq.
|
|
expectedPerSubjectInProcess map[string]struct{} // Current 'expected per subject' subjects in process.
|
|
|
|
// Direct get subscription.
|
|
directLeaderSub *subscription
|
|
directSub *subscription
|
|
lastBySub *subscription
|
|
mirrorDirectSub *subscription // Mirrors only.
|
|
mirrorLastBySub *subscription // Mirrors only.
|
|
|
|
monitorWg sync.WaitGroup // Wait group for the monitor routine.
|
|
|
|
// If standalone/single-server, the offline reason needs to be stored directly in the stream.
|
|
// Otherwise, if clustered it will be part of the stream assignment.
|
|
offlineReason string
|
|
|
|
batches *batching // Inflight batches prior to committing them.
|
|
batchApply *batchApply // State to check for batch completeness before applying it.
|
|
}
|
|
|
|
// inflightSubjectRunningTotal stores a running total of inflight messages for a specific subject.
|
|
type inflightSubjectRunningTotal struct {
|
|
bytes uint64 // Running total of inflight bytes for inflight messages.
|
|
ops uint64 // Inflight operations, i.e. inflight messages for this subject. If this reaches zero, we can remove the running total.
|
|
}
|
|
|
|
// msgCounterRunningTotal stores a running total and a number of inflight
|
|
// but not yet applied clustered proposals/operations for this counter.
|
|
type msgCounterRunningTotal struct {
|
|
total *big.Int // Running total.
|
|
sources CounterSources // Last seen counter sources.
|
|
ops uint64 // Inflight operations. If this reaches zero, we can remove the running total.
|
|
}
|
|
|
|
type sourceInfo struct {
|
|
name string // The name of the stream being sourced.
|
|
iname string // The unique index name of this particular source.
|
|
cname string // The name of the current consumer for this source.
|
|
sub *subscription // The subscription to the consumer.
|
|
|
|
msgs *ipQueue[*inMsg] // Intra-process queue for incoming messages.
|
|
sseq uint64 // Last stream message sequence number seen from the source.
|
|
dseq uint64 // Last delivery (i.e. consumer's) sequence number.
|
|
lag uint64 // 0 or number of messages pending (as last reported by the consumer) - 1.
|
|
err *ApiError // The API error that caused the last consumer setup to fail.
|
|
fails int // The number of times trying to setup the consumer failed.
|
|
last atomic.Int64 // Time the consumer was created or of last message it received.
|
|
lreq time.Time // The last time setupMirrorConsumer/setupSourceConsumer was called.
|
|
qch chan struct{} // Quit channel.
|
|
sip bool // Setup in progress.
|
|
wg sync.WaitGroup // WaitGroup for the consumer's go routine.
|
|
sf string // The subject filter.
|
|
sfs []string // The subject filters.
|
|
trs []*subjectTransform // The subject transforms.
|
|
}
|
|
|
|
// For mirrors and direct get
|
|
const (
|
|
dgetGroup = sysGroup
|
|
dgetCaughtUpThresh = 10
|
|
)
|
|
|
|
// Headers for published messages.
|
|
const (
|
|
JSMsgId = "Nats-Msg-Id"
|
|
JSExpectedStream = "Nats-Expected-Stream"
|
|
JSExpectedLastSeq = "Nats-Expected-Last-Sequence"
|
|
JSExpectedLastSubjSeq = "Nats-Expected-Last-Subject-Sequence"
|
|
JSExpectedLastSubjSeqSubj = "Nats-Expected-Last-Subject-Sequence-Subject"
|
|
JSExpectedLastMsgId = "Nats-Expected-Last-Msg-Id"
|
|
JSStreamSource = "Nats-Stream-Source"
|
|
JSLastConsumerSeq = "Nats-Last-Consumer"
|
|
JSLastStreamSeq = "Nats-Last-Stream"
|
|
JSConsumerStalled = "Nats-Consumer-Stalled"
|
|
JSMsgRollup = "Nats-Rollup"
|
|
JSMsgSize = "Nats-Msg-Size"
|
|
JSResponseType = "Nats-Response-Type"
|
|
JSMessageTTL = "Nats-TTL"
|
|
JSMarkerReason = "Nats-Marker-Reason"
|
|
JSMessageIncr = "Nats-Incr"
|
|
JSMessageCounterSources = "Nats-Counter-Sources"
|
|
JSBatchId = "Nats-Batch-Id"
|
|
JSBatchSeq = "Nats-Batch-Sequence"
|
|
JSBatchCommit = "Nats-Batch-Commit"
|
|
JSSchedulePattern = "Nats-Schedule"
|
|
JSScheduleTimeZone = "Nats-Schedule-Time-Zone"
|
|
JSScheduleTTL = "Nats-Schedule-TTL"
|
|
JSScheduleRollup = "Nats-Schedule-Rollup"
|
|
JSScheduleTarget = "Nats-Schedule-Target"
|
|
JSScheduleSource = "Nats-Schedule-Source"
|
|
)
|
|
|
|
// Headers for published KV messages.
|
|
var (
|
|
KVOperation = "KV-Operation"
|
|
KVOperationValuePurge = []byte("PURGE")
|
|
)
|
|
|
|
// Headers for scheduled messages.
|
|
const (
|
|
JSScheduler = "Nats-Scheduler"
|
|
JSScheduleNext = "Nats-Schedule-Next"
|
|
JSScheduleNextPurge = "purge" // If it's a non-repeating/delayed message, the schedule is purged.
|
|
)
|
|
|
|
// Headers for republished messages and direct get responses.
|
|
const (
|
|
JSStream = "Nats-Stream"
|
|
JSSequence = "Nats-Sequence"
|
|
JSTimeStamp = "Nats-Time-Stamp"
|
|
JSSubject = "Nats-Subject"
|
|
JSLastSequence = "Nats-Last-Sequence" // The sequence number of the precedent message either republished or in a batch of responses. 0 in the first message.
|
|
JSNumPending = "Nats-Num-Pending" // Number of messages pending in the multi/batched get response
|
|
JSUpToSequence = "Nats-UpTo-Sequence"
|
|
)
|
|
|
|
// Rollups, can be subject only or all messages.
|
|
const (
|
|
JSMsgRollupSubject = "sub"
|
|
JSMsgRollupAll = "all"
|
|
)
|
|
|
|
// Applied limits in the Nats-Applied-Limit header.
|
|
const (
|
|
JSMarkerReasonMaxAge = "MaxAge"
|
|
JSMarkerReasonPurge = "Purge"
|
|
JSMarkerReasonRemove = "Remove"
|
|
)
|
|
|
|
const (
|
|
jsCreateResponse = "create"
|
|
)
|
|
|
|
// Dedupe entry
|
|
type ddentry struct {
|
|
id string // The unique message ID provided by the client.
|
|
seq uint64 // The sequence number of the message.
|
|
ts int64 // The timestamp of the message.
|
|
}
|
|
|
|
// Replicas Range
|
|
const StreamMaxReplicas = 5
|
|
|
|
// AddStream adds a stream for the given account.
|
|
func (a *Account) addStream(config *StreamConfig) (*stream, error) {
|
|
return a.addStreamWithAssignment(config, nil, nil, false, false)
|
|
}
|
|
|
|
// recoverStream recovers a stream from disk for the given account.
|
|
func (a *Account) recoverStream(config *StreamConfig) (*stream, error) {
|
|
return a.addStreamWithAssignment(config, nil, nil, false, true)
|
|
}
|
|
|
|
// AddStreamWithStore adds a stream for the given account with custome store config options.
|
|
func (a *Account) addStreamWithStore(config *StreamConfig, fsConfig *FileStoreConfig) (*stream, error) {
|
|
return a.addStreamWithAssignment(config, fsConfig, nil, false, false)
|
|
}
|
|
|
|
func (a *Account) addStreamPedantic(config *StreamConfig, pedantic bool) (*stream, error) {
|
|
return a.addStreamWithAssignment(config, nil, nil, pedantic, false)
|
|
}
|
|
|
|
func (a *Account) addStreamWithAssignment(config *StreamConfig, fsConfig *FileStoreConfig, sa *streamAssignment, pedantic, recovering bool) (*stream, error) {
|
|
s, jsa, err := a.checkForJetStream()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// If we do not have the stream currently assigned to us in cluster mode we will proceed but warn.
|
|
// This can happen on startup with restored state where on meta replay we still do not have
|
|
// the assignment. Running in single server mode this always returns true.
|
|
if !jsa.streamAssigned(config.Name) {
|
|
s.Debugf("Stream '%s > %s' does not seem to be assigned to this server", a.Name, config.Name)
|
|
}
|
|
|
|
// Sensible defaults.
|
|
ccfg, apiErr := s.checkStreamCfg(config, a, pedantic)
|
|
if apiErr != nil {
|
|
return nil, apiErr
|
|
}
|
|
cfg := &ccfg
|
|
|
|
singleServerMode := !s.JetStreamIsClustered() && s.standAloneMode()
|
|
if singleServerMode && cfg.Replicas > 1 {
|
|
return nil, ApiErrors[JSStreamReplicasNotSupportedErr]
|
|
}
|
|
|
|
// Make sure we are ok when these are done in parallel.
|
|
// We used to call Add(1) in the "else" clause of the "if loaded"
|
|
// statement. This caused a data race because it was possible
|
|
// that one go routine stores (with count==0) and another routine
|
|
// gets "loaded==true" and calls wg.Wait() while the other routine
|
|
// then calls wg.Add(1). It also could mean that two routines execute
|
|
// the rest of the code concurrently.
|
|
swg := &sync.WaitGroup{}
|
|
swg.Add(1)
|
|
v, loaded := jsa.inflight.LoadOrStore(cfg.Name, swg)
|
|
wg := v.(*sync.WaitGroup)
|
|
if loaded {
|
|
wg.Wait()
|
|
// This waitgroup is "thrown away" (since there was an existing one).
|
|
swg.Done()
|
|
} else {
|
|
defer func() {
|
|
jsa.inflight.Delete(cfg.Name)
|
|
wg.Done()
|
|
}()
|
|
}
|
|
|
|
// Note that isClustered will be false during recovery, even if we're part of a cluster. It shouldn't be used then.
|
|
js, isClustered := jsa.jetStreamAndClustered()
|
|
jsa.mu.Lock()
|
|
if mset, ok := jsa.streams[cfg.Name]; ok {
|
|
jsa.mu.Unlock()
|
|
// Check to see if configs are same.
|
|
ocfg := mset.config()
|
|
|
|
// set the index name on cfg since it would not contain a value for iname while the return from mset.config() does to ensure the DeepEqual works
|
|
for _, s := range cfg.Sources {
|
|
s.setIndexName()
|
|
}
|
|
|
|
// Hold lock, because we'll be reading from and writing to a shared object.
|
|
js.mu.Lock()
|
|
copyStreamMetadata(cfg, &ocfg)
|
|
deepEqual := reflect.DeepEqual(cfg, &ocfg)
|
|
js.mu.Unlock()
|
|
if deepEqual {
|
|
if sa != nil {
|
|
mset.setStreamAssignment(sa)
|
|
}
|
|
return mset, nil
|
|
} else {
|
|
return nil, ApiErrors[JSStreamNameExistErr]
|
|
}
|
|
}
|
|
jsa.usageMu.RLock()
|
|
selected, tier, hasTier := jsa.selectLimits(cfg.Replicas)
|
|
jsa.usageMu.RUnlock()
|
|
|
|
if !hasTier {
|
|
jsa.mu.Unlock()
|
|
return nil, NewJSNoLimitsError()
|
|
}
|
|
|
|
// Skip if we're recovering.
|
|
if !recovering {
|
|
reserved := int64(0)
|
|
if !isClustered {
|
|
reserved = jsa.tieredReservation(tier, cfg)
|
|
}
|
|
jsa.mu.Unlock()
|
|
js.mu.RLock()
|
|
if isClustered {
|
|
_, reserved = js.tieredStreamAndReservationCount(a.Name, tier, cfg)
|
|
}
|
|
if err := js.checkAllLimits(&selected, cfg, reserved, 0); err != nil {
|
|
js.mu.RUnlock()
|
|
return nil, err
|
|
}
|
|
js.mu.RUnlock()
|
|
jsa.mu.Lock()
|
|
}
|
|
|
|
// If mirror, check if the transforms (if any) are valid.
|
|
if cfg.Mirror != nil {
|
|
if len(cfg.Mirror.SubjectTransforms) == 0 {
|
|
if cfg.Mirror.FilterSubject != _EMPTY_ && !IsValidSubject(cfg.Mirror.FilterSubject) {
|
|
jsa.mu.Unlock()
|
|
return nil, fmt.Errorf("subject filter '%s' for the mirror %w", cfg.Mirror.FilterSubject, ErrBadSubject)
|
|
}
|
|
} else {
|
|
for _, st := range cfg.Mirror.SubjectTransforms {
|
|
if st.Source != _EMPTY_ && !IsValidSubject(st.Source) {
|
|
jsa.mu.Unlock()
|
|
return nil, fmt.Errorf("invalid subject transform source '%s' for the mirror: %w", st.Source, ErrBadSubject)
|
|
}
|
|
// check the transform, if any, is valid
|
|
if st.Destination != _EMPTY_ {
|
|
if _, err = NewSubjectTransform(st.Source, st.Destination); err != nil {
|
|
jsa.mu.Unlock()
|
|
return nil, fmt.Errorf("subject transform from '%s' to '%s' for the mirror: %w", st.Source, st.Destination, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup our internal indexed names here for sources and check if the transforms (if any) are valid.
|
|
for _, ssi := range cfg.Sources {
|
|
if len(ssi.SubjectTransforms) == 0 {
|
|
// check the filter, if any, is valid
|
|
if ssi.FilterSubject != _EMPTY_ && !IsValidSubject(ssi.FilterSubject) {
|
|
jsa.mu.Unlock()
|
|
return nil, fmt.Errorf("subject filter '%s' for the source: %w", ssi.FilterSubject, ErrBadSubject)
|
|
}
|
|
} else {
|
|
for _, st := range ssi.SubjectTransforms {
|
|
if st.Source != _EMPTY_ && !IsValidSubject(st.Source) {
|
|
jsa.mu.Unlock()
|
|
return nil, fmt.Errorf("subject filter '%s' for the source: %w", st.Source, ErrBadSubject)
|
|
}
|
|
// check the transform, if any, is valid
|
|
if st.Destination != _EMPTY_ {
|
|
if _, err = NewSubjectTransform(st.Source, st.Destination); err != nil {
|
|
jsa.mu.Unlock()
|
|
return nil, fmt.Errorf("subject transform from '%s' to '%s' for the source: %w", st.Source, st.Destination, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for overlapping subjects with other streams.
|
|
// These are not allowed for now.
|
|
if jsa.subjectsOverlap(cfg.Subjects, nil) {
|
|
jsa.mu.Unlock()
|
|
return nil, NewJSStreamSubjectOverlapError()
|
|
}
|
|
|
|
// Setup the internal clients.
|
|
c := s.createInternalJetStreamClient()
|
|
ic := s.createInternalJetStreamClient()
|
|
|
|
// Work out the stream ingest limits.
|
|
mlen := s.opts.StreamMaxBufferedMsgs
|
|
msz := uint64(s.opts.StreamMaxBufferedSize)
|
|
if mlen == 0 {
|
|
mlen = streamDefaultMaxQueueMsgs
|
|
}
|
|
if msz == 0 {
|
|
msz = streamDefaultMaxQueueBytes
|
|
}
|
|
|
|
qpfx := fmt.Sprintf("[ACC:%s] stream '%s' ", a.Name, config.Name)
|
|
mset := &stream{
|
|
acc: a,
|
|
jsa: jsa,
|
|
cfg: *cfg,
|
|
js: js,
|
|
srv: s,
|
|
client: c,
|
|
sysc: ic,
|
|
tier: tier,
|
|
stype: cfg.Storage,
|
|
consumers: make(map[string]*consumer),
|
|
msgs: newIPQueue[*inMsg](s, qpfx+"messages",
|
|
ipqSizeCalculation(func(msg *inMsg) uint64 {
|
|
return uint64(len(msg.hdr) + len(msg.msg) + len(msg.rply) + len(msg.subj))
|
|
}),
|
|
ipqLimitByLen[*inMsg](mlen),
|
|
ipqLimitBySize[*inMsg](msz),
|
|
),
|
|
gets: newIPQueue[*directGetReq](s, qpfx+"direct gets"),
|
|
qch: make(chan struct{}),
|
|
mqch: make(chan struct{}),
|
|
uch: make(chan struct{}, 4),
|
|
sch: make(chan struct{}, 1),
|
|
created: time.Now().UTC(),
|
|
}
|
|
|
|
// Add created timestamp used for the store, must match that of the stream assignment if it exists.
|
|
if sa != nil {
|
|
// The following assignment does not require mutex
|
|
// protection: sa.Created is immutable.
|
|
mset.created = sa.Created
|
|
}
|
|
|
|
// Start our signaling routine to process consumers.
|
|
mset.sigq = newIPQueue[*cMsg](s, qpfx+"obs") // of *cMsg
|
|
go mset.signalConsumersLoop()
|
|
|
|
// For no-ack consumers when we are interest retention.
|
|
if cfg.Retention != LimitsPolicy {
|
|
mset.ackq = newIPQueue[uint64](s, qpfx+"acks")
|
|
}
|
|
|
|
// Check for input subject transform
|
|
if cfg.SubjectTransform != nil {
|
|
tr, err := NewSubjectTransform(cfg.SubjectTransform.Source, cfg.SubjectTransform.Destination)
|
|
if err != nil {
|
|
jsa.mu.Unlock()
|
|
return nil, fmt.Errorf("stream subject transform from '%s' to '%s': %w", cfg.SubjectTransform.Source, cfg.SubjectTransform.Destination, err)
|
|
}
|
|
mset.itr = tr
|
|
}
|
|
|
|
// Check for RePublish.
|
|
if cfg.RePublish != nil {
|
|
tr, err := NewSubjectTransform(cfg.RePublish.Source, cfg.RePublish.Destination)
|
|
if err != nil {
|
|
jsa.mu.Unlock()
|
|
return nil, fmt.Errorf("stream republish transform from '%s' to '%s': %w", cfg.RePublish.Source, cfg.RePublish.Destination, err)
|
|
}
|
|
// Assign our transform for republishing.
|
|
mset.tr = tr
|
|
}
|
|
storeDir := filepath.Join(jsa.storeDir, streamsDir, cfg.Name)
|
|
jsa.mu.Unlock()
|
|
|
|
// Bind to the user account.
|
|
c.registerWithAccount(a)
|
|
// Bind to the system account.
|
|
ic.registerWithAccount(s.SystemAccount())
|
|
|
|
// Create the appropriate storage
|
|
fsCfg := fsConfig
|
|
if fsCfg == nil {
|
|
fsCfg = &FileStoreConfig{}
|
|
// If we are file based and not explicitly configured
|
|
// we may be able to auto-tune based on max msgs or bytes.
|
|
if cfg.Storage == FileStorage {
|
|
mset.autoTuneFileStorageBlockSize(fsCfg)
|
|
}
|
|
}
|
|
fsCfg.StoreDir = storeDir
|
|
// Grab configured sync interval.
|
|
fsCfg.SyncInterval = s.getOpts().SyncInterval
|
|
fsCfg.SyncAlways = s.getOpts().SyncAlways
|
|
fsCfg.Compression = config.Compression
|
|
// Async flushing is only allowed if the stream has a sync log backing it.
|
|
fsCfg.AsyncFlush = !fsCfg.SyncAlways && config.Replicas > 1
|
|
|
|
// Async persist mode opts in to async flushing,
|
|
// sync always would also be disabled if it was configured.
|
|
if config.PersistMode == AsyncPersistMode {
|
|
fsCfg.SyncAlways = false
|
|
fsCfg.AsyncFlush = true
|
|
}
|
|
if err := mset.setupStore(fsCfg); err != nil {
|
|
mset.stop(true, false)
|
|
return nil, NewJSStreamStoreFailedError(err)
|
|
}
|
|
|
|
// Create our pubAck template here. Better than json marshal each time on success.
|
|
if domain := s.getOpts().JetStreamDomain; domain != _EMPTY_ {
|
|
mset.pubAck = fmt.Appendf(nil, "{%q:%q,%q:%q,%q:", "stream", cfg.Name, "domain", domain, "seq")
|
|
} else {
|
|
mset.pubAck = fmt.Appendf(nil, "{%q:%q,%q:", "stream", cfg.Name, "seq")
|
|
}
|
|
end := len(mset.pubAck)
|
|
mset.pubAck = mset.pubAck[:end:end]
|
|
|
|
// Set our known last sequence.
|
|
var state StreamState
|
|
mset.store.FastState(&state)
|
|
|
|
// Possible race with consumer.setLeader during recovery.
|
|
mset.mu.Lock()
|
|
mset.lseq = state.LastSeq
|
|
|
|
// Ensure dedupe state is loaded.
|
|
mset.ddMu.Lock()
|
|
mset.rebuildDedupe()
|
|
mset.ddMu.Unlock()
|
|
mset.mu.Unlock()
|
|
|
|
// Set our stream assignment if in clustered mode.
|
|
reserveResources := true
|
|
if sa != nil {
|
|
mset.setStreamAssignment(sa)
|
|
|
|
// If the stream is resetting we must not double-account resources, they were already accounted for.
|
|
js.mu.Lock()
|
|
if sa.resetting {
|
|
reserveResources, sa.resetting = false, false
|
|
}
|
|
js.mu.Unlock()
|
|
}
|
|
|
|
// Setup our internal send go routine.
|
|
mset.setupSendCapabilities()
|
|
|
|
// Reserve resources if MaxBytes present.
|
|
if reserveResources {
|
|
mset.js.reserveStreamResources(&mset.cfg)
|
|
}
|
|
|
|
// Call directly to set leader if not in clustered mode.
|
|
// This can be called though before we actually setup clustering, so check both.
|
|
if singleServerMode {
|
|
if err := mset.setLeader(true); err != nil {
|
|
mset.stop(true, false)
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// This is always true in single server mode.
|
|
if mset.IsLeader() {
|
|
// Send advisory.
|
|
var suppress bool
|
|
if !s.standAloneMode() && sa == nil {
|
|
if cfg.Replicas > 1 {
|
|
suppress = true
|
|
}
|
|
} else if sa != nil {
|
|
suppress = sa.hasResponded()
|
|
}
|
|
if !suppress {
|
|
mset.sendCreateAdvisory()
|
|
}
|
|
}
|
|
|
|
// Register with our account last.
|
|
jsa.mu.Lock()
|
|
jsa.streams[cfg.Name] = mset
|
|
jsa.mu.Unlock()
|
|
|
|
return mset, nil
|
|
}
|
|
|
|
// Composes the index name. Contains the stream name, subject filter, and transform destination
|
|
// when the stream is external we will use the api prefix as part of the index name
|
|
// (as the same stream name could be used in multiple JS domains)
|
|
func (ssi *StreamSource) composeIName() string {
|
|
var iName = ssi.Name
|
|
|
|
if ssi.External != nil {
|
|
iName = iName + ":" + getHash(ssi.External.ApiPrefix)
|
|
}
|
|
if ssi.Consumer != nil {
|
|
iName = iName + ":C=" + getHash(ssi.Consumer.Name)
|
|
}
|
|
|
|
source := ssi.FilterSubject
|
|
destination := fwcs
|
|
|
|
if len(ssi.SubjectTransforms) == 0 {
|
|
// normalize filter and destination in case they are empty
|
|
if source == _EMPTY_ {
|
|
source = fwcs
|
|
}
|
|
if destination == _EMPTY_ {
|
|
destination = fwcs
|
|
}
|
|
} else {
|
|
var sources, destinations []string
|
|
|
|
for _, tr := range ssi.SubjectTransforms {
|
|
trsrc, trdest := tr.Source, tr.Destination
|
|
if trsrc == _EMPTY_ {
|
|
trsrc = fwcs
|
|
}
|
|
if trdest == _EMPTY_ {
|
|
trdest = fwcs
|
|
}
|
|
sources = append(sources, trsrc)
|
|
destinations = append(destinations, trdest)
|
|
}
|
|
source = strings.Join(sources, "\f")
|
|
destination = strings.Join(destinations, "\f")
|
|
}
|
|
|
|
return strings.Join([]string{iName, source, destination}, " ")
|
|
}
|
|
|
|
// Sets the index name.
|
|
func (ssi *StreamSource) setIndexName() {
|
|
ssi.iname = ssi.composeIName()
|
|
}
|
|
|
|
// Composes the consumer index name. Contains the stream name and consumer name used for durable sourcing (if any).
|
|
// When the stream is external we will use the api prefix as part of the index name
|
|
// (as the same stream and consumer names could be used in multiple JS domains)
|
|
func (ssi *StreamSource) composeCName() string {
|
|
var iName = ssi.Name
|
|
|
|
if ssi.External != nil {
|
|
iName = iName + ":" + getHash(ssi.External.ApiPrefix)
|
|
}
|
|
var c string
|
|
if ssi.Consumer != nil {
|
|
c = ssi.Consumer.Name
|
|
}
|
|
|
|
return strings.Join([]string{iName, c}, " ")
|
|
}
|
|
|
|
func (mset *stream) streamAssignment() *streamAssignment {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
return mset.sa
|
|
}
|
|
|
|
func (mset *stream) setStreamAssignment(sa *streamAssignment) {
|
|
var node RaftNode
|
|
var peers []string
|
|
|
|
mset.mu.RLock()
|
|
js := mset.js
|
|
mset.mu.RUnlock()
|
|
|
|
if js != nil {
|
|
js.mu.RLock()
|
|
if sa.Group != nil {
|
|
node = sa.Group.node
|
|
peers = sa.Group.Peers
|
|
}
|
|
js.mu.RUnlock()
|
|
}
|
|
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
|
|
mset.sa = sa
|
|
if sa == nil {
|
|
return
|
|
}
|
|
|
|
// Set our node.
|
|
mset.node = node
|
|
if mset.node != nil {
|
|
mset.node.UpdateKnownPeers(peers)
|
|
}
|
|
|
|
// Setup our info sub here as well for all stream members. This is now by design.
|
|
if mset.infoSub == nil {
|
|
isubj := fmt.Sprintf(clusterStreamInfoT, mset.jsa.acc(), mset.cfg.Name)
|
|
// Note below the way we subscribe here is so that we can send requests to ourselves.
|
|
mset.infoSub, _ = mset.srv.systemSubscribe(isubj, _EMPTY_, false, mset.sysc, mset.handleClusterStreamInfoRequest)
|
|
}
|
|
|
|
// Trigger update chan.
|
|
select {
|
|
case mset.uch <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (mset *stream) monitorQuitC() <-chan struct{} {
|
|
if mset == nil {
|
|
return nil
|
|
}
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
// Recreate if a prior monitor routine was stopped.
|
|
if mset.mqch == nil {
|
|
mset.mqch = make(chan struct{})
|
|
}
|
|
return mset.mqch
|
|
}
|
|
|
|
// signalMonitorQuit signals to exit the monitor loop. If there's no Raft node,
|
|
// this will be the only way to stop the monitor goroutine.
|
|
func (mset *stream) signalMonitorQuit() {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
if mset.mqch != nil {
|
|
close(mset.mqch)
|
|
mset.mqch = nil
|
|
}
|
|
}
|
|
|
|
func (mset *stream) updateC() <-chan struct{} {
|
|
if mset == nil {
|
|
return nil
|
|
}
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
return mset.uch
|
|
}
|
|
|
|
// IsLeader will return if we are the current leader.
|
|
func (mset *stream) IsLeader() bool {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
return mset.isLeader()
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) isLeader() bool {
|
|
if mset.isClustered() {
|
|
return mset.node.Leader()
|
|
}
|
|
return true
|
|
}
|
|
|
|
// isLeaderNodeState should NOT be used normally, use isLeader instead.
|
|
// Returns whether the node thinks it is the leader, regardless of whether applies are up-to-date yet
|
|
// (unlike isLeader, which requires applies to be caught up).
|
|
// May be used to respond to clients after a leader change, when applying entries from a former leader.
|
|
// Lock should be held.
|
|
func (mset *stream) isLeaderNodeState() bool {
|
|
if mset.isClustered() {
|
|
return mset.node.State() == Leader
|
|
}
|
|
return true
|
|
}
|
|
|
|
// TODO(dlc) - Check to see if we can accept being the leader or we should step down.
|
|
func (mset *stream) setLeader(isLeader bool) error {
|
|
mset.mu.Lock()
|
|
// If we are here we have a change in leader status.
|
|
if isLeader {
|
|
// Make sure we are listening for sync requests.
|
|
// TODO(dlc) - Original design was that all in sync members of the group would do DQ.
|
|
if mset.isClustered() {
|
|
mset.startClusterSubs()
|
|
}
|
|
|
|
// Setup subscriptions if we were not already the leader.
|
|
if err := mset.subscribeToStream(); err != nil {
|
|
if mset.isClustered() {
|
|
// Stepdown since we have an error.
|
|
mset.node.StepDown()
|
|
}
|
|
mset.mu.Unlock()
|
|
return err
|
|
}
|
|
|
|
// Reset any inflight fast batches. We were likely a follower before and need
|
|
// to send an ack to the publishers so they know we're still there.
|
|
if mset.batches != nil {
|
|
mset.batches.mu.Lock()
|
|
for batchId, b := range mset.batches.fast {
|
|
mset.batches.fastBatchReset(mset, batchId, b)
|
|
}
|
|
mset.batches.mu.Unlock()
|
|
}
|
|
} else {
|
|
// cancel timer to create the source consumers if not fired yet
|
|
if mset.sourcesConsumerSetup != nil {
|
|
mset.sourcesConsumerSetup.Stop()
|
|
mset.sourcesConsumerSetup = nil
|
|
} else {
|
|
// Stop any source consumers
|
|
mset.stopSourceConsumers()
|
|
}
|
|
|
|
// Stop responding to sync requests.
|
|
mset.stopClusterSubs()
|
|
// Unsubscribe from direct stream.
|
|
mset.unsubscribeToStream(false, false)
|
|
// Clear catchup state
|
|
mset.clearAllCatchupPeers()
|
|
}
|
|
mset.store.ResetState()
|
|
mset.mu.Unlock()
|
|
|
|
// If we are interest based make sure to check consumers.
|
|
// This is to make sure we process any outstanding acks.
|
|
mset.checkInterestState()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) startClusterSubs() {
|
|
if mset.syncSub == nil {
|
|
mset.syncSub, _ = mset.srv.systemSubscribe(mset.sa.Sync, _EMPTY_, false, mset.sysc, mset.handleClusterSyncRequest)
|
|
}
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) stopClusterSubs() {
|
|
if mset.syncSub != nil {
|
|
mset.srv.sysUnsubscribe(mset.syncSub)
|
|
mset.syncSub = nil
|
|
}
|
|
}
|
|
|
|
// account gets the account for this stream.
|
|
func (mset *stream) account() *Account {
|
|
return mset.accountLocked(true)
|
|
}
|
|
|
|
func (mset *stream) accountLocked(needLock bool) *Account {
|
|
if needLock {
|
|
mset.mu.RLock()
|
|
}
|
|
jsa := mset.jsa
|
|
if needLock {
|
|
mset.mu.RUnlock()
|
|
}
|
|
if jsa == nil {
|
|
return nil
|
|
}
|
|
return jsa.acc()
|
|
}
|
|
|
|
// Helper to determine the max msg size for this stream if file based.
|
|
func (mset *stream) maxMsgSize() uint64 {
|
|
maxMsgSize := mset.cfg.MaxMsgSize
|
|
if maxMsgSize <= 0 {
|
|
// Pull from the account.
|
|
if mset.jsa != nil {
|
|
if acc := mset.jsa.acc(); acc != nil {
|
|
acc.mu.RLock()
|
|
maxMsgSize = acc.mpay
|
|
acc.mu.RUnlock()
|
|
}
|
|
}
|
|
// If all else fails use default.
|
|
if maxMsgSize <= 0 {
|
|
maxMsgSize = MAX_PAYLOAD_SIZE
|
|
}
|
|
}
|
|
// Now determine an estimation for the subjects etc.
|
|
maxSubject := -1
|
|
for _, subj := range mset.cfg.Subjects {
|
|
if subjectIsLiteral(subj) {
|
|
if len(subj) > maxSubject {
|
|
maxSubject = len(subj)
|
|
}
|
|
}
|
|
}
|
|
if maxSubject < 0 {
|
|
const defaultMaxSubject = 256
|
|
maxSubject = defaultMaxSubject
|
|
}
|
|
// filestore will add in estimates for record headers, etc.
|
|
return fileStoreMsgSizeEstimate(maxSubject, int(maxMsgSize))
|
|
}
|
|
|
|
// If we are file based and the file storage config was not explicitly set
|
|
// we can autotune block sizes to better match. Our target will be to store 125%
|
|
// of the theoretical limit. We will round up to nearest 100 bytes as well.
|
|
func (mset *stream) autoTuneFileStorageBlockSize(fsCfg *FileStoreConfig) {
|
|
var totalEstSize uint64
|
|
|
|
// MaxBytes will take precedence for now.
|
|
if mset.cfg.MaxBytes > 0 {
|
|
totalEstSize = uint64(mset.cfg.MaxBytes)
|
|
} else if mset.cfg.MaxMsgs > 0 {
|
|
// Determine max message size to estimate.
|
|
totalEstSize = mset.maxMsgSize() * uint64(mset.cfg.MaxMsgs)
|
|
} else if mset.cfg.MaxMsgsPer > 0 {
|
|
fsCfg.BlockSize = uint64(defaultKVBlockSize)
|
|
return
|
|
} else {
|
|
// If nothing set will let underlying filestore determine blkSize.
|
|
return
|
|
}
|
|
|
|
blkSize := (totalEstSize / 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
|
|
}
|
|
fsCfg.BlockSize = uint64(blkSize)
|
|
}
|
|
|
|
// rebuildDedupe will rebuild any dedupe structures needed after recovery of a stream.
|
|
// Will be called lazily to avoid penalizing startup times.
|
|
// TODO(dlc) - Might be good to know if this should be checked at all for streams with no
|
|
// headers and msgId in them. Would need signaling from the storage layer.
|
|
// mset.mu and mset.ddMu locks should be held.
|
|
func (mset *stream) rebuildDedupe() {
|
|
duplicates := mset.cfg.Duplicates
|
|
if duplicates <= 0 {
|
|
return
|
|
}
|
|
|
|
// We have some messages. Lookup starting sequence by duplicate time window.
|
|
sseq := mset.store.GetSeqFromTime(time.Now().Add(-duplicates))
|
|
if sseq == 0 {
|
|
return
|
|
}
|
|
|
|
var smv StoreMsg
|
|
var state StreamState
|
|
mset.store.FastState(&state)
|
|
|
|
for seq := sseq; seq <= state.LastSeq; seq++ {
|
|
sm, err := mset.store.LoadMsg(seq, &smv)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
var msgId string
|
|
if len(sm.hdr) > 0 {
|
|
if msgId = getMsgId(sm.hdr); msgId != _EMPTY_ {
|
|
mset.storeMsgIdLocked(&ddentry{msgId, sm.seq, sm.ts})
|
|
}
|
|
}
|
|
if seq == state.LastSeq {
|
|
mset.lmsgId = msgId
|
|
}
|
|
}
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) lastSeqAndCLFS() (uint64, uint64) {
|
|
return mset.lseq, mset.getCLFS()
|
|
}
|
|
|
|
func (mset *stream) getCLFS() uint64 {
|
|
if mset == nil {
|
|
return 0
|
|
}
|
|
mset.clMu.Lock()
|
|
defer mset.clMu.Unlock()
|
|
return mset.clfs
|
|
}
|
|
|
|
func (mset *stream) setCLFS(clfs uint64) {
|
|
mset.clMu.Lock()
|
|
mset.clfs = clfs
|
|
mset.clMu.Unlock()
|
|
}
|
|
|
|
func (mset *stream) lastSeq() uint64 {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
return mset.lseq
|
|
}
|
|
|
|
func (mset *stream) sendCreateAdvisory() {
|
|
mset.mu.RLock()
|
|
name := mset.cfg.Name
|
|
outq := mset.outq
|
|
srv := mset.srv
|
|
mset.mu.RUnlock()
|
|
|
|
if outq == nil {
|
|
return
|
|
}
|
|
|
|
// finally send an event that this stream was created
|
|
m := JSStreamActionAdvisory{
|
|
TypedEvent: TypedEvent{
|
|
Type: JSStreamActionAdvisoryType,
|
|
ID: nuid.Next(),
|
|
Time: time.Now().UTC(),
|
|
},
|
|
Stream: name,
|
|
Action: CreateEvent,
|
|
Domain: srv.getOpts().JetStreamDomain,
|
|
}
|
|
|
|
j, err := json.Marshal(m)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
subj := JSAdvisoryStreamCreatedPre + "." + name
|
|
outq.sendMsg(subj, j)
|
|
}
|
|
|
|
func (mset *stream) sendDeleteAdvisoryLocked() {
|
|
if mset.outq == nil {
|
|
return
|
|
}
|
|
|
|
m := JSStreamActionAdvisory{
|
|
TypedEvent: TypedEvent{
|
|
Type: JSStreamActionAdvisoryType,
|
|
ID: nuid.Next(),
|
|
Time: time.Now().UTC(),
|
|
},
|
|
Stream: mset.cfg.Name,
|
|
Action: DeleteEvent,
|
|
Domain: mset.srv.getOpts().JetStreamDomain,
|
|
}
|
|
|
|
j, err := json.Marshal(m)
|
|
if err == nil {
|
|
subj := JSAdvisoryStreamDeletedPre + "." + mset.cfg.Name
|
|
mset.outq.sendMsg(subj, j)
|
|
}
|
|
}
|
|
|
|
func (mset *stream) sendUpdateAdvisoryLocked() {
|
|
if mset.outq == nil {
|
|
return
|
|
}
|
|
|
|
m := JSStreamActionAdvisory{
|
|
TypedEvent: TypedEvent{
|
|
Type: JSStreamActionAdvisoryType,
|
|
ID: nuid.Next(),
|
|
Time: time.Now().UTC(),
|
|
},
|
|
Stream: mset.cfg.Name,
|
|
Action: ModifyEvent,
|
|
Domain: mset.srv.getOpts().JetStreamDomain,
|
|
}
|
|
|
|
j, err := json.Marshal(m)
|
|
if err == nil {
|
|
subj := JSAdvisoryStreamUpdatedPre + "." + mset.cfg.Name
|
|
mset.outq.sendMsg(subj, j)
|
|
}
|
|
}
|
|
|
|
func (mset *stream) sendStreamBatchAbandonedAdvisory(batchId string, reason BatchAbandonReason) {
|
|
if mset == nil {
|
|
return
|
|
}
|
|
s := mset.srv
|
|
stream, acc := mset.name(), mset.account()
|
|
subj := JSAdvisoryStreamBatchAbandonedPre + "." + stream
|
|
adv := &JSStreamBatchAbandonedAdvisory{
|
|
TypedEvent: TypedEvent{
|
|
Type: JSStreamBatchAbandonedAdvisoryType,
|
|
ID: nuid.Next(),
|
|
Time: time.Now().UTC(),
|
|
},
|
|
Stream: stream,
|
|
Domain: s.getOpts().JetStreamDomain,
|
|
BatchId: batchId,
|
|
Reason: reason,
|
|
}
|
|
|
|
// Send to the user's account if not the system account.
|
|
if acc != s.SystemAccount() {
|
|
s.publishAdvisory(acc, subj, adv)
|
|
}
|
|
// Now do system level one. Place account info in adv, and nil account means system.
|
|
adv.Account = acc.GetName()
|
|
s.publishAdvisory(nil, subj, adv)
|
|
}
|
|
|
|
// Created returns created time.
|
|
func (mset *stream) createdTime() time.Time {
|
|
mset.mu.RLock()
|
|
created := mset.created
|
|
mset.mu.RUnlock()
|
|
return created
|
|
}
|
|
|
|
// Internal to allow creation time to be restored.
|
|
func (mset *stream) setCreatedTime(created time.Time) {
|
|
mset.mu.Lock()
|
|
mset.created = created
|
|
mset.mu.Unlock()
|
|
}
|
|
|
|
// subjectsOverlap to see if these subjects overlap with existing subjects.
|
|
// Use only for non-clustered JetStream
|
|
// RLock minimum should be held.
|
|
func (jsa *jsAccount) subjectsOverlap(subjects []string, self *stream) bool {
|
|
for _, mset := range jsa.streams {
|
|
if self != nil && mset == self {
|
|
continue
|
|
}
|
|
for _, subj := range mset.cfg.Subjects {
|
|
for _, tsubj := range subjects {
|
|
if SubjectsCollide(tsubj, subj) {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// StreamDefaultDuplicatesWindow default duplicates window.
|
|
const StreamDefaultDuplicatesWindow = 2 * time.Minute
|
|
|
|
func (s *Server) checkStreamCfg(config *StreamConfig, acc *Account, pedantic bool) (StreamConfig, *ApiError) {
|
|
lim := &s.getOpts().JetStreamLimits
|
|
|
|
if config == nil {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration invalid"))
|
|
}
|
|
if !isValidAssetName(config.Name) {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("stream name is required and can not contain '.', '*', '>', '\\', '/'"))
|
|
}
|
|
if len(config.Name) > JSMaxNameLen {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("stream name is too long, maximum allowed is %d", JSMaxNameLen))
|
|
}
|
|
if len(config.Description) > JSMaxDescriptionLen {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("stream description is too long, maximum allowed is %d", JSMaxDescriptionLen))
|
|
}
|
|
|
|
var metadataLen int
|
|
for k, v := range config.Metadata {
|
|
metadataLen += len(k) + len(v)
|
|
}
|
|
if metadataLen > JSMaxMetadataLen {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("stream metadata exceeds maximum size of %d bytes", JSMaxMetadataLen))
|
|
}
|
|
|
|
cfg := *config
|
|
|
|
if _, err := cfg.Retention.MarshalJSON(); err != nil {
|
|
return cfg, NewJSStreamInvalidConfigError(fmt.Errorf("invalid retention"))
|
|
}
|
|
if _, err := cfg.Discard.MarshalJSON(); err != nil {
|
|
return cfg, NewJSStreamInvalidConfigError(fmt.Errorf("invalid discard policy"))
|
|
}
|
|
if _, err := cfg.Compression.MarshalJSON(); err != nil {
|
|
return cfg, NewJSStreamInvalidConfigError(fmt.Errorf("invalid compression"))
|
|
}
|
|
|
|
// Make file the default.
|
|
if cfg.Storage == 0 {
|
|
cfg.Storage = FileStorage
|
|
}
|
|
if _, err := cfg.Storage.MarshalJSON(); err != nil {
|
|
return cfg, NewJSStreamInvalidConfigError(fmt.Errorf("invalid storage type"))
|
|
}
|
|
|
|
if cfg.Replicas == 0 {
|
|
cfg.Replicas = 1
|
|
}
|
|
if cfg.Replicas > StreamMaxReplicas {
|
|
return cfg, NewJSStreamInvalidConfigError(fmt.Errorf("maximum replicas is %d", StreamMaxReplicas))
|
|
}
|
|
if cfg.Replicas < 0 {
|
|
return cfg, NewJSReplicasCountCannotBeNegativeError()
|
|
}
|
|
if cfg.MaxMsgs == 0 || cfg.MaxMsgs < -1 {
|
|
if pedantic && cfg.MaxMsgs < -1 {
|
|
return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_msgs must be set to -1"))
|
|
}
|
|
cfg.MaxMsgs = -1
|
|
}
|
|
if cfg.MaxMsgsPer == 0 || cfg.MaxMsgsPer < -1 {
|
|
if pedantic && cfg.MaxMsgsPer < -1 {
|
|
return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_msgs_per_subject must be set to -1"))
|
|
}
|
|
cfg.MaxMsgsPer = -1
|
|
}
|
|
if cfg.MaxBytes == 0 || cfg.MaxBytes < -1 {
|
|
if pedantic && cfg.MaxBytes < -1 {
|
|
return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_bytes must be set to -1"))
|
|
}
|
|
cfg.MaxBytes = -1
|
|
}
|
|
if cfg.MaxMsgSize == 0 || cfg.MaxMsgSize < -1 {
|
|
if pedantic && cfg.MaxMsgSize < -1 {
|
|
return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_msg_size must be set to -1"))
|
|
}
|
|
cfg.MaxMsgSize = -1
|
|
}
|
|
if cfg.MaxConsumers == 0 || cfg.MaxConsumers < -1 {
|
|
if pedantic && cfg.MaxConsumers < -1 {
|
|
return StreamConfig{}, NewJSPedanticError(fmt.Errorf("max_consumers must be set to -1"))
|
|
}
|
|
cfg.MaxConsumers = -1
|
|
}
|
|
if cfg.MaxAge < 0 {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("max age can not be negative"))
|
|
}
|
|
if cfg.MaxAge != 0 && cfg.MaxAge < 100*time.Millisecond {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("max age needs to be >= 100ms"))
|
|
}
|
|
|
|
if cfg.Duplicates == 0 && cfg.Mirror == nil && len(cfg.Sources) == 0 {
|
|
maxWindow := StreamDefaultDuplicatesWindow
|
|
if lim.Duplicates > 0 && maxWindow > lim.Duplicates {
|
|
if pedantic {
|
|
return StreamConfig{}, NewJSPedanticError(fmt.Errorf("duplicate window limits are higher than current limits"))
|
|
}
|
|
maxWindow = lim.Duplicates
|
|
}
|
|
if cfg.MaxAge != 0 && cfg.MaxAge < maxWindow {
|
|
if pedantic {
|
|
return StreamConfig{}, NewJSPedanticError(fmt.Errorf("duplicate window cannot be bigger than max age"))
|
|
}
|
|
cfg.Duplicates = cfg.MaxAge
|
|
} else {
|
|
cfg.Duplicates = maxWindow
|
|
}
|
|
}
|
|
if cfg.Duplicates < 0 {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("duplicates window can not be negative"))
|
|
}
|
|
// Check that duplicates is not larger then age if set.
|
|
if cfg.MaxAge != 0 && cfg.Duplicates > cfg.MaxAge {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("duplicates window can not be larger then max age"))
|
|
}
|
|
if lim.Duplicates > 0 && cfg.Duplicates > lim.Duplicates {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("duplicates window can not be larger then server limit of %v",
|
|
lim.Duplicates.String()))
|
|
}
|
|
if cfg.Duplicates > 0 && cfg.Duplicates < 100*time.Millisecond {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("duplicates window needs to be >= 100ms"))
|
|
}
|
|
|
|
if cfg.DenyPurge && cfg.AllowRollup {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("roll-ups require the purge permission"))
|
|
}
|
|
|
|
// Counter is not compatible with some settings.
|
|
if cfg.AllowMsgCounter {
|
|
if cfg.Discard == DiscardNew {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream cannot use discard new"))
|
|
}
|
|
if cfg.AllowMsgTTL {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream cannot use message TTLs"))
|
|
}
|
|
if cfg.Retention != LimitsPolicy {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("counter stream can only use limits retention"))
|
|
}
|
|
}
|
|
|
|
// Check for new discard new per subject, we require the discard policy to also be new.
|
|
if cfg.DiscardNewPer {
|
|
if cfg.Discard != DiscardNew {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("discard new per subject requires discard new policy to be set"))
|
|
}
|
|
if cfg.MaxMsgsPer <= 0 {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("discard new per subject requires max msgs per subject > 0"))
|
|
}
|
|
}
|
|
|
|
if cfg.SubjectDeleteMarkerTTL > 0 {
|
|
if cfg.SubjectDeleteMarkerTTL < time.Second {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("subject delete marker TTL must be at least 1 second"))
|
|
}
|
|
if !cfg.AllowMsgTTL {
|
|
if pedantic {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("subject delete marker cannot be set if message TTLs are disabled"))
|
|
}
|
|
cfg.AllowMsgTTL = true
|
|
}
|
|
if !cfg.AllowRollup {
|
|
if pedantic {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("subject delete marker cannot be set if roll-ups are disabled"))
|
|
}
|
|
cfg.AllowRollup, cfg.DenyPurge = true, false
|
|
}
|
|
} else if cfg.SubjectDeleteMarkerTTL < 0 {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("subject delete marker TTL must not be negative"))
|
|
}
|
|
|
|
if cfg.AllowMsgSchedules {
|
|
if !cfg.AllowRollup {
|
|
if pedantic {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("message scheduling cannot be set if roll-ups are disabled"))
|
|
}
|
|
cfg.AllowRollup, cfg.DenyPurge = true, false
|
|
}
|
|
}
|
|
|
|
if cfg.PersistMode == AsyncPersistMode {
|
|
if cfg.Storage != FileStorage {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("async persist mode is only supported on file storage"))
|
|
}
|
|
if cfg.Replicas > 1 {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("async persist mode is not supported on replicated streams"))
|
|
}
|
|
if cfg.AllowAtomicPublish {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("async persist mode is not supported with atomic batch publish"))
|
|
}
|
|
}
|
|
|
|
getStream := func(streamName string) (bool, StreamConfig) {
|
|
var exists bool
|
|
var cfg StreamConfig
|
|
if s.JetStreamIsClustered() {
|
|
if js, _ := s.getJetStreamCluster(); js != nil {
|
|
js.mu.RLock()
|
|
if sa := js.streamAssignment(acc.Name, streamName); sa != nil {
|
|
cfg = *sa.Config.clone()
|
|
exists = true
|
|
}
|
|
js.mu.RUnlock()
|
|
}
|
|
} else if mset, err := acc.lookupStream(streamName); err == nil {
|
|
cfg = mset.cfg
|
|
exists = true
|
|
}
|
|
return exists, cfg
|
|
}
|
|
|
|
hasStream := func(streamName string) (bool, int32, []string) {
|
|
exists, cfg := getStream(streamName)
|
|
return exists, cfg.MaxMsgSize, cfg.Subjects
|
|
}
|
|
|
|
var streamSubs []string
|
|
var deliveryPrefixes []string
|
|
var apiPrefixes []string
|
|
|
|
// Do some pre-checking for mirror config to avoid cycles in clustered mode.
|
|
if cfg.Mirror != nil {
|
|
if cfg.FirstSeq > 0 {
|
|
return StreamConfig{}, NewJSMirrorWithFirstSeqError()
|
|
}
|
|
if len(cfg.Subjects) > 0 {
|
|
return StreamConfig{}, NewJSMirrorWithSubjectsError()
|
|
}
|
|
if len(cfg.Sources) > 0 {
|
|
return StreamConfig{}, NewJSMirrorWithSourcesError()
|
|
}
|
|
if cfg.AllowMsgCounter {
|
|
return StreamConfig{}, NewJSMirrorWithCountersError()
|
|
}
|
|
if cfg.AllowAtomicPublish {
|
|
return StreamConfig{}, NewJSMirrorWithAtomicPublishError()
|
|
}
|
|
if cfg.AllowBatchPublish {
|
|
return StreamConfig{}, NewJSMirrorWithBatchPublishError()
|
|
}
|
|
if cfg.AllowMsgSchedules {
|
|
return StreamConfig{}, NewJSMirrorWithMsgSchedulesError()
|
|
}
|
|
if c := cfg.Mirror.Consumer; c != nil {
|
|
if !isValidAssetName(c.Name) {
|
|
return StreamConfig{}, NewJSMirrorDurableConsumerCfgInvalidError()
|
|
}
|
|
if !subjectIsLiteral(c.DeliverSubject) || !IsValidSubject(c.DeliverSubject) {
|
|
return StreamConfig{}, NewJSMirrorDurableConsumerCfgInvalidError()
|
|
}
|
|
if cfg.Mirror.OptStartSeq != 0 || cfg.Mirror.OptStartTime != nil {
|
|
return StreamConfig{}, NewJSMirrorDurableConsumerCfgInvalidError()
|
|
}
|
|
if cfg.Mirror.FilterSubject != _EMPTY_ {
|
|
return StreamConfig{}, NewJSMirrorDurableConsumerCfgInvalidError()
|
|
}
|
|
}
|
|
if cfg.Mirror.FilterSubject != _EMPTY_ && len(cfg.Mirror.SubjectTransforms) != 0 {
|
|
return StreamConfig{}, NewJSMirrorMultipleFiltersNotAllowedError()
|
|
}
|
|
if cfg.SubjectDeleteMarkerTTL > 0 {
|
|
// Delete markers cannot be configured on a mirror as it would result in new
|
|
// tombstones which would use up sequence numbers, diverging from the origin
|
|
// stream.
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("subject delete markers forbidden on mirrors"))
|
|
}
|
|
// Check subject filters overlap.
|
|
for outer, tr := range cfg.Mirror.SubjectTransforms {
|
|
if tr.Source != _EMPTY_ && !IsValidSubject(tr.Source) {
|
|
return StreamConfig{}, NewJSMirrorInvalidSubjectFilterError(fmt.Errorf("%w %s", ErrBadSubject, tr.Source))
|
|
}
|
|
|
|
err := ValidateMapping(tr.Source, tr.Destination)
|
|
if err != nil {
|
|
return StreamConfig{}, NewJSMirrorInvalidTransformDestinationError(err)
|
|
}
|
|
|
|
for inner, innertr := range cfg.Mirror.SubjectTransforms {
|
|
if inner != outer && SubjectsCollide(tr.Source, innertr.Source) {
|
|
return StreamConfig{}, NewJSMirrorOverlappingSubjectFiltersError()
|
|
}
|
|
}
|
|
}
|
|
// Do not perform checks if External is provided, as it could lead to
|
|
// checking against itself (if sourced stream name is the same on different JetStream)
|
|
if cfg.Mirror.External == nil {
|
|
if !isValidAssetName(cfg.Mirror.Name) {
|
|
return StreamConfig{}, NewJSMirrorInvalidStreamNameError()
|
|
}
|
|
// We do not require other stream to exist anymore, but if we can see it check payloads.
|
|
exists, maxMsgSize, subs := hasStream(cfg.Mirror.Name)
|
|
if len(subs) > 0 {
|
|
streamSubs = append(streamSubs, subs...)
|
|
}
|
|
if exists {
|
|
if cfg.MaxMsgSize > 0 && maxMsgSize > 0 && cfg.MaxMsgSize < maxMsgSize {
|
|
return StreamConfig{}, NewJSMirrorMaxMessageSizeTooBigError()
|
|
}
|
|
}
|
|
// Determine if we are inheriting direct gets.
|
|
if exists, ocfg := getStream(cfg.Mirror.Name); exists {
|
|
if pedantic && cfg.MirrorDirect != ocfg.AllowDirect {
|
|
return StreamConfig{}, NewJSPedanticError(fmt.Errorf("origin stream has direct get set, mirror has it disabled"))
|
|
}
|
|
cfg.MirrorDirect = ocfg.AllowDirect
|
|
} else if js := s.getJetStream(); js != nil && js.isClustered() {
|
|
// Could not find it here. If we are clustered we can look it up.
|
|
js.mu.RLock()
|
|
if cc := js.cluster; cc != nil {
|
|
if as := cc.streams[acc.Name]; as != nil {
|
|
if sa := as[cfg.Mirror.Name]; sa != nil {
|
|
if pedantic && cfg.MirrorDirect != sa.Config.AllowDirect {
|
|
js.mu.RUnlock()
|
|
return StreamConfig{}, NewJSPedanticError(fmt.Errorf("origin stream has direct get set, mirror has it disabled"))
|
|
}
|
|
cfg.MirrorDirect = sa.Config.AllowDirect
|
|
}
|
|
}
|
|
}
|
|
js.mu.RUnlock()
|
|
}
|
|
} else {
|
|
if cfg.Mirror.External.DeliverPrefix != _EMPTY_ {
|
|
deliveryPrefixes = append(deliveryPrefixes, cfg.Mirror.External.DeliverPrefix)
|
|
}
|
|
|
|
if cfg.Mirror.External.ApiPrefix != _EMPTY_ {
|
|
apiPrefixes = append(apiPrefixes, cfg.Mirror.External.ApiPrefix)
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(cfg.Sources) > 0 {
|
|
if cfg.AllowMsgSchedules {
|
|
return StreamConfig{}, NewJSSourceWithMsgSchedulesError()
|
|
}
|
|
}
|
|
|
|
// check sources for duplicates
|
|
var iNames = make(map[string]struct{})
|
|
var cNames = make(map[string]struct{})
|
|
for _, src := range cfg.Sources {
|
|
if src == nil || !isValidAssetName(src.Name) {
|
|
return StreamConfig{}, NewJSSourceInvalidStreamNameError()
|
|
}
|
|
if _, ok := iNames[src.composeIName()]; !ok {
|
|
iNames[src.composeIName()] = struct{}{}
|
|
} else {
|
|
return StreamConfig{}, NewJSSourceDuplicateDetectedError()
|
|
}
|
|
|
|
if src.FilterSubject != _EMPTY_ && len(src.SubjectTransforms) != 0 {
|
|
return StreamConfig{}, NewJSSourceMultipleFiltersNotAllowedError()
|
|
}
|
|
|
|
for _, tr := range src.SubjectTransforms {
|
|
if tr.Source != _EMPTY_ && !IsValidSubject(tr.Source) {
|
|
return StreamConfig{}, NewJSSourceInvalidSubjectFilterError(fmt.Errorf("%w %s", ErrBadSubject, tr.Source))
|
|
}
|
|
err := ValidateMapping(tr.Source, tr.Destination)
|
|
if err != nil {
|
|
return StreamConfig{}, NewJSSourceInvalidTransformDestinationError(err)
|
|
}
|
|
}
|
|
|
|
// Check subject filters overlap.
|
|
for outer, tr := range src.SubjectTransforms {
|
|
for inner, innertr := range src.SubjectTransforms {
|
|
if inner != outer && subjectIsSubsetMatch(tr.Source, innertr.Source) {
|
|
return StreamConfig{}, NewJSSourceOverlappingSubjectFiltersError()
|
|
}
|
|
}
|
|
}
|
|
|
|
if c := src.Consumer; c != nil {
|
|
if !isValidAssetName(c.Name) {
|
|
return StreamConfig{}, NewJSSourceDurableConsumerCfgInvalidError()
|
|
}
|
|
if !subjectIsLiteral(c.DeliverSubject) || !IsValidSubject(c.DeliverSubject) {
|
|
return StreamConfig{}, NewJSSourceDurableConsumerCfgInvalidError()
|
|
}
|
|
if src.OptStartSeq != 0 || src.OptStartTime != nil {
|
|
return StreamConfig{}, NewJSSourceDurableConsumerCfgInvalidError()
|
|
}
|
|
if src.FilterSubject != _EMPTY_ {
|
|
return StreamConfig{}, NewJSSourceDurableConsumerCfgInvalidError()
|
|
}
|
|
// Reusing the same consumer for multiple sources of the same stream isn't allowed.
|
|
if _, ok := cNames[src.composeCName()]; !ok {
|
|
cNames[src.composeCName()] = struct{}{}
|
|
} else {
|
|
return StreamConfig{}, NewJSSourceDurableConsumerDuplicateDetectedError()
|
|
}
|
|
}
|
|
|
|
// Do not perform checks if External is provided, as it could lead to
|
|
// checking against itself (if sourced stream name is the same on different JetStream)
|
|
if src.External == nil {
|
|
exists, maxMsgSize, subs := hasStream(src.Name)
|
|
if len(subs) > 0 {
|
|
streamSubs = append(streamSubs, subs...)
|
|
}
|
|
if exists {
|
|
if cfg.MaxMsgSize > 0 && maxMsgSize > 0 && cfg.MaxMsgSize < maxMsgSize {
|
|
return StreamConfig{}, NewJSSourceMaxMessageSizeTooBigError()
|
|
}
|
|
}
|
|
continue
|
|
} else {
|
|
if src.External.DeliverPrefix != _EMPTY_ {
|
|
deliveryPrefixes = append(deliveryPrefixes, src.External.DeliverPrefix)
|
|
}
|
|
if src.External.ApiPrefix != _EMPTY_ {
|
|
apiPrefixes = append(apiPrefixes, src.External.ApiPrefix)
|
|
}
|
|
}
|
|
}
|
|
|
|
// check prefix overlap with subjects
|
|
for _, pfx := range deliveryPrefixes {
|
|
if !IsValidPublishSubject(pfx) {
|
|
return StreamConfig{}, NewJSStreamInvalidExternalDeliverySubjError(pfx)
|
|
}
|
|
for _, sub := range streamSubs {
|
|
if SubjectsCollide(sub, fmt.Sprintf("%s.%s", pfx, sub)) {
|
|
return StreamConfig{}, NewJSStreamExternalDelPrefixOverlapsError(pfx, sub)
|
|
}
|
|
}
|
|
}
|
|
// check if api prefixes overlap
|
|
for _, apiPfx := range apiPrefixes {
|
|
if !IsValidPublishSubject(apiPfx) {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(
|
|
fmt.Errorf("stream external api prefix %q must be a valid subject without wildcards", apiPfx))
|
|
}
|
|
if SubjectsCollide(apiPfx, JSApiPrefix) {
|
|
return StreamConfig{}, NewJSStreamExternalApiOverlapError(apiPfx, JSApiPrefix)
|
|
}
|
|
}
|
|
|
|
// cycle check for source cycle
|
|
toVisit := []*StreamConfig{&cfg}
|
|
visited := make(map[string]struct{})
|
|
overlaps := func(subjects []string, filter string) bool {
|
|
if filter == _EMPTY_ {
|
|
return true
|
|
}
|
|
for _, subject := range subjects {
|
|
if SubjectsCollide(subject, filter) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
for len(toVisit) > 0 {
|
|
cfg := toVisit[0]
|
|
toVisit = toVisit[1:]
|
|
visited[cfg.Name] = struct{}{}
|
|
for _, src := range cfg.Sources {
|
|
if src.External != nil {
|
|
continue
|
|
}
|
|
// We can detect a cycle between streams, but let's double check that the
|
|
// subjects actually form a cycle.
|
|
if _, ok := visited[src.Name]; ok {
|
|
if overlaps(cfg.Subjects, src.FilterSubject) {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(errors.New("detected cycle"))
|
|
}
|
|
} else if exists, cfg := getStream(src.Name); exists {
|
|
toVisit = append(toVisit, &cfg)
|
|
}
|
|
}
|
|
// Avoid cycles hiding behind mirrors
|
|
if m := cfg.Mirror; m != nil {
|
|
if m.External == nil {
|
|
if _, ok := visited[m.Name]; ok {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(errors.New("detected cycle"))
|
|
}
|
|
if exists, cfg := getStream(m.Name); exists {
|
|
toVisit = append(toVisit, &cfg)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(cfg.Subjects) == 0 {
|
|
if cfg.Mirror == nil && len(cfg.Sources) == 0 {
|
|
cfg.Subjects = append(cfg.Subjects, cfg.Name)
|
|
}
|
|
} else {
|
|
if cfg.Mirror != nil {
|
|
return StreamConfig{}, NewJSMirrorWithSubjectsError()
|
|
}
|
|
|
|
// Check for literal duplication of subject interest in config
|
|
// and no overlap with any JS or SYS API subject space.
|
|
dset := make(map[string]struct{}, len(cfg.Subjects))
|
|
for i, subj := range cfg.Subjects {
|
|
// Make sure the subject is valid. Check this first.
|
|
if !IsValidSubject(subj) {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("invalid subject"))
|
|
}
|
|
if _, ok := dset[subj]; ok {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("duplicate subjects detected"))
|
|
}
|
|
// Check for trying to capture everything.
|
|
if subj == fwcs {
|
|
if !cfg.NoAck {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("capturing all subjects requires no-ack to be true"))
|
|
}
|
|
// Capturing everything also will require R1.
|
|
if cfg.Replicas != 1 {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("capturing all subjects requires replicas of 1"))
|
|
}
|
|
}
|
|
// Also check to make sure we do not overlap with our $JS API subjects.
|
|
if !cfg.NoAck {
|
|
for _, namespace := range []string{"$JS.>", "$JSC.>", "$NRG.>"} {
|
|
if SubjectsCollide(subj, namespace) {
|
|
// We allow an exception for $JS.EVENT.> since these could have been created in the past.
|
|
if !subjectIsSubsetMatch(subj, "$JS.EVENT.>") {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("subjects that overlap with jetstream api require no-ack to be true"))
|
|
}
|
|
}
|
|
}
|
|
if SubjectsCollide(subj, "$SYS.>") {
|
|
if !subjectIsSubsetMatch(subj, "$SYS.ACCOUNT.>") {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("subjects that overlap with system api require no-ack to be true"))
|
|
}
|
|
}
|
|
}
|
|
// Now check if we have multiple subjects that we do not overlap ourselves
|
|
// which would cause duplicate entries (assuming no MsgID).
|
|
for _, tsubj := range cfg.Subjects[i+1:] {
|
|
if SubjectsCollide(tsubj, subj) {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("subject %q overlaps with %q", subj, tsubj))
|
|
}
|
|
}
|
|
// Mark for duplicate check.
|
|
dset[subj] = struct{}{}
|
|
}
|
|
}
|
|
|
|
if len(cfg.Subjects) == 0 && len(cfg.Sources) == 0 && cfg.Mirror == nil {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(
|
|
fmt.Errorf("stream needs at least one configured subject or be a source/mirror"))
|
|
}
|
|
|
|
// Check for MaxBytes required and it's limit
|
|
if required, limit := acc.maxBytesLimits(&cfg); required && cfg.MaxBytes <= 0 {
|
|
return StreamConfig{}, NewJSStreamMaxBytesRequiredError()
|
|
} else if limit > 0 && cfg.MaxBytes > limit {
|
|
return StreamConfig{}, NewJSStreamMaxStreamBytesExceededError()
|
|
}
|
|
|
|
// Check the subject transform if any
|
|
if cfg.SubjectTransform != nil {
|
|
if cfg.SubjectTransform.Source != _EMPTY_ && !IsValidSubject(cfg.SubjectTransform.Source) {
|
|
return StreamConfig{}, NewJSStreamTransformInvalidSourceError(fmt.Errorf("%w %s", ErrBadSubject, cfg.SubjectTransform.Source))
|
|
}
|
|
|
|
err := ValidateMapping(cfg.SubjectTransform.Source, cfg.SubjectTransform.Destination)
|
|
if err != nil {
|
|
return StreamConfig{}, NewJSStreamTransformInvalidDestinationError(err)
|
|
}
|
|
}
|
|
|
|
// If we have a republish directive check if we can create a transform here.
|
|
if cfg.RePublish != nil {
|
|
// Check to make sure source is a valid subset of the subjects we have.
|
|
// Also make sure it does not form a cycle.
|
|
// Empty same as all.
|
|
if cfg.RePublish.Source == _EMPTY_ {
|
|
if pedantic {
|
|
return StreamConfig{}, NewJSPedanticError(fmt.Errorf("republish source can not be empty"))
|
|
}
|
|
cfg.RePublish.Source = fwcs
|
|
}
|
|
// A RePublish from '>' to '>' could be used, normally this would form a cycle with the stream subjects.
|
|
// But if this aligns to a different subject based on the transform, we allow it still.
|
|
// The RePublish will be implicit based on the transform, but only if the transform's source
|
|
// is the only stream subject.
|
|
if cfg.RePublish.Destination == fwcs && cfg.RePublish.Source == fwcs && cfg.SubjectTransform != nil &&
|
|
len(cfg.Subjects) == 1 && cfg.SubjectTransform.Source == cfg.Subjects[0] {
|
|
if pedantic {
|
|
return StreamConfig{}, NewJSPedanticError(fmt.Errorf("implicit republish based on subject transform"))
|
|
}
|
|
// RePublish all messages with the transformed subject.
|
|
cfg.RePublish.Source, cfg.RePublish.Destination = cfg.SubjectTransform.Destination, cfg.SubjectTransform.Destination
|
|
}
|
|
var formsCycle bool
|
|
for _, subj := range cfg.Subjects {
|
|
if SubjectsCollide(cfg.RePublish.Destination, subj) {
|
|
formsCycle = true
|
|
break
|
|
}
|
|
}
|
|
if formsCycle {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration for republish destination forms a cycle"))
|
|
}
|
|
if _, err := NewSubjectTransform(cfg.RePublish.Source, cfg.RePublish.Destination); err != nil {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration for republish with transform from '%s' to '%s' not valid", cfg.RePublish.Source, cfg.RePublish.Destination))
|
|
}
|
|
}
|
|
|
|
// Remove placement if it's an empty object.
|
|
if cfg.Placement != nil && reflect.DeepEqual(cfg.Placement, &Placement{}) {
|
|
cfg.Placement = nil
|
|
}
|
|
// For now don't allow preferred server in placement.
|
|
if cfg.Placement != nil && cfg.Placement.Preferred != _EMPTY_ {
|
|
return StreamConfig{}, NewJSStreamInvalidConfigError(fmt.Errorf("preferred server not permitted in placement"))
|
|
}
|
|
|
|
return cfg, nil
|
|
}
|
|
|
|
// Config returns the stream's configuration.
|
|
func (mset *stream) config() StreamConfig {
|
|
mset.cfgMu.RLock()
|
|
defer mset.cfgMu.RUnlock()
|
|
return mset.cfg
|
|
}
|
|
|
|
func (mset *stream) fileStoreConfig() (FileStoreConfig, error) {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
fs, ok := mset.store.(*fileStore)
|
|
if !ok {
|
|
return FileStoreConfig{}, ErrStoreWrongType
|
|
}
|
|
return fs.fileStoreConfig(), nil
|
|
}
|
|
|
|
// Do not hold jsAccount or jetStream lock
|
|
func (jsa *jsAccount) configUpdateCheck(old, new *StreamConfig, s *Server, pedantic bool) (*StreamConfig, error) {
|
|
cfg, apiErr := s.checkStreamCfg(new, jsa.acc(), pedantic)
|
|
if apiErr != nil {
|
|
return nil, apiErr
|
|
}
|
|
|
|
// Name must match.
|
|
if cfg.Name != old.Name {
|
|
return nil, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration name must match original"))
|
|
}
|
|
// Can't change storage types.
|
|
if cfg.Storage != old.Storage {
|
|
return nil, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration update can not change storage type"))
|
|
}
|
|
// Can only change retention from limits to interest or back, not to/from work queue for now.
|
|
if cfg.Retention != old.Retention {
|
|
if old.Retention == WorkQueuePolicy || cfg.Retention == WorkQueuePolicy {
|
|
return nil, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration update can not change retention policy to/from workqueue"))
|
|
}
|
|
}
|
|
// Can not change from true to false.
|
|
if !cfg.Sealed && old.Sealed {
|
|
return nil, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration update can not unseal a sealed stream"))
|
|
}
|
|
// Can not change from true to false.
|
|
if !cfg.DenyDelete && old.DenyDelete {
|
|
return nil, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration update can not cancel deny message deletes"))
|
|
}
|
|
// Can not change from true to false.
|
|
if !cfg.DenyPurge && old.DenyPurge {
|
|
return nil, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration update can not cancel deny purge"))
|
|
}
|
|
// Check for mirror changes which are not allowed.
|
|
// We will allow removing the mirror config to "promote" the mirror to a normal stream.
|
|
if cfg.Mirror != nil && !reflect.DeepEqual(cfg.Mirror, old.Mirror) {
|
|
return nil, NewJSStreamMirrorNotUpdatableError()
|
|
}
|
|
|
|
// Check on new discard new per subject.
|
|
if cfg.DiscardNewPer {
|
|
if cfg.Discard != DiscardNew {
|
|
return nil, NewJSStreamInvalidConfigError(fmt.Errorf("discard new per subject requires discard new policy to be set"))
|
|
}
|
|
if cfg.MaxMsgsPer <= 0 {
|
|
return nil, NewJSStreamInvalidConfigError(fmt.Errorf("discard new per subject requires max msgs per subject > 0"))
|
|
}
|
|
}
|
|
|
|
// Check on the allowed message TTL status.
|
|
if old.AllowMsgTTL && !cfg.AllowMsgTTL {
|
|
return nil, NewJSStreamInvalidConfigError(fmt.Errorf("message TTL status can not be disabled"))
|
|
}
|
|
|
|
// Can't change counter setting.
|
|
if cfg.AllowMsgCounter != old.AllowMsgCounter {
|
|
return nil, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration update can not change message counter setting"))
|
|
}
|
|
|
|
// Can't disable message schedules setting.
|
|
if old.AllowMsgSchedules && !cfg.AllowMsgSchedules {
|
|
return nil, NewJSStreamInvalidConfigError(fmt.Errorf("message schedules can not be disabled"))
|
|
}
|
|
|
|
if old.PersistMode != cfg.PersistMode {
|
|
return nil, NewJSStreamInvalidConfigError(fmt.Errorf("stream configuration update can not change persist mode"))
|
|
}
|
|
|
|
// Do some adjustments for being sealed.
|
|
// Pedantic mode will allow those changes to be made, as they are deterministic and important to get a sealed stream.
|
|
if cfg.Sealed {
|
|
cfg.MaxAge = 0
|
|
cfg.Discard = DiscardNew
|
|
cfg.DenyDelete, cfg.DenyPurge = true, true
|
|
cfg.AllowRollup = false
|
|
}
|
|
|
|
// Check limits. We need some extra handling to allow updating MaxBytes.
|
|
|
|
// First, let's calculate the difference between the new and old MaxBytes.
|
|
maxBytesDiff := max(cfg.MaxBytes, 0) - max(old.MaxBytes, 0)
|
|
if maxBytesDiff < 0 {
|
|
// If we're updating to a lower MaxBytes (maxBytesDiff is negative),
|
|
// then set to zero so checkBytesLimits doesn't set addBytes to 1.
|
|
maxBytesDiff = 0
|
|
}
|
|
// If maxBytesDiff == 0, then that means MaxBytes didn't change.
|
|
// If maxBytesDiff > 0, then we want to reserve additional bytes.
|
|
|
|
// Save the user configured MaxBytes.
|
|
newMaxBytes := cfg.MaxBytes
|
|
maxBytesOffset := int64(0)
|
|
|
|
// We temporarily set cfg.MaxBytes to maxBytesDiff because checkAllLimits
|
|
// adds cfg.MaxBytes to the current reserved limit and checks if we've gone
|
|
// over. However, we don't want an addition cfg.MaxBytes, we only want to
|
|
// reserve the difference between the new and the old values.
|
|
cfg.MaxBytes = maxBytesDiff
|
|
|
|
// Check limits.
|
|
js, isClustered := jsa.jetStreamAndClustered()
|
|
jsa.mu.RLock()
|
|
acc := jsa.account
|
|
jsa.usageMu.RLock()
|
|
selected, tier, hasTier := jsa.selectLimits(cfg.Replicas)
|
|
if !hasTier && old.Replicas != cfg.Replicas {
|
|
selected, tier, hasTier = jsa.selectLimits(old.Replicas)
|
|
}
|
|
jsa.usageMu.RUnlock()
|
|
reserved := int64(0)
|
|
if !isClustered {
|
|
reserved = jsa.tieredReservation(tier, &cfg)
|
|
}
|
|
jsa.mu.RUnlock()
|
|
if !hasTier {
|
|
return nil, NewJSNoLimitsError()
|
|
}
|
|
js.mu.RLock()
|
|
defer js.mu.RUnlock()
|
|
if isClustered {
|
|
_, reserved = js.tieredStreamAndReservationCount(acc.Name, tier, &cfg)
|
|
}
|
|
// reservation does not account for this stream, hence add the old value
|
|
if old.MaxBytes > 0 {
|
|
if tier == _EMPTY_ && old.Replicas > 1 {
|
|
reserved = addSaturate(reserved, mulSaturate(int64(old.Replicas), old.MaxBytes))
|
|
} else {
|
|
reserved = addSaturate(reserved, old.MaxBytes)
|
|
}
|
|
}
|
|
if err := js.checkAllLimits(&selected, &cfg, reserved, maxBytesOffset); err != nil {
|
|
return nil, err
|
|
}
|
|
// Restore the user configured MaxBytes.
|
|
cfg.MaxBytes = newMaxBytes
|
|
return &cfg, nil
|
|
}
|
|
|
|
// Update will allow certain configuration properties of an existing stream to be updated.
|
|
func (mset *stream) update(config *StreamConfig) error {
|
|
return mset.updateWithAdvisory(config, true, false)
|
|
}
|
|
|
|
func (mset *stream) updatePedantic(config *StreamConfig, pedantic bool) error {
|
|
return mset.updateWithAdvisory(config, true, pedantic)
|
|
}
|
|
|
|
// Update will allow certain configuration properties of an existing stream to be updated.
|
|
func (mset *stream) updateWithAdvisory(config *StreamConfig, sendAdvisory bool, pedantic bool) error {
|
|
_, jsa, err := mset.acc.checkForJetStream()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
mset.mu.RLock()
|
|
ocfg := mset.cfg
|
|
s := mset.srv
|
|
mset.mu.RUnlock()
|
|
|
|
cfg, err := mset.jsa.configUpdateCheck(&ocfg, config, s, pedantic)
|
|
if err != nil {
|
|
return NewJSStreamInvalidConfigError(err, Unless(err))
|
|
}
|
|
|
|
// In the event that some of the stream-level limits have changed, yell appropriately
|
|
// if any of the consumers exceed that limit.
|
|
updateLimits := ocfg.ConsumerLimits.InactiveThreshold != cfg.ConsumerLimits.InactiveThreshold ||
|
|
ocfg.ConsumerLimits.MaxAckPending != cfg.ConsumerLimits.MaxAckPending
|
|
if updateLimits {
|
|
var errorConsumers []string
|
|
consumers := map[string]*ConsumerConfig{}
|
|
if mset.js.isClustered() {
|
|
for _, c := range mset.sa.consumers {
|
|
consumers[c.Name] = c.Config
|
|
}
|
|
} else {
|
|
for _, c := range mset.consumers {
|
|
consumers[c.name] = &c.cfg
|
|
}
|
|
}
|
|
for name, ccfg := range consumers {
|
|
if ccfg.InactiveThreshold > cfg.ConsumerLimits.InactiveThreshold ||
|
|
ccfg.MaxAckPending > cfg.ConsumerLimits.MaxAckPending {
|
|
errorConsumers = append(errorConsumers, name)
|
|
}
|
|
}
|
|
if len(errorConsumers) > 0 {
|
|
// TODO(nat): Return a parsable error so that we can surface something
|
|
// sensible through the JS API.
|
|
return fmt.Errorf("change to limits violates consumers: %s", strings.Join(errorConsumers, ", "))
|
|
}
|
|
}
|
|
|
|
jsa.mu.RLock()
|
|
if jsa.subjectsOverlap(cfg.Subjects, mset) {
|
|
jsa.mu.RUnlock()
|
|
return NewJSStreamSubjectOverlapError()
|
|
}
|
|
jsa.mu.RUnlock()
|
|
|
|
mset.mu.Lock()
|
|
if mset.active {
|
|
// Check for mirror promotion.
|
|
if ocfg.Mirror != nil && cfg.Mirror == nil {
|
|
// Only try deleting the sourcing consumer if one wasn't provided to us.
|
|
if ocfg.Mirror.Consumer == nil {
|
|
mset.tryDeleteMirrorConsumer(ocfg.Mirror)
|
|
}
|
|
mset.cancelMirrorConsumer()
|
|
mset.mirror = nil
|
|
}
|
|
|
|
// Now check for subject interest differences.
|
|
current := make(map[string]struct{}, len(ocfg.Subjects))
|
|
for _, s := range ocfg.Subjects {
|
|
current[s] = struct{}{}
|
|
}
|
|
// Update config with new values. The store update will enforce any stricter limits.
|
|
|
|
// Now walk new subjects. All of these need to be added, but we will check
|
|
// the originals first, since if it is in there we can skip, already added.
|
|
for _, s := range cfg.Subjects {
|
|
if _, ok := current[s]; !ok {
|
|
if _, err := mset.subscribeInternal(s, mset.processInboundJetStreamMsg); err != nil {
|
|
mset.mu.Unlock()
|
|
return err
|
|
}
|
|
}
|
|
delete(current, s)
|
|
}
|
|
// What is left in current needs to be deleted.
|
|
for s := range current {
|
|
if err := mset.unsubscribeInternal(s); err != nil {
|
|
mset.mu.Unlock()
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Check for the Duplicates
|
|
mset.ddMu.Lock()
|
|
if cfg.Duplicates != ocfg.Duplicates && mset.ddtmr != nil {
|
|
// Let it fire right away, it will adjust properly on purge.
|
|
mset.ddtmr.Reset(time.Microsecond)
|
|
}
|
|
mset.ddMu.Unlock()
|
|
|
|
// Check for Sources.
|
|
if len(cfg.Sources) > 0 || len(ocfg.Sources) > 0 {
|
|
currentIName := make(map[string]struct{})
|
|
currentConsumers := make(map[string]*StreamSource)
|
|
needsStartingSeqNum := make(map[string]struct{})
|
|
|
|
getSourcingConsumerIName := func(ssi *StreamSource, sources []*StreamSource) string {
|
|
var iName = ssi.Name
|
|
if ssi.External != nil {
|
|
iName = iName + ":" + getHash(ssi.External.ApiPrefix)
|
|
}
|
|
return fmt.Sprintf("%s %s", iName, mset.createSourcingConsumerHash(ssi, sources))
|
|
}
|
|
for _, s := range ocfg.Sources {
|
|
currentIName[s.iname] = struct{}{}
|
|
// Only track the sourcing consumer for deletion if one wasn't provided to us.
|
|
if s.Consumer == nil {
|
|
currentConsumers[getSourcingConsumerIName(s, ocfg.Sources)] = s
|
|
}
|
|
}
|
|
for _, s := range cfg.Sources {
|
|
s.setIndexName()
|
|
if _, ok := currentIName[s.iname]; !ok {
|
|
// new source
|
|
if mset.sources == nil {
|
|
mset.sources = make(map[string]*sourceInfo)
|
|
}
|
|
mset.cfg.Sources = append(mset.cfg.Sources, s)
|
|
|
|
var si *sourceInfo
|
|
|
|
if len(s.SubjectTransforms) == 0 {
|
|
si = &sourceInfo{name: s.Name, iname: s.iname, sf: s.FilterSubject}
|
|
} else {
|
|
si = &sourceInfo{name: s.Name, iname: s.iname}
|
|
si.trs = make([]*subjectTransform, len(s.SubjectTransforms))
|
|
si.sfs = make([]string, len(s.SubjectTransforms))
|
|
for i := range s.SubjectTransforms {
|
|
// err can be ignored as already validated in config check
|
|
si.sfs[i] = s.SubjectTransforms[i].Source
|
|
var err error
|
|
si.trs[i], err = NewSubjectTransform(s.SubjectTransforms[i].Source, s.SubjectTransforms[i].Destination)
|
|
if err != nil {
|
|
mset.mu.Unlock()
|
|
return fmt.Errorf("unable to get subject transform for source: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
mset.sources[s.iname] = si
|
|
needsStartingSeqNum[s.iname] = struct{}{}
|
|
} else {
|
|
// source already exists
|
|
delete(currentIName, s.iname)
|
|
}
|
|
|
|
// Remove the source if it still exists, but only if not using a pre-existing consumer.
|
|
if s.Consumer == nil {
|
|
delete(currentConsumers, getSourcingConsumerIName(s, cfg.Sources))
|
|
}
|
|
}
|
|
// Delete source consumers if any aren't used anymore.
|
|
for _, s := range currentConsumers {
|
|
id := mset.createSourcingConsumerHash(s, ocfg.Sources)
|
|
mset.tryDeleteSourceConsumer(id, s)
|
|
}
|
|
// What is left in currentIName needs to be deleted.
|
|
for iName := range currentIName {
|
|
mset.cancelSourceConsumer(iName)
|
|
delete(mset.sources, iName)
|
|
}
|
|
neededCopy := make(map[string]struct{}, len(needsStartingSeqNum))
|
|
for iName := range needsStartingSeqNum {
|
|
neededCopy[iName] = struct{}{}
|
|
}
|
|
mset.setStartingSequenceForSources(needsStartingSeqNum)
|
|
for iName := range neededCopy {
|
|
mset.setupSourceConsumer(iName, mset.sources[iName].sseq+1, time.Time{})
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check for a change in allow direct/mirror status.
|
|
// These will run on all members, so just update as appropriate here.
|
|
// We do make sure we are caught up under monitorStream() during initial startup.
|
|
if cfg.AllowDirect != ocfg.AllowDirect {
|
|
if cfg.AllowDirect {
|
|
mset.subscribeToDirect()
|
|
} else {
|
|
mset.unsubscribeToDirect()
|
|
}
|
|
}
|
|
if cfg.MirrorDirect != ocfg.MirrorDirect {
|
|
if cfg.MirrorDirect {
|
|
mset.subscribeToMirrorDirect()
|
|
} else {
|
|
mset.unsubscribeToMirrorDirect()
|
|
}
|
|
}
|
|
|
|
// Check for changes to RePublish.
|
|
if cfg.RePublish != nil {
|
|
// Empty same as all.
|
|
if cfg.RePublish.Source == _EMPTY_ {
|
|
cfg.RePublish.Source = fwcs
|
|
}
|
|
if cfg.RePublish.Destination == _EMPTY_ {
|
|
cfg.RePublish.Destination = fwcs
|
|
}
|
|
tr, err := NewSubjectTransform(cfg.RePublish.Source, cfg.RePublish.Destination)
|
|
if err != nil {
|
|
mset.mu.Unlock()
|
|
return fmt.Errorf("stream configuration for republish from '%s' to '%s': %w", cfg.RePublish.Source, cfg.RePublish.Destination, err)
|
|
}
|
|
// Assign our transform for republishing.
|
|
mset.tr = tr
|
|
} else {
|
|
mset.tr = nil
|
|
}
|
|
|
|
// Check for changes to subject transform
|
|
if ocfg.SubjectTransform == nil && cfg.SubjectTransform != nil {
|
|
tr, err := NewSubjectTransform(cfg.SubjectTransform.Source, cfg.SubjectTransform.Destination)
|
|
if err != nil {
|
|
mset.mu.Unlock()
|
|
return fmt.Errorf("stream configuration for subject transform from '%s' to '%s': %w", cfg.SubjectTransform.Source, cfg.SubjectTransform.Destination, err)
|
|
}
|
|
mset.itr = tr
|
|
} else if ocfg.SubjectTransform != nil && cfg.SubjectTransform != nil &&
|
|
(ocfg.SubjectTransform.Source != cfg.SubjectTransform.Source || ocfg.SubjectTransform.Destination != cfg.SubjectTransform.Destination) {
|
|
tr, err := NewSubjectTransform(cfg.SubjectTransform.Source, cfg.SubjectTransform.Destination)
|
|
if err != nil {
|
|
mset.mu.Unlock()
|
|
return fmt.Errorf("stream configuration for subject transform from '%s' to '%s': %w", cfg.SubjectTransform.Source, cfg.SubjectTransform.Destination, err)
|
|
}
|
|
mset.itr = tr
|
|
} else if ocfg.SubjectTransform != nil && cfg.SubjectTransform == nil {
|
|
mset.itr = nil
|
|
}
|
|
|
|
js := mset.js
|
|
|
|
if targetTier := tierName(cfg.Replicas); mset.tier != targetTier {
|
|
// In cases such as R1->R3, only one update is needed
|
|
jsa.usageMu.RLock()
|
|
_, ok := jsa.limits[targetTier]
|
|
jsa.usageMu.RUnlock()
|
|
if ok {
|
|
// error never set
|
|
_, reported, _ := mset.store.Utilization()
|
|
jsa.updateUsage(mset.tier, mset.stype, -int64(reported))
|
|
jsa.updateUsage(targetTier, mset.stype, int64(reported))
|
|
mset.tier = targetTier
|
|
}
|
|
// else in case the new tier does not exist (say on move), keep the old tier around
|
|
// a subsequent update to an existing tier will then move from existing past tier to existing new tier
|
|
}
|
|
|
|
if mset.isLeader() && mset.sa != nil && ocfg.Retention != cfg.Retention && cfg.Retention == InterestPolicy {
|
|
// Before we can update the retention policy for the consumer, we need
|
|
// the replica count of all consumers to match the stream.
|
|
for _, c := range mset.sa.consumers {
|
|
if c.Config.Replicas > 0 && c.Config.Replicas != cfg.Replicas {
|
|
mset.mu.Unlock()
|
|
return fmt.Errorf("consumer %q replica count must be %d", c.Name, cfg.Replicas)
|
|
}
|
|
}
|
|
}
|
|
|
|
// If atomic publish is disabled, delete any in-progress batches.
|
|
if !cfg.AllowAtomicPublish {
|
|
mset.deleteAtomicBatches(false)
|
|
mset.deleteBatchApplyState()
|
|
}
|
|
// If fast batch publish is disabled, delete any in-progress batches.
|
|
if !cfg.AllowBatchPublish {
|
|
mset.deleteFastBatches()
|
|
}
|
|
if !cfg.AllowAtomicPublish && !cfg.AllowBatchPublish {
|
|
mset.batches = nil
|
|
}
|
|
|
|
// Now update config and store's version of our config.
|
|
// Although we are under the stream write lock, we will also assign the new
|
|
// configuration under mset.cfgMu lock. This is so that in places where
|
|
// mset.mu cannot be acquired (like many cases in consumer.go where code
|
|
// is under the consumer's lock), and the stream's configuration needs to
|
|
// be inspected, one can use mset.cfgMu's read lock to do that safely.
|
|
mset.cfgMu.Lock()
|
|
mset.cfg = *cfg
|
|
mset.cfgMu.Unlock()
|
|
|
|
// If we're changing retention and haven't errored because of consumer
|
|
// replicas by now, whip through and update the consumer retention.
|
|
if ocfg.Retention != cfg.Retention {
|
|
toUpdate := make([]*consumer, 0, len(mset.consumers))
|
|
for _, c := range mset.consumers {
|
|
toUpdate = append(toUpdate, c)
|
|
}
|
|
var ss StreamState
|
|
mset.store.FastState(&ss)
|
|
mset.mu.Unlock()
|
|
for _, c := range toUpdate {
|
|
c.mu.Lock()
|
|
c.retention = cfg.Retention
|
|
c.mu.Unlock()
|
|
if c.retention == InterestPolicy {
|
|
// If we're switching to interest, force a check of the
|
|
// interest of existing stream messages.
|
|
c.checkStateForInterestStream(&ss)
|
|
}
|
|
}
|
|
mset.mu.Lock()
|
|
}
|
|
|
|
// If we are the leader never suppress update advisory, simply send.
|
|
if mset.isLeader() && sendAdvisory {
|
|
mset.sendUpdateAdvisoryLocked()
|
|
}
|
|
mset.mu.Unlock()
|
|
|
|
if js != nil {
|
|
maxBytesDiff := max(cfg.MaxBytes, 0) - max(ocfg.MaxBytes, 0)
|
|
if maxBytesDiff > 0 {
|
|
// Reserve the difference
|
|
js.reserveStreamResources(&StreamConfig{
|
|
MaxBytes: maxBytesDiff,
|
|
Storage: cfg.Storage,
|
|
})
|
|
} else if maxBytesDiff < 0 {
|
|
// Release the difference
|
|
js.releaseStreamResources(&StreamConfig{
|
|
MaxBytes: -maxBytesDiff,
|
|
Storage: ocfg.Storage,
|
|
})
|
|
}
|
|
}
|
|
|
|
mset.store.UpdateConfig(cfg)
|
|
|
|
return nil
|
|
}
|
|
|
|
// tryDeleteMirrorConsumer is a best-effort single try to delete a consumer used for stream mirroring.
|
|
// Lock should be held.
|
|
func (mset *stream) tryDeleteMirrorConsumer(mirror *StreamSource) {
|
|
id := mset.createStableConsumerHash()
|
|
consumerName := fmt.Sprintf("JS_MIRROR_%s", id)
|
|
log := mset.mirror != nil && mset.mirror.cname == consumerName
|
|
mset.tryDeleteSourcingConsumer("mirror", mirror, consumerName, log)
|
|
}
|
|
|
|
// tryDeleteSourceConsumer is a best-effort single try to delete a consumer used for stream sourcing.
|
|
// Lock should be held.
|
|
func (mset *stream) tryDeleteSourceConsumer(id string, source *StreamSource) {
|
|
consumerName := fmt.Sprintf("JS_SRC_%s", id)
|
|
si := mset.sources[source.iname]
|
|
log := si != nil && si.cname == consumerName
|
|
mset.tryDeleteSourcingConsumer("source", source, consumerName, log)
|
|
}
|
|
|
|
// tryDeleteSourcingConsumer is a best-effort single try to delete a sourcing consumer.
|
|
// Lock should be held.
|
|
func (mset *stream) tryDeleteSourcingConsumer(kind string, source *StreamSource, consumerName string, log bool) {
|
|
acc := mset.acc
|
|
accName, streamName, sourceName := acc.Name, mset.cfg.Name, source.Name
|
|
subject := fmt.Sprintf(JSApiConsumerDeleteT, sourceName, consumerName)
|
|
if source.External != nil {
|
|
subject = strings.Replace(subject, JSApiPrefix, source.External.ApiPrefix, 1)
|
|
subject = strings.ReplaceAll(subject, "..", ".")
|
|
}
|
|
s := mset.srv
|
|
go func() {
|
|
warn := func(err error) {
|
|
if log {
|
|
s.Warnf("Cleanup of %s consumer '%s > %s' failed for stream '%s > %s': %v", kind, sourceName, consumerName, accName, streamName, err)
|
|
}
|
|
}
|
|
|
|
respCh := make(chan *JSApiConsumerDeleteResponse, 1)
|
|
reply := infoReplySubject()
|
|
cdSub, err := acc.subscribeInternal(reply, func(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
|
|
_, msg := c.msgParts(rmsg)
|
|
|
|
var cdr JSApiConsumerDeleteResponse
|
|
if err := json.Unmarshal(msg, &cdr); err != nil {
|
|
warn(err)
|
|
return
|
|
}
|
|
select {
|
|
case respCh <- &cdr:
|
|
default:
|
|
}
|
|
})
|
|
if err != nil {
|
|
warn(err)
|
|
return
|
|
}
|
|
defer acc.unsubscribeInternal(cdSub)
|
|
|
|
// Send the delete request.
|
|
err = s.sendInternalAccountMsgWithReply(acc, subject, reply, nil, nil, false)
|
|
if err != nil {
|
|
warn(err)
|
|
return
|
|
}
|
|
select {
|
|
case cdr := <-respCh:
|
|
if cdr.Error != nil {
|
|
warn(cdr.Error)
|
|
}
|
|
case <-time.After(sourceHealthCheckInterval):
|
|
warn(errors.New("timed out"))
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Small helper to return the Name field from mset.cfg, protected by
|
|
// the mset.cfgMu mutex. This is simply because we have several places
|
|
// in consumer.go where we need it.
|
|
func (mset *stream) getCfgName() string {
|
|
mset.cfgMu.RLock()
|
|
defer mset.cfgMu.RUnlock()
|
|
return mset.cfg.Name
|
|
}
|
|
|
|
// Purge will remove all messages from the stream and underlying store based on the request.
|
|
func (mset *stream) purge(preq *JSApiStreamPurgeRequest) (purged uint64, err error) {
|
|
return mset.purgeLocked(preq, true)
|
|
}
|
|
|
|
func (mset *stream) purgeLocked(preq *JSApiStreamPurgeRequest, needLock bool) (purged uint64, err error) {
|
|
if needLock {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
}
|
|
if mset.closed.Load() {
|
|
return 0, errStreamClosed
|
|
}
|
|
if mset.cfg.Sealed {
|
|
return 0, errors.New("sealed stream")
|
|
}
|
|
store, mlseq := mset.store, mset.lseq
|
|
|
|
if preq != nil {
|
|
purged, err = mset.store.PurgeEx(preq.Subject, preq.Sequence, preq.Keep)
|
|
} else {
|
|
purged, err = mset.store.Purge()
|
|
}
|
|
if err != nil {
|
|
return purged, err
|
|
}
|
|
|
|
// Grab our stream state.
|
|
var state StreamState
|
|
store.FastState(&state)
|
|
fseq, lseq := state.FirstSeq, state.LastSeq
|
|
|
|
// Check if our last has moved past what our original last sequence was, if so reset.
|
|
if lseq > mlseq {
|
|
mset.lseq = lseq
|
|
}
|
|
|
|
// Clear any pending acks below first seq.
|
|
mset.clearAllPreAcksBelowFloor(fseq)
|
|
|
|
// Purge consumers.
|
|
// Check for filtered purge.
|
|
if preq != nil && preq.Subject != _EMPTY_ {
|
|
ss, err := store.FilteredState(fseq, preq.Subject)
|
|
if err != nil {
|
|
return purged, err
|
|
}
|
|
fseq = ss.First
|
|
}
|
|
|
|
// Take a copy of cList to avoid o.purge() potentially taking the stream lock and
|
|
// violating the lock ordering.
|
|
mset.clsMu.RLock()
|
|
cList := slices.Clone(mset.cList)
|
|
mset.clsMu.RUnlock()
|
|
for _, o := range cList {
|
|
start := fseq
|
|
o.mu.RLock()
|
|
// we update consumer sequences if:
|
|
// no subject was specified, we can purge all consumers sequences
|
|
doPurge := preq == nil ||
|
|
preq.Subject == _EMPTY_ ||
|
|
// consumer filter subject is equal to purged subject
|
|
// or consumer filter subject is subset of purged subject,
|
|
// but not the other way around.
|
|
o.isEqualOrSubsetMatch(preq.Subject)
|
|
// Check if a consumer has a wider subject space than what we purged
|
|
var isWider bool
|
|
if !doPurge && preq != nil && o.isFilteredMatch(preq.Subject) {
|
|
doPurge, isWider = true, true
|
|
start = state.FirstSeq
|
|
}
|
|
o.mu.RUnlock()
|
|
if doPurge {
|
|
o.purge(start, lseq, isWider)
|
|
}
|
|
}
|
|
|
|
return purged, nil
|
|
}
|
|
|
|
// RemoveMsg will remove a message from a stream.
|
|
// FIXME(dlc) - Should pick one and be consistent.
|
|
func (mset *stream) removeMsg(seq uint64) (bool, error) {
|
|
return mset.deleteMsg(seq)
|
|
}
|
|
|
|
// DeleteMsg will remove a message from a stream.
|
|
func (mset *stream) deleteMsg(seq uint64) (bool, error) {
|
|
if mset.closed.Load() {
|
|
return false, errStreamClosed
|
|
}
|
|
removed, err := mset.store.RemoveMsg(seq)
|
|
if err != nil {
|
|
return removed, err
|
|
}
|
|
mset.mu.Lock()
|
|
mset.clearAllPreAcks(seq)
|
|
mset.mu.Unlock()
|
|
return removed, err
|
|
}
|
|
|
|
// EraseMsg will securely remove a message and rewrite the data with random data.
|
|
func (mset *stream) eraseMsg(seq uint64) (bool, error) {
|
|
if mset.closed.Load() {
|
|
return false, errStreamClosed
|
|
}
|
|
removed, err := mset.store.EraseMsg(seq)
|
|
if err != nil {
|
|
return removed, err
|
|
}
|
|
mset.mu.Lock()
|
|
mset.clearAllPreAcks(seq)
|
|
mset.mu.Unlock()
|
|
return removed, err
|
|
}
|
|
|
|
// Are we a mirror?
|
|
func (mset *stream) isMirror() bool {
|
|
mset.cfgMu.RLock()
|
|
defer mset.cfgMu.RUnlock()
|
|
return mset.cfg.Mirror != nil
|
|
}
|
|
|
|
func (mset *stream) sourcesInfo() (sis []*StreamSourceInfo) {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
sis = make([]*StreamSourceInfo, 0, len(mset.sources))
|
|
for _, si := range mset.sources {
|
|
sis = append(sis, mset.sourceInfo(si))
|
|
}
|
|
return sis
|
|
}
|
|
|
|
// Lock should be held
|
|
func (mset *stream) sourceInfo(si *sourceInfo) *StreamSourceInfo {
|
|
if si == nil {
|
|
return nil
|
|
}
|
|
|
|
var ssi = StreamSourceInfo{Name: si.name, Lag: si.lag, Error: si.err, FilterSubject: si.sf}
|
|
|
|
trConfigs := make([]SubjectTransformConfig, len(si.sfs))
|
|
for i := range si.sfs {
|
|
var destination string
|
|
if si.trs[i] != nil {
|
|
destination = si.trs[i].dest
|
|
}
|
|
trConfigs[i] = SubjectTransformConfig{si.sfs[i], destination}
|
|
}
|
|
|
|
ssi.SubjectTransforms = trConfigs
|
|
|
|
// If we have not heard from the source, set Active to -1.
|
|
if last := si.last.Load(); last == 0 {
|
|
ssi.Active = -1
|
|
} else {
|
|
ssi.Active = time.Since(time.Unix(0, last))
|
|
}
|
|
|
|
var ext *ExternalStream
|
|
if mset.cfg.Mirror != nil {
|
|
ext = mset.cfg.Mirror.External
|
|
} else if ss := mset.streamSource(si.iname); ss != nil && ss.External != nil {
|
|
ext = ss.External
|
|
}
|
|
if ext != nil {
|
|
ssi.External = &ExternalStream{
|
|
ApiPrefix: ext.ApiPrefix,
|
|
DeliverPrefix: ext.DeliverPrefix,
|
|
}
|
|
}
|
|
return &ssi
|
|
}
|
|
|
|
// Return our source info for our mirror.
|
|
func (mset *stream) mirrorInfo() *StreamSourceInfo {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
return mset.sourceInfo(mset.mirror)
|
|
}
|
|
|
|
// retryDisconnectedSyncConsumers() will check if we have any disconnected
|
|
// sync consumers for either mirror or a source and will reset and retry to connect.
|
|
func (mset *stream) retryDisconnectedSyncConsumers() {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
|
|
// Only applicable if we are the stream leader.
|
|
if !mset.isLeader() {
|
|
return
|
|
}
|
|
|
|
// Not client.isClosed(): internal account clients have nil nc, which would make isClosed always true here.
|
|
clientClosed := func(c *client) bool {
|
|
return c != nil && (c.flags.isSet(closeConnection) || c.flags.isSet(connMarkedClosed))
|
|
}
|
|
// Stale sources need to be reset: we expect a heartbeat every sourceHealthHB, so missing a couple
|
|
// is a strong signal the remote delivery is no longer reaching us and a retry is warranted.
|
|
stale := func(si *sourceInfo) bool {
|
|
return time.Since(time.Unix(0, si.last.Load())) > 2*sourceHealthHB
|
|
}
|
|
shouldRetry := func(si *sourceInfo) bool {
|
|
if si != nil && (si.sip || si.sub == nil || clientClosed(si.sub.client) || stale(si)) {
|
|
si.fails, si.sip = 0, false
|
|
mset.cancelSourceInfo(si)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Check mirrors first.
|
|
if si := mset.mirror; si != nil {
|
|
if shouldRetry(si) {
|
|
mset.scheduleSetupMirrorConsumerRetry()
|
|
}
|
|
} else {
|
|
for _, si := range mset.sources {
|
|
if shouldRetry(si) {
|
|
mset.setupSourceConsumer(si.iname, si.sseq+1, time.Time{})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const (
|
|
// Our consumer HB interval.
|
|
sourceHealthHB = 1 * time.Second
|
|
// How often we check and our stalled interval.
|
|
sourceHealthCheckInterval = 10 * time.Second
|
|
)
|
|
|
|
// Will run as a Go routine to process mirror consumer messages.
|
|
func (mset *stream) processMirrorMsgs(mirror *sourceInfo, ready *sync.WaitGroup) {
|
|
s := mset.srv
|
|
defer func() {
|
|
mirror.wg.Done()
|
|
s.grWG.Done()
|
|
}()
|
|
|
|
// Grab stream quit channel.
|
|
mset.mu.Lock()
|
|
msgs, qch, siqch := mirror.msgs, mset.qch, mirror.qch
|
|
// If the mirror was already canceled before we got here, exit early.
|
|
if siqch == nil {
|
|
mset.mu.Unlock()
|
|
ready.Done()
|
|
return
|
|
}
|
|
// Set the last seen as now so that we don't fail at the first check.
|
|
mirror.last.Store(time.Now().UnixNano())
|
|
mset.mu.Unlock()
|
|
|
|
// Signal the caller that we have captured the above fields.
|
|
ready.Done()
|
|
|
|
// Make sure we have valid ipq for msgs.
|
|
if msgs == nil {
|
|
mset.mu.Lock()
|
|
mset.cancelMirrorConsumer()
|
|
mset.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
t := time.NewTicker(sourceHealthCheckInterval)
|
|
defer t.Stop()
|
|
|
|
for {
|
|
select {
|
|
case <-s.quitCh:
|
|
return
|
|
case <-qch:
|
|
return
|
|
case <-siqch:
|
|
return
|
|
case <-msgs.ch:
|
|
ims := msgs.pop()
|
|
for _, im := range ims {
|
|
if !mset.processInboundMirrorMsg(im) {
|
|
break
|
|
}
|
|
im.returnToPool()
|
|
}
|
|
msgs.recycle(&ims)
|
|
case <-t.C:
|
|
mset.mu.RLock()
|
|
var stalled bool
|
|
if mset.mirror != nil {
|
|
stalled = time.Since(time.Unix(0, mset.mirror.last.Load())) > sourceHealthCheckInterval
|
|
}
|
|
isLeader := mset.isLeader()
|
|
mset.mu.RUnlock()
|
|
// No longer leader.
|
|
if !isLeader {
|
|
mset.mu.Lock()
|
|
mset.cancelMirrorConsumer()
|
|
mset.mu.Unlock()
|
|
return
|
|
}
|
|
// We are stalled.
|
|
if stalled {
|
|
mset.retryMirrorConsumer()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Checks that the message is from our current direct consumer. We can not depend on sub comparison
|
|
// since cross account imports break.
|
|
func (si *sourceInfo) isCurrentSub(cname string) bool {
|
|
return si.cname != _EMPTY_ && si.cname == cname
|
|
}
|
|
|
|
// processInboundMirrorMsg handles processing messages bound for a stream.
|
|
func (mset *stream) processInboundMirrorMsg(m *inMsg) bool {
|
|
mset.mu.Lock()
|
|
if mset.mirror == nil {
|
|
mset.mu.Unlock()
|
|
return false
|
|
}
|
|
if !mset.isLeader() {
|
|
mset.cancelMirrorConsumer()
|
|
mset.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
isControl := m.isControlMsg()
|
|
cname := consumerFromAckReply(m.rply)
|
|
|
|
// Ignore from old subscriptions.
|
|
// The reason we can not just compare subs is that on cross account imports they will not match.
|
|
if !mset.mirror.isCurrentSub(cname) && !isControl {
|
|
mset.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
// Check for heartbeats and flow control messages.
|
|
if isControl {
|
|
var needsRetry bool
|
|
// Flow controls have reply subjects.
|
|
if m.rply != _EMPTY_ {
|
|
mset.handleFlowControl(m, mset.mirror.dseq, mset.mirror.sseq)
|
|
} else {
|
|
// For idle heartbeats make sure we did not miss anything and check if we are considered stalled.
|
|
if ldseq := parseInt64(sliceHeader(JSLastConsumerSeq, m.hdr)); ldseq > 0 && uint64(ldseq) != mset.mirror.dseq {
|
|
needsRetry = true
|
|
} else if fcReply := sliceHeader(JSConsumerStalled, m.hdr); len(fcReply) > 0 {
|
|
// Other side thinks we are stalled, so send flow control reply.
|
|
mset.outq.sendMsg(string(fcReply), nil)
|
|
}
|
|
}
|
|
mset.mu.Unlock()
|
|
if needsRetry {
|
|
mset.retryMirrorConsumer()
|
|
}
|
|
return !needsRetry
|
|
}
|
|
|
|
sseq, dseq, dc, ts, pending := ackReplyInfo(m.rply)
|
|
|
|
if dc > 1 {
|
|
mset.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
// Mirror info tracking.
|
|
olag, osseq, odseq := mset.mirror.lag, mset.mirror.sseq, mset.mirror.dseq
|
|
if sseq <= mset.mirror.sseq {
|
|
// Ignore older messages.
|
|
// If the deliver sequence matches, we only update delivered accounting.
|
|
if dseq == mset.mirror.dseq+1 {
|
|
mset.mirror.dseq++
|
|
}
|
|
mset.mu.Unlock()
|
|
return true
|
|
} else if sseq == mset.mirror.sseq+1 {
|
|
mset.mirror.dseq = dseq
|
|
mset.mirror.sseq++
|
|
} else if mset.mirror.cname == _EMPTY_ {
|
|
mset.mirror.cname = cname
|
|
mset.mirror.dseq, mset.mirror.sseq = dseq, sseq
|
|
} else {
|
|
// If the deliver sequence matches then the upstream stream has expired or deleted messages.
|
|
if dseq == mset.mirror.dseq+1 {
|
|
mset.skipMsgs(mset.mirror.sseq+1, sseq-1)
|
|
mset.mirror.dseq++
|
|
mset.mirror.sseq = sseq
|
|
} else {
|
|
mset.mu.Unlock()
|
|
mset.retryMirrorConsumer()
|
|
return false
|
|
}
|
|
}
|
|
|
|
if pending == 0 {
|
|
mset.mirror.lag = 0
|
|
} else {
|
|
mset.mirror.lag = pending - 1
|
|
}
|
|
|
|
// Check if we allow mirror direct here. If so check they we have mostly caught up.
|
|
// The reason we do not require 0 is if the source is active we may always be slightly behind.
|
|
if mset.cfg.MirrorDirect && mset.mirrorDirectSub == nil && pending < dgetCaughtUpThresh {
|
|
if err := mset.subscribeToMirrorDirect(); err != nil {
|
|
// Disable since we had problems above.
|
|
mset.cfg.MirrorDirect = false
|
|
}
|
|
}
|
|
|
|
// Do the subject transform if there's one
|
|
if len(mset.mirror.trs) > 0 {
|
|
for _, tr := range mset.mirror.trs {
|
|
if tr == nil {
|
|
continue
|
|
} else {
|
|
tsubj, err := tr.Match(m.subj)
|
|
if err == nil {
|
|
m.subj = tsubj
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
s, js, stype := mset.srv, mset.js, mset.cfg.Storage
|
|
node := mset.node
|
|
mset.mu.Unlock()
|
|
|
|
var err error
|
|
if node != nil {
|
|
if js.limitsExceeded(stype) {
|
|
s.resourcesExceededError(stype)
|
|
err = ApiErrors[JSInsufficientResourcesErr]
|
|
} else {
|
|
err = node.Propose(encodeStreamMsg(m.subj, _EMPTY_, m.hdr, m.msg, sseq-1, ts, true))
|
|
}
|
|
} else {
|
|
err = mset.processJetStreamMsg(m.subj, _EMPTY_, m.hdr, m.msg, sseq-1, ts, nil, true, true)
|
|
}
|
|
if err != nil {
|
|
if strings.Contains(err.Error(), "no space left") {
|
|
s.Errorf("JetStream out of space, will be DISABLED")
|
|
s.DisableJetStream()
|
|
return false
|
|
}
|
|
if err != errLastSeqMismatch {
|
|
mset.mu.RLock()
|
|
accName, sname := mset.acc.Name, mset.cfg.Name
|
|
mset.mu.RUnlock()
|
|
s.RateLimitWarnf("Error processing inbound mirror message for '%s' > '%s': %v",
|
|
accName, sname, err)
|
|
} else {
|
|
// We may have missed messages, restart.
|
|
if lseq := mset.lastSeq(); sseq <= lseq {
|
|
mset.mu.Lock()
|
|
mset.mirror.lag = olag
|
|
mset.mirror.sseq = lseq
|
|
mset.mirror.dseq = odseq
|
|
mset.mu.Unlock()
|
|
return false
|
|
} else {
|
|
mset.mu.Lock()
|
|
mset.mirror.dseq = odseq
|
|
mset.mirror.sseq = osseq
|
|
mset.mu.Unlock()
|
|
mset.retryMirrorConsumer()
|
|
}
|
|
}
|
|
}
|
|
return err == nil
|
|
}
|
|
|
|
func (mset *stream) setMirrorErr(err *ApiError) {
|
|
mset.mu.Lock()
|
|
if mset.mirror != nil {
|
|
mset.mirror.err = err
|
|
}
|
|
mset.mu.Unlock()
|
|
}
|
|
|
|
// Cancels a mirror consumer.
|
|
//
|
|
// Lock held on entry
|
|
func (mset *stream) cancelMirrorConsumer() {
|
|
if mset.mirror == nil {
|
|
return
|
|
}
|
|
mset.cancelSourceInfo(mset.mirror)
|
|
}
|
|
|
|
// Similar to setupMirrorConsumer except that it will print a debug statement
|
|
// indicating that there is a retry.
|
|
//
|
|
// Lock is acquired in this function
|
|
func (mset *stream) retryMirrorConsumer() error {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
mset.srv.Debugf("Retrying mirror consumer for '%s > %s'", mset.acc.Name, mset.cfg.Name)
|
|
mset.cancelMirrorConsumer()
|
|
return mset.setupMirrorConsumer()
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) skipMsgs(start, end uint64) {
|
|
node, store := mset.node, mset.store
|
|
// If we are not clustered we can short circuit now with store.SkipMsgs
|
|
if node == nil {
|
|
store.SkipMsgs(start, end-start+1)
|
|
mset.lseq = end
|
|
return
|
|
}
|
|
|
|
// Must only be enabled once every peer in the cluster supports receiving
|
|
// deleteRangeOp in the normal apply path; older peers panic on unknown ops.
|
|
if mset.srv.getOpts().getFeatureFlag(FeatureFlagJsRaftDeleteRange) {
|
|
node.Propose(encodeDeleteRange(&DeleteRange{First: start, Num: end - start + 1}))
|
|
return
|
|
}
|
|
|
|
var entries []*Entry
|
|
for seq := start; seq <= end; seq++ {
|
|
entries = append(entries, newEntry(EntryNormal, encodeStreamMsg(_EMPTY_, _EMPTY_, nil, nil, seq-1, 0, false)))
|
|
// So a single message does not get too big.
|
|
if len(entries) > 10_000 {
|
|
node.ProposeMulti(entries)
|
|
// We need to re-create `entries` because there is a reference
|
|
// to it in the node's pae map.
|
|
entries = entries[:0]
|
|
}
|
|
}
|
|
// Send all at once.
|
|
if len(entries) > 0 {
|
|
node.ProposeMulti(entries)
|
|
}
|
|
}
|
|
|
|
const (
|
|
// Base retry backoff duration.
|
|
retryBackOff = 5 * time.Second
|
|
// Maximum amount we will wait.
|
|
retryMaximum = 2 * time.Minute
|
|
)
|
|
|
|
// Calculate our backoff based on number of failures.
|
|
func calculateRetryBackoff(fails int) time.Duration {
|
|
backoff := time.Duration(retryBackOff) * time.Duration(fails*2)
|
|
if backoff > retryMaximum {
|
|
backoff = retryMaximum
|
|
}
|
|
return backoff
|
|
}
|
|
|
|
// This will schedule a call to setupMirrorConsumer, taking into account the last
|
|
// time it was retried and determine the soonest setupMirrorConsumer can be called
|
|
// without tripping the sourceConsumerRetryThreshold. We will also take into account
|
|
// number of failures and will back off our retries.
|
|
// The mset.mirror pointer has been verified to be not nil by the caller.
|
|
//
|
|
// Lock held on entry
|
|
func (mset *stream) scheduleSetupMirrorConsumerRetry() {
|
|
// We are trying to figure out how soon we can retry. setupMirrorConsumer will reject
|
|
// a retry if last was done less than "sourceConsumerRetryThreshold" ago.
|
|
next := sourceConsumerRetryThreshold - time.Since(mset.mirror.lreq)
|
|
if next < 0 {
|
|
// It means that we have passed the threshold and so we are ready to go.
|
|
next = 0
|
|
}
|
|
// Take into account failures here.
|
|
next += calculateRetryBackoff(mset.mirror.fails)
|
|
|
|
// Add some jitter.
|
|
next += time.Duration(rand.Intn(int(100*time.Millisecond))) + 100*time.Millisecond
|
|
|
|
stopAndClearTimer(&mset.mirrorConsumerSetup)
|
|
mset.mirrorConsumerSetup = time.AfterFunc(next, func() {
|
|
mset.mu.Lock()
|
|
mset.setupMirrorConsumer()
|
|
mset.mu.Unlock()
|
|
})
|
|
}
|
|
|
|
// How long we wait for a response from a consumer create request for a source or mirror.
|
|
var srcConsumerWaitTime = 30 * time.Second
|
|
|
|
// Setup our mirror consumer.
|
|
// Lock should be held.
|
|
func (mset *stream) setupMirrorConsumer() error {
|
|
if mset.closed.Load() {
|
|
return errStreamClosed
|
|
}
|
|
if mset.outq == nil {
|
|
return errors.New("outq required")
|
|
}
|
|
// We use to prevent update of a mirror configuration in cluster
|
|
// mode but not in standalone. This is now fixed. However, without
|
|
// rejecting the update, it could be that if the source stream was
|
|
// removed and then later the mirrored stream config changed to
|
|
// remove mirror configuration, this function would panic when
|
|
// accessing mset.cfg.Mirror fields. Adding this protection in case
|
|
// we allow in the future the mirror config to be changed (removed).
|
|
if mset.cfg.Mirror == nil {
|
|
return errors.New("invalid mirror configuration")
|
|
}
|
|
|
|
// If this is the first time
|
|
if mset.mirror == nil {
|
|
mset.mirror = &sourceInfo{name: mset.cfg.Mirror.Name}
|
|
} else {
|
|
mset.cancelSourceInfo(mset.mirror)
|
|
mset.mirror.sseq = mset.lseq
|
|
}
|
|
|
|
// If we are no longer the leader stop trying.
|
|
if !mset.isLeader() {
|
|
return nil
|
|
}
|
|
|
|
mirror := mset.mirror
|
|
|
|
// We want to throttle here in terms of how fast we request new consumers,
|
|
// or if the previous is still in progress.
|
|
if last := time.Since(mirror.lreq); last < sourceConsumerRetryThreshold || mirror.sip {
|
|
mset.scheduleSetupMirrorConsumerRetry()
|
|
return nil
|
|
}
|
|
mirror.lreq = time.Now()
|
|
|
|
// Determine subjects etc.
|
|
var deliverSubject string
|
|
var durableDeliverSubject string
|
|
ext := mset.cfg.Mirror.External
|
|
if mset.cfg.Mirror.Consumer != nil {
|
|
durableDeliverSubject = mset.cfg.Mirror.Consumer.DeliverSubject
|
|
mirror.cname = mset.cfg.Mirror.Consumer.Name
|
|
} else if ext != nil && ext.DeliverPrefix != _EMPTY_ {
|
|
deliverSubject = strings.ReplaceAll(ext.DeliverPrefix+syncSubject(".M"), "..", ".")
|
|
} else {
|
|
deliverSubject = syncSubject("$JS.M")
|
|
}
|
|
|
|
// Now send off request to create/update our consumer. This will be all API based even in single server mode.
|
|
// We calculate durable names apriori so we do not need to save them off.
|
|
|
|
var state StreamState
|
|
mset.store.FastState(&state)
|
|
|
|
id := mset.createStableConsumerHash()
|
|
metadata := map[string]string{}
|
|
metadata["_nats.mirror.stream"] = mset.cfg.Name
|
|
metadata["_nats.mirror.acc"] = mset.acc.Name
|
|
if domain := mset.srv.getOpts().JetStreamDomain; domain != _EMPTY_ {
|
|
metadata["_nats.mirror.domain"] = domain
|
|
}
|
|
|
|
req := &CreateConsumerRequest{
|
|
Stream: mset.cfg.Mirror.Name,
|
|
Config: ConsumerConfig{
|
|
Name: fmt.Sprintf("JS_MIRROR_%s", id),
|
|
DeliverSubject: deliverSubject,
|
|
DeliverPolicy: DeliverByStartSequence,
|
|
OptStartSeq: state.LastSeq + 1,
|
|
AckPolicy: AckNone,
|
|
AckWait: 22 * time.Hour,
|
|
MaxDeliver: 1,
|
|
Heartbeat: sourceHealthHB,
|
|
FlowControl: true,
|
|
Direct: true,
|
|
Sourcing: true,
|
|
InactiveThreshold: sourceHealthCheckInterval,
|
|
Metadata: metadata,
|
|
},
|
|
}
|
|
|
|
// Only use start optionals on first time.
|
|
if state.Msgs == 0 && state.FirstSeq == 0 {
|
|
req.Config.OptStartSeq = 0
|
|
if mset.cfg.Mirror.OptStartSeq > 0 {
|
|
req.Config.OptStartSeq = mset.cfg.Mirror.OptStartSeq
|
|
} else if mset.cfg.Mirror.OptStartTime != nil {
|
|
req.Config.OptStartTime = mset.cfg.Mirror.OptStartTime
|
|
req.Config.DeliverPolicy = DeliverByStartTime
|
|
}
|
|
}
|
|
if req.Config.OptStartSeq == 0 && req.Config.OptStartTime == nil {
|
|
// If starting out and lastSeq is 0.
|
|
req.Config.DeliverPolicy = DeliverAll
|
|
}
|
|
|
|
// Filters
|
|
if mset.cfg.Mirror.FilterSubject != _EMPTY_ {
|
|
req.Config.FilterSubject = mset.cfg.Mirror.FilterSubject
|
|
mirror.sf = mset.cfg.Mirror.FilterSubject
|
|
}
|
|
|
|
if lst := len(mset.cfg.Mirror.SubjectTransforms); lst > 0 {
|
|
sfs := make([]string, lst)
|
|
trs := make([]*subjectTransform, lst)
|
|
|
|
for i, tr := range mset.cfg.Mirror.SubjectTransforms {
|
|
// will not fail as already checked before that the transform will work
|
|
subjectTransform, err := NewSubjectTransform(tr.Source, tr.Destination)
|
|
if err != nil {
|
|
mset.srv.Errorf("Unable to get transform for mirror consumer: %v", err)
|
|
}
|
|
sfs[i] = tr.Source
|
|
trs[i] = subjectTransform
|
|
}
|
|
mirror.sfs = sfs
|
|
mirror.trs = trs
|
|
// If there was no explicit FilterSubject defined and we have a single
|
|
// subject transform, use Config.FilterSubject instead of FilterSubjects
|
|
// so that we can use the extended consumer create API down below.
|
|
if req.Config.FilterSubject == _EMPTY_ && len(sfs) == 1 {
|
|
req.Config.FilterSubject = sfs[0]
|
|
} else {
|
|
req.Config.FilterSubjects = sfs
|
|
}
|
|
}
|
|
|
|
respCh := make(chan *JSApiConsumerCreateResponse, 1)
|
|
reply := infoReplySubject()
|
|
crSub, err := mset.subscribeInternal(reply, func(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
|
|
_, msg := c.msgParts(rmsg)
|
|
|
|
var ccr JSApiConsumerCreateResponse
|
|
if err := json.Unmarshal(msg, &ccr); err != nil {
|
|
c.Warnf("JetStream bad mirror consumer create response: %q", msg)
|
|
mset.setMirrorErr(ApiErrors[JSInvalidJSONErr])
|
|
return
|
|
}
|
|
select {
|
|
case respCh <- &ccr:
|
|
default:
|
|
}
|
|
})
|
|
if err != nil {
|
|
mirror.err = NewJSMirrorConsumerSetupFailedError(err, Unless(err))
|
|
mset.scheduleSetupMirrorConsumerRetry()
|
|
return nil
|
|
}
|
|
|
|
generateSubject := func() (subject string) {
|
|
if durableDeliverSubject != _EMPTY_ {
|
|
// If we're using a pre-existing consumer, we'll send a consumer reset request instead.
|
|
subject = fmt.Sprintf(JSApiConsumerResetT, mset.cfg.Mirror.Name, mirror.cname)
|
|
} else if req.Config.FilterSubject != _EMPTY_ {
|
|
subject = fmt.Sprintf(JSApiConsumerCreateExT, mset.cfg.Mirror.Name, req.Config.Name, req.Config.FilterSubject)
|
|
} else {
|
|
subject = fmt.Sprintf(JSApiConsumerCreateT, mset.cfg.Mirror.Name)
|
|
}
|
|
if ext != nil {
|
|
subject = strings.Replace(subject, JSApiPrefix, ext.ApiPrefix, 1)
|
|
subject = strings.ReplaceAll(subject, "..", ".")
|
|
}
|
|
return subject
|
|
}
|
|
subject := generateSubject()
|
|
|
|
// Reset
|
|
mirror.msgs = nil
|
|
mirror.err = nil
|
|
mirror.sip = true
|
|
|
|
if durableDeliverSubject != _EMPTY_ {
|
|
// Send the consumer reset request
|
|
mset.outq.send(newJSPubMsg(subject, _EMPTY_, reply, nil, nil, nil, 0))
|
|
} else {
|
|
// Marshal now that we are done with `req`.
|
|
b, _ := json.Marshal(req)
|
|
|
|
// Send the consumer create request
|
|
// Confirm the server supports API level 4, which contains durable sourcing, AckFlowControl, and consumer reset.
|
|
hdr := genHeader(nil, JSRequiredApiLevel, "4")
|
|
mset.outq.send(newJSPubMsg(subject, _EMPTY_, reply, hdr, b, nil, 0))
|
|
}
|
|
|
|
go func() {
|
|
|
|
var retry bool
|
|
defer func() {
|
|
mset.mu.Lock()
|
|
// Check that this is still valid and if so, clear the "setup in progress" flag.
|
|
if mset.mirror != nil {
|
|
mset.mirror.sip = false
|
|
// If we need to retry, schedule now
|
|
// If sub is not nil means we re-established somewhere else so do not re-attempt here.
|
|
if retry && mset.mirror.sub == nil {
|
|
mset.mirror.fails++
|
|
// Cancel here since we can not do anything with this consumer at this point.
|
|
mset.cancelSourceInfo(mset.mirror)
|
|
mset.scheduleSetupMirrorConsumerRetry()
|
|
} else {
|
|
// Clear on success.
|
|
mset.mirror.fails = 0
|
|
}
|
|
}
|
|
mset.mu.Unlock()
|
|
}()
|
|
|
|
// Wait for previous processMirrorMsgs go routine to be completely done.
|
|
// If none is running, this will not block.
|
|
mset.mu.Lock()
|
|
if mset.mirror == nil {
|
|
// Mirror config has been removed.
|
|
mset.unsubscribe(crSub)
|
|
mset.mu.Unlock()
|
|
return
|
|
}
|
|
wg := &mset.mirror.wg
|
|
mset.mu.Unlock()
|
|
wg.Wait()
|
|
|
|
SELECT:
|
|
select {
|
|
case ccr := <-respCh:
|
|
mset.mu.Lock()
|
|
// Mirror config has been removed.
|
|
if mset.mirror == nil {
|
|
mset.unsubscribe(crSub)
|
|
mset.mu.Unlock()
|
|
return
|
|
}
|
|
ready := sync.WaitGroup{}
|
|
mirror := mset.mirror
|
|
mirror.err = nil
|
|
|
|
if ccr.Error != nil || ccr.ConsumerInfo == nil {
|
|
// If the responding server doesn't support sourcing consumers, retry without it.
|
|
if req.Config.Sourcing && ccr.Error != nil &&
|
|
(ccr.Error.ErrCode == uint16(JSRequiredApiLevelErr) || ccr.Error.ErrCode == uint16(JSInvalidJSONErr)) {
|
|
// Unset for retry.
|
|
req.Config.Sourcing = false
|
|
// Specify a unique consumer name, as the other end will not know to do this.
|
|
req.Config.Name = fmt.Sprintf("JS_MIRROR_%s_%s", id, createConsumerName())
|
|
b, _ := json.Marshal(req)
|
|
// Regenerate subject since the previous name could've been included in it.
|
|
subject = generateSubject()
|
|
mset.outq.send(newJSPubMsg(subject, _EMPTY_, reply, nil, b, nil, 0))
|
|
mset.mu.Unlock()
|
|
goto SELECT
|
|
}
|
|
mset.unsubscribe(crSub)
|
|
mset.srv.Warnf("JetStream error response for create mirror consumer: %+v", ccr.Error)
|
|
mirror.err = ccr.Error
|
|
// Let's retry as soon as possible, but we are gated by sourceConsumerRetryThreshold
|
|
retry = true
|
|
mset.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// If using durable sourcing, we need the consumer to use acks based on flow control.
|
|
if durableDeliverSubject != _EMPTY_ && ccr.ConsumerInfo.Config.AckPolicy != AckFlowControl {
|
|
mset.unsubscribe(crSub)
|
|
mirror.err = NewJSMirrorConsumerRequiresAckFCError()
|
|
retry = true
|
|
mset.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// We can now unsubscribe.
|
|
mset.unsubscribe(crSub)
|
|
|
|
// Setup actual subscription to process messages from our source.
|
|
qname := fmt.Sprintf("[ACC:%s] stream mirror '%s' of '%s' msgs", mset.acc.Name, mset.cfg.Name, mset.cfg.Mirror.Name)
|
|
// Create a new queue each time
|
|
mirror.msgs = newIPQueue[*inMsg](mset.srv, qname)
|
|
msgs := mirror.msgs
|
|
if durableDeliverSubject != _EMPTY_ {
|
|
deliverSubject = durableDeliverSubject
|
|
} else {
|
|
deliverSubject = ccr.ConsumerInfo.Config.DeliverSubject
|
|
}
|
|
sub, err := mset.subscribeInternal(deliverSubject, func(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
|
|
hdr, msg := c.msgParts(copyBytes(rmsg)) // Need to copy.
|
|
if len(hdr) > 0 {
|
|
// Remove any Nats-Expected- headers as we don't want to validate them.
|
|
hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Expected-")
|
|
// Remove any Nats-Batch- headers, batching is not supported when mirroring.
|
|
hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Batch-")
|
|
}
|
|
mset.queueInbound(msgs, subject, reply, hdr, msg, nil, nil)
|
|
mirror.last.Store(time.Now().UnixNano())
|
|
})
|
|
if err != nil {
|
|
mirror.err = NewJSMirrorConsumerSetupFailedError(err, Unless(err))
|
|
retry = true
|
|
mset.mu.Unlock()
|
|
return
|
|
}
|
|
// Save our sub.
|
|
mirror.sub = sub
|
|
|
|
// Check if we need to skip messages.
|
|
// Re-capture state since the previous may be stale.
|
|
state = StreamState{}
|
|
mset.store.FastState(&state)
|
|
if state.LastSeq < ccr.ConsumerInfo.Delivered.Stream {
|
|
// Check to see if delivered is past our last and we have no msgs. This will help the
|
|
// case when mirroring a stream that has a very high starting sequence number.
|
|
if state.Msgs == 0 && ccr.ConsumerInfo.Delivered.Stream > state.LastSeq {
|
|
mset.store.PurgeEx(_EMPTY_, ccr.ConsumerInfo.Delivered.Stream+1, 0)
|
|
mset.lseq = ccr.ConsumerInfo.Delivered.Stream
|
|
} else {
|
|
mset.skipMsgs(state.LastSeq+1, ccr.ConsumerInfo.Delivered.Stream)
|
|
}
|
|
}
|
|
|
|
// Capture consumer name.
|
|
mirror.cname = ccr.ConsumerInfo.Name
|
|
mirror.dseq = 0
|
|
mirror.sseq = max(ccr.ConsumerInfo.Delivered.Stream, state.LastSeq)
|
|
mirror.qch = make(chan struct{})
|
|
mirror.wg.Add(1)
|
|
ready.Add(1)
|
|
if !mset.srv.startGoRoutine(
|
|
func() { mset.processMirrorMsgs(mirror, &ready) },
|
|
pprofLabels{
|
|
"type": "mirror",
|
|
"account": mset.acc.Name,
|
|
"stream": mset.cfg.Name,
|
|
"consumer": mirror.cname,
|
|
},
|
|
) {
|
|
mirror.wg.Done()
|
|
ready.Done()
|
|
}
|
|
mset.mu.Unlock()
|
|
ready.Wait()
|
|
case <-time.After(srcConsumerWaitTime):
|
|
mset.unsubscribe(crSub)
|
|
// We already waited 30 seconds, let's retry now.
|
|
retry = true
|
|
}
|
|
}()
|
|
|
|
return nil
|
|
}
|
|
|
|
func (mset *stream) streamSource(iname string) *StreamSource {
|
|
for _, ssi := range mset.cfg.Sources {
|
|
if ssi.iname == iname {
|
|
return ssi
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) retrySourceConsumerAtSeq(iName string, seq uint64) {
|
|
s := mset.srv
|
|
|
|
s.Debugf("Retrying source consumer for '%s > %s'", mset.acc.Name, mset.cfg.Name)
|
|
|
|
// setupSourceConsumer will check that the source is still configured.
|
|
mset.setupSourceConsumer(iName, seq, time.Time{})
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) cancelSourceConsumer(iname string) {
|
|
if si := mset.sources[iname]; si != nil {
|
|
mset.cancelSourceInfo(si)
|
|
si.sseq, si.dseq = 0, 0
|
|
}
|
|
}
|
|
|
|
// The `si` has been verified to be not nil. The sourceInfo's sub will
|
|
// be unsubscribed and set to nil (if not already done) and the
|
|
// cname will be reset. The message processing's go routine quit channel
|
|
// will be closed if still opened.
|
|
//
|
|
// Lock should be held
|
|
func (mset *stream) cancelSourceInfo(si *sourceInfo) {
|
|
if si.sub != nil {
|
|
mset.unsubscribe(si.sub)
|
|
si.sub = nil
|
|
}
|
|
mset.removeInternalConsumer(si)
|
|
if si.qch != nil {
|
|
close(si.qch)
|
|
si.qch = nil
|
|
}
|
|
if si.msgs != nil {
|
|
si.msgs.drain()
|
|
si.msgs.unregister()
|
|
}
|
|
// If we have a schedule setup go ahead and delete that.
|
|
if t := mset.sourceSetupSchedules[si.iname]; t != nil {
|
|
t.Stop()
|
|
delete(mset.sourceSetupSchedules, si.iname)
|
|
}
|
|
}
|
|
|
|
const sourceConsumerRetryThreshold = 2 * time.Second
|
|
|
|
// This is the main function to call when needing to setup a new consumer for the source.
|
|
// It actually only does the scheduling of the execution of trySetupSourceConsumer in order to implement retry backoff
|
|
// and throttle the number of requests.
|
|
// Lock should be held.
|
|
func (mset *stream) setupSourceConsumer(iname string, seq uint64, startTime time.Time) {
|
|
if mset.sourceSetupSchedules == nil {
|
|
mset.sourceSetupSchedules = map[string]*time.Timer{}
|
|
}
|
|
|
|
if _, ok := mset.sourceSetupSchedules[iname]; ok {
|
|
// If there is already a timer scheduled, we don't need to do anything.
|
|
return
|
|
}
|
|
|
|
si := mset.sources[iname]
|
|
if si == nil || si.sip { // if sourceInfo was removed or setup is in progress, nothing to do
|
|
return
|
|
}
|
|
|
|
// First calculate the delay until the next time we can
|
|
var scheduleDelay time.Duration
|
|
|
|
if !si.lreq.IsZero() { // it's not the very first time we are called, compute the delay
|
|
// We want to throttle here in terms of how fast we request new consumers
|
|
if sinceLast := time.Since(si.lreq); sinceLast < sourceConsumerRetryThreshold {
|
|
scheduleDelay = sourceConsumerRetryThreshold - sinceLast
|
|
}
|
|
// Is it a retry? If so, add a backoff
|
|
if si.fails > 0 {
|
|
scheduleDelay += calculateRetryBackoff(si.fails)
|
|
}
|
|
}
|
|
|
|
// Always add some jitter
|
|
scheduleDelay += time.Duration(rand.Intn(int(100*time.Millisecond))) + 100*time.Millisecond
|
|
|
|
// Schedule the call to trySetupSourceConsumer
|
|
mset.sourceSetupSchedules[iname] = time.AfterFunc(scheduleDelay, func() {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
|
|
delete(mset.sourceSetupSchedules, iname)
|
|
mset.trySetupSourceConsumer(iname, seq, startTime)
|
|
})
|
|
}
|
|
|
|
// This is where we will actually try to create a new consumer for the source
|
|
// Lock should be held.
|
|
func (mset *stream) trySetupSourceConsumer(iname string, seq uint64, startTime time.Time) {
|
|
// Ignore if closed or not leader.
|
|
if mset.closed.Load() || !mset.isLeader() {
|
|
return
|
|
}
|
|
|
|
si := mset.sources[iname]
|
|
if si == nil {
|
|
return
|
|
}
|
|
|
|
// Cancel previous instance if applicable
|
|
mset.cancelSourceInfo(si)
|
|
|
|
ssi := mset.streamSource(iname)
|
|
if ssi == nil {
|
|
return
|
|
}
|
|
|
|
si.lreq = time.Now()
|
|
|
|
// Determine subjects etc.
|
|
var deliverSubject string
|
|
var durableDeliverSubject string
|
|
ext := ssi.External
|
|
if ssi.Consumer != nil {
|
|
durableDeliverSubject = ssi.Consumer.DeliverSubject
|
|
si.cname = ssi.Consumer.Name
|
|
} else if ext != nil && ext.DeliverPrefix != _EMPTY_ {
|
|
deliverSubject = strings.ReplaceAll(ext.DeliverPrefix+syncSubject(".S"), "..", ".")
|
|
} else {
|
|
deliverSubject = syncSubject("$JS.S")
|
|
}
|
|
|
|
id := mset.createSourcingConsumerHash(ssi, mset.cfg.Sources)
|
|
metadata := map[string]string{}
|
|
metadata["_nats.src.stream"] = mset.cfg.Name
|
|
metadata["_nats.src.acc"] = mset.acc.Name
|
|
if domain := mset.srv.getOpts().JetStreamDomain; domain != _EMPTY_ {
|
|
metadata["_nats.src.domain"] = domain
|
|
}
|
|
|
|
req := &CreateConsumerRequest{
|
|
Stream: si.name,
|
|
Config: ConsumerConfig{
|
|
Name: fmt.Sprintf("JS_SRC_%s", id),
|
|
DeliverSubject: deliverSubject,
|
|
AckPolicy: AckNone,
|
|
AckWait: 22 * time.Hour,
|
|
MaxDeliver: 1,
|
|
Heartbeat: sourceHealthHB,
|
|
FlowControl: true,
|
|
Direct: true,
|
|
Sourcing: true,
|
|
InactiveThreshold: sourceHealthCheckInterval,
|
|
Metadata: metadata,
|
|
},
|
|
}
|
|
|
|
// If starting, check any configs.
|
|
if !startTime.IsZero() && seq > 1 {
|
|
req.Config.OptStartTime = &startTime
|
|
req.Config.DeliverPolicy = DeliverByStartTime
|
|
} else if seq <= 1 {
|
|
if ssi.OptStartSeq > 0 {
|
|
req.Config.OptStartSeq = ssi.OptStartSeq
|
|
req.Config.DeliverPolicy = DeliverByStartSequence
|
|
} else {
|
|
// We have not recovered state so check that configured time is less that our first seq time.
|
|
var state StreamState
|
|
mset.store.FastState(&state)
|
|
if ssi.OptStartTime != nil {
|
|
if !state.LastTime.IsZero() && ssi.OptStartTime.Before(state.LastTime) {
|
|
req.Config.OptStartTime = &state.LastTime
|
|
} else {
|
|
req.Config.OptStartTime = ssi.OptStartTime
|
|
}
|
|
req.Config.DeliverPolicy = DeliverByStartTime
|
|
} else if state.FirstSeq > 1 && !state.LastTime.IsZero() {
|
|
req.Config.OptStartTime = &state.LastTime
|
|
req.Config.DeliverPolicy = DeliverByStartTime
|
|
}
|
|
}
|
|
|
|
} else {
|
|
req.Config.OptStartSeq = seq
|
|
req.Config.DeliverPolicy = DeliverByStartSequence
|
|
}
|
|
// Filters
|
|
if ssi.FilterSubject != _EMPTY_ {
|
|
req.Config.FilterSubject = ssi.FilterSubject
|
|
}
|
|
|
|
var filterSubjects []string
|
|
for _, tr := range ssi.SubjectTransforms {
|
|
filterSubjects = append(filterSubjects, tr.Source)
|
|
}
|
|
req.Config.FilterSubjects = filterSubjects
|
|
|
|
respCh := make(chan *JSApiConsumerCreateResponse, 1)
|
|
reply := infoReplySubject()
|
|
crSub, err := mset.subscribeInternal(reply, func(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
|
|
_, msg := c.msgParts(rmsg)
|
|
var ccr JSApiConsumerCreateResponse
|
|
if err := json.Unmarshal(msg, &ccr); err != nil {
|
|
c.Warnf("JetStream bad source consumer create response: %q", msg)
|
|
return
|
|
}
|
|
select {
|
|
case respCh <- &ccr:
|
|
default:
|
|
}
|
|
})
|
|
if err != nil {
|
|
si.err = NewJSSourceConsumerSetupFailedError(err, Unless(err))
|
|
mset.setupSourceConsumer(iname, seq, startTime)
|
|
return
|
|
}
|
|
|
|
generateSubject := func() (subject string) {
|
|
if durableDeliverSubject != _EMPTY_ {
|
|
// If we're using a pre-existing consumer, we'll send a consumer reset request instead.
|
|
subject = fmt.Sprintf(JSApiConsumerResetT, si.name, si.cname)
|
|
} else if req.Config.FilterSubject != _EMPTY_ {
|
|
subject = fmt.Sprintf(JSApiConsumerCreateExT, si.name, req.Config.Name, req.Config.FilterSubject)
|
|
} else if len(req.Config.FilterSubjects) == 1 {
|
|
// It is necessary to switch to using FilterSubject here as the extended consumer
|
|
// create API checks for it, so as to not accidentally allow multiple filtered subjects.
|
|
req.Config.FilterSubject = req.Config.FilterSubjects[0]
|
|
req.Config.FilterSubjects = nil
|
|
subject = fmt.Sprintf(JSApiConsumerCreateExT, si.name, req.Config.Name, req.Config.FilterSubject)
|
|
} else {
|
|
subject = fmt.Sprintf(JSApiConsumerCreateT, si.name)
|
|
}
|
|
if ext != nil {
|
|
subject = strings.Replace(subject, JSApiPrefix, ext.ApiPrefix, 1)
|
|
subject = strings.ReplaceAll(subject, "..", ".")
|
|
}
|
|
return subject
|
|
}
|
|
subject := generateSubject()
|
|
|
|
// Reset
|
|
si.msgs = nil
|
|
si.err = nil
|
|
si.sip = true
|
|
|
|
if durableDeliverSubject != _EMPTY_ {
|
|
// Send the consumer reset request
|
|
mset.outq.send(newJSPubMsg(subject, _EMPTY_, reply, nil, nil, nil, 0))
|
|
} else {
|
|
// Marshal request.
|
|
b, _ := json.Marshal(req)
|
|
|
|
// Send the consumer create request
|
|
// Confirm the server supports API level 4, which contains durable sourcing, AckFlowControl, and consumer reset.
|
|
hdr := genHeader(nil, JSRequiredApiLevel, "4")
|
|
mset.outq.send(newJSPubMsg(subject, _EMPTY_, reply, hdr, b, nil, 0))
|
|
}
|
|
|
|
go func() {
|
|
|
|
var retry bool
|
|
defer func() {
|
|
mset.mu.Lock()
|
|
// Check that this is still valid and if so, clear the "setup in progress" flag.
|
|
if si := mset.sources[iname]; si != nil {
|
|
si.sip = false
|
|
// If we need to retry, schedule now
|
|
// If sub is not nil means we re-established somewhere else so do not re-attempt here.
|
|
if retry && si.sub == nil {
|
|
si.fails++
|
|
// Cancel here since we can not do anything with this consumer at this point.
|
|
mset.cancelSourceInfo(si)
|
|
mset.setupSourceConsumer(iname, seq, startTime)
|
|
} else {
|
|
// Clear on success.
|
|
si.fails = 0
|
|
}
|
|
}
|
|
mset.mu.Unlock()
|
|
}()
|
|
|
|
SELECT:
|
|
select {
|
|
case ccr := <-respCh:
|
|
mset.mu.Lock()
|
|
// Check that it has not been removed or canceled (si.sub would be nil)
|
|
if si := mset.sources[iname]; si == nil {
|
|
mset.unsubscribe(crSub)
|
|
} else {
|
|
si.err = nil
|
|
|
|
if ccr.Error != nil || ccr.ConsumerInfo == nil {
|
|
// If the responding server doesn't support sourcing consumers, retry without it.
|
|
if req.Config.Sourcing && ccr.Error != nil &&
|
|
(ccr.Error.ErrCode == uint16(JSRequiredApiLevelErr) || ccr.Error.ErrCode == uint16(JSInvalidJSONErr)) {
|
|
// Unset for retry.
|
|
req.Config.Sourcing = false
|
|
// Specify a unique consumer name, as the other end will not know to do this.
|
|
req.Config.Name = fmt.Sprintf("JS_SRC_%s_%s", id, createConsumerName())
|
|
b, _ := json.Marshal(req)
|
|
// Regenerate subject since the previous name could've been included in it.
|
|
subject = generateSubject()
|
|
mset.outq.send(newJSPubMsg(subject, _EMPTY_, reply, nil, b, nil, 0))
|
|
mset.mu.Unlock()
|
|
goto SELECT
|
|
}
|
|
mset.unsubscribe(crSub)
|
|
// Note: this warning can happen a few times when starting up the server when sourcing streams are
|
|
// defined, this is normal as the streams are re-created in no particular order and it is possible
|
|
// that a stream sourcing another could come up before all of its sources have been recreated.
|
|
mset.srv.Warnf("JetStream error response for stream %s create source consumer %s: %+v", mset.cfg.Name, si.name, ccr.Error)
|
|
si.err = ccr.Error
|
|
// Let's retry as soon as possible, but we are gated by sourceConsumerRetryThreshold
|
|
retry = true
|
|
mset.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// If using durable sourcing, we need the consumer to use acks based on flow control.
|
|
if durableDeliverSubject != _EMPTY_ && ccr.ConsumerInfo.Config.AckPolicy != AckFlowControl {
|
|
mset.unsubscribe(crSub)
|
|
si.err = NewJSSourceConsumerRequiresAckFCError()
|
|
retry = true
|
|
mset.mu.Unlock()
|
|
return
|
|
}
|
|
|
|
// We can now unsubscribe.
|
|
mset.unsubscribe(crSub)
|
|
|
|
// Check if our shared msg queue and go routine is running or not.
|
|
if mset.smsgs == nil {
|
|
qname := fmt.Sprintf("[ACC:%s] stream sources '%s' msgs", mset.acc.Name, mset.cfg.Name)
|
|
mset.smsgs = newIPQueue[*inMsg](mset.srv, qname)
|
|
mset.srv.startGoRoutine(func() { mset.processAllSourceMsgs() },
|
|
pprofLabels{
|
|
"type": "source",
|
|
"account": mset.acc.Name,
|
|
"stream": mset.cfg.Name,
|
|
},
|
|
)
|
|
}
|
|
|
|
// Setup actual subscription to process messages from our source.
|
|
if si.sseq < ccr.ConsumerInfo.Delivered.Stream {
|
|
si.sseq = ccr.ConsumerInfo.Delivered.Stream
|
|
}
|
|
// Capture consumer name.
|
|
si.cname = ccr.ConsumerInfo.Name
|
|
|
|
// Do not set si.sseq to seq here. si.sseq will be set in processInboundSourceMsg
|
|
si.dseq = 0
|
|
si.qch = make(chan struct{})
|
|
// Set the last seen as now so that we don't fail at the first check.
|
|
si.last.Store(time.Now().UnixNano())
|
|
|
|
msgs := mset.smsgs
|
|
if durableDeliverSubject != _EMPTY_ {
|
|
deliverSubject = durableDeliverSubject
|
|
} else {
|
|
deliverSubject = ccr.ConsumerInfo.Config.DeliverSubject
|
|
}
|
|
sub, err := mset.subscribeInternal(deliverSubject, func(sub *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
|
|
hdr, msg := c.msgParts(copyBytes(rmsg)) // Need to copy.
|
|
mset.queueInbound(msgs, subject, reply, hdr, msg, si, nil)
|
|
si.last.Store(time.Now().UnixNano())
|
|
})
|
|
if err != nil {
|
|
si.err = NewJSSourceConsumerSetupFailedError(err, Unless(err))
|
|
retry = true
|
|
mset.mu.Unlock()
|
|
return
|
|
}
|
|
// Save our sub.
|
|
si.sub = sub
|
|
}
|
|
mset.mu.Unlock()
|
|
case <-time.After(srcConsumerWaitTime):
|
|
mset.unsubscribe(crSub)
|
|
// We already waited 30 seconds, let's retry now.
|
|
retry = true
|
|
}
|
|
}()
|
|
}
|
|
|
|
// This will process all inbound source msgs.
|
|
// We mux them into one go routine to avoid lock contention and high cpu and thread thrashing.
|
|
// TODO(dlc) make this more then one and pin sources to one of a group.
|
|
func (mset *stream) processAllSourceMsgs() {
|
|
s := mset.srv
|
|
defer s.grWG.Done()
|
|
|
|
mset.mu.RLock()
|
|
msgs, qch := mset.smsgs, mset.qch
|
|
mset.mu.RUnlock()
|
|
|
|
t := time.NewTicker(sourceHealthCheckInterval)
|
|
defer t.Stop()
|
|
|
|
// When we detect we are no longer leader, we will cleanup.
|
|
// Should always return right after this is called.
|
|
cleanUp := func() {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
for _, si := range mset.sources {
|
|
mset.cancelSourceConsumer(si.iname)
|
|
}
|
|
mset.smsgs.drain()
|
|
mset.smsgs.unregister()
|
|
mset.smsgs = nil
|
|
}
|
|
|
|
for {
|
|
select {
|
|
case <-s.quitCh:
|
|
return
|
|
case <-qch:
|
|
return
|
|
case <-msgs.ch:
|
|
ims := msgs.pop()
|
|
for _, im := range ims {
|
|
if !mset.processInboundSourceMsg(im.si, im) {
|
|
// If we are no longer leader bail.
|
|
if !mset.IsLeader() {
|
|
msgs.recycle(&ims)
|
|
cleanUp()
|
|
return
|
|
}
|
|
break
|
|
}
|
|
im.returnToPool()
|
|
}
|
|
msgs.recycle(&ims)
|
|
case <-t.C:
|
|
// If we are no longer leader bail.
|
|
if !mset.IsLeader() {
|
|
cleanUp()
|
|
return
|
|
}
|
|
|
|
// Check health of all sources.
|
|
var stalled []*sourceInfo
|
|
mset.mu.RLock()
|
|
for _, si := range mset.sources {
|
|
if time.Since(time.Unix(0, si.last.Load())) > sourceHealthCheckInterval {
|
|
stalled = append(stalled, si)
|
|
}
|
|
}
|
|
numSources := len(mset.sources)
|
|
mset.mu.RUnlock()
|
|
|
|
// This can happen on an update when no longer have sources.
|
|
if numSources == 0 {
|
|
cleanUp()
|
|
return
|
|
}
|
|
|
|
// We do not want to block here so do in separate Go routine.
|
|
if len(stalled) > 0 {
|
|
go func() {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
for _, si := range stalled {
|
|
mset.setupSourceConsumer(si.iname, si.sseq+1, time.Time{})
|
|
}
|
|
}()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// isControlMsg determines if this is a control message.
|
|
func (m *inMsg) isControlMsg() bool {
|
|
return len(m.msg) == 0 && len(m.hdr) > 0 && bytes.HasPrefix(m.hdr, []byte("NATS/1.0 100 "))
|
|
}
|
|
|
|
// Sends a reply to a flow control request.
|
|
// Lock should be held.
|
|
func (mset *stream) sendFlowControlReply(reply string, hdr []byte) {
|
|
if mset.isLeader() && mset.outq != nil {
|
|
dseq := parseInt64(sliceHeader(JSLastConsumerSeq, hdr))
|
|
sseq := parseInt64(sliceHeader(JSLastStreamSeq, hdr))
|
|
|
|
// If we're responding to flow control without being delivered messages (for example after a restart),
|
|
// we'll only have the stream sequence.
|
|
if sseq > 0 {
|
|
if dseq < 0 {
|
|
dseq = 0
|
|
}
|
|
const t = "NATS/1.0\r\n%s: %d\r\n%s: %d\r\n\r\n"
|
|
hdr = fmt.Appendf(nil, t, JSLastConsumerSeq, dseq, JSLastStreamSeq, sseq)
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
} else {
|
|
mset.outq.sendMsg(reply, nil)
|
|
}
|
|
}
|
|
}
|
|
|
|
// handleFlowControl will properly handle flow control messages for both R==1 and R>1.
|
|
// Lock should be held.
|
|
func (mset *stream) handleFlowControl(m *inMsg, dseq, sseq uint64) {
|
|
// If we are clustered we will send the flow control message through the replication stack.
|
|
if mset.isClustered() {
|
|
// Append the current delivery and stream sequences, to be sent after replication.
|
|
m.hdr = genHeader(m.hdr, JSLastConsumerSeq, strconv.FormatUint(dseq, 10))
|
|
m.hdr = genHeader(m.hdr, JSLastStreamSeq, strconv.FormatUint(sseq, 10))
|
|
mset.node.Propose(encodeStreamMsg(_EMPTY_, m.rply, m.hdr, nil, 0, 0, false))
|
|
} else {
|
|
const t = "NATS/1.0\r\n%s: %d\r\n%s: %d\r\n\r\n"
|
|
hdr := fmt.Appendf(nil, t, JSLastConsumerSeq, dseq, JSLastStreamSeq, sseq)
|
|
mset.outq.send(newJSPubMsg(m.rply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
}
|
|
}
|
|
|
|
// processInboundSourceMsg handles processing other stream messages bound for this stream.
|
|
func (mset *stream) processInboundSourceMsg(si *sourceInfo, m *inMsg) bool {
|
|
mset.mu.Lock()
|
|
// If we are no longer the leader cancel this subscriber.
|
|
if !mset.isLeader() {
|
|
mset.cancelSourceConsumer(si.iname)
|
|
mset.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
isControl := m.isControlMsg()
|
|
cname := consumerFromAckReply(m.rply)
|
|
|
|
// Ignore from old subscriptions.
|
|
if !si.isCurrentSub(cname) && !isControl {
|
|
mset.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
// Check for heartbeats and flow control messages.
|
|
if isControl {
|
|
var needsRetry bool
|
|
// Flow controls have reply subjects.
|
|
if m.rply != _EMPTY_ {
|
|
mset.handleFlowControl(m, si.dseq, si.sseq)
|
|
} else {
|
|
// For idle heartbeats make sure we did not miss anything.
|
|
if ldseq := parseInt64(sliceHeader(JSLastConsumerSeq, m.hdr)); ldseq > 0 && uint64(ldseq) != si.dseq {
|
|
needsRetry = true
|
|
mset.retrySourceConsumerAtSeq(si.iname, si.sseq+1)
|
|
} else if fcReply := sliceHeader(JSConsumerStalled, m.hdr); len(fcReply) > 0 {
|
|
// Other side thinks we are stalled, so send flow control reply.
|
|
mset.outq.sendMsg(string(fcReply), nil)
|
|
}
|
|
}
|
|
mset.mu.Unlock()
|
|
return !needsRetry
|
|
}
|
|
|
|
sseq, dseq, dc, _, pending := ackReplyInfo(m.rply)
|
|
|
|
if dc > 1 {
|
|
mset.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
// Tracking is done here.
|
|
osseq, odseq := si.sseq, si.dseq
|
|
if sseq <= si.sseq {
|
|
// Ignore older messages.
|
|
// If the deliver sequence matches, we only update delivered accounting.
|
|
if dseq == si.dseq+1 {
|
|
si.dseq++
|
|
}
|
|
mset.mu.Unlock()
|
|
return true
|
|
} else if dseq == si.dseq+1 {
|
|
si.dseq++
|
|
si.sseq = sseq
|
|
} else if dseq > si.dseq {
|
|
if si.cname == _EMPTY_ {
|
|
si.cname = cname
|
|
si.dseq, si.sseq = dseq, sseq
|
|
} else {
|
|
mset.retrySourceConsumerAtSeq(si.iname, si.sseq+1)
|
|
mset.mu.Unlock()
|
|
return false
|
|
}
|
|
} else {
|
|
mset.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
if pending == 0 {
|
|
si.lag = 0
|
|
} else {
|
|
si.lag = pending - 1
|
|
}
|
|
node := mset.node
|
|
mset.mu.Unlock()
|
|
|
|
hdr, msg := m.hdr, m.msg
|
|
|
|
// If we are daisy chained here make sure to remove the original one.
|
|
if len(hdr) > 0 {
|
|
hdr = removeHeaderIfPresent(hdr, JSStreamSource)
|
|
// Remove any Nats-Expected- headers as we don't want to validate them.
|
|
hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Expected-")
|
|
// Remove any Nats-Batch- headers, batching is not supported when sourcing.
|
|
hdr = removeHeaderIfPrefixPresent(hdr, "Nats-Batch-")
|
|
}
|
|
// Hold onto the origin reply which has all the metadata.
|
|
hdr = genHeader(hdr, JSStreamSource, si.genSourceHeader(m.subj, m.rply))
|
|
|
|
// Do the subject transform for the source if there's one
|
|
if len(si.trs) > 0 {
|
|
for _, tr := range si.trs {
|
|
if tr == nil {
|
|
continue
|
|
} else {
|
|
tsubj, err := tr.Match(m.subj)
|
|
if err == nil {
|
|
m.subj = tsubj
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
var err error
|
|
// If we are clustered we need to propose this message to the underlying raft group.
|
|
if node != nil {
|
|
err = mset.processClusteredInboundMsg(m.subj, _EMPTY_, hdr, msg, nil, true)
|
|
} else {
|
|
err = mset.processJetStreamMsg(m.subj, _EMPTY_, hdr, msg, 0, 0, nil, true, true)
|
|
}
|
|
if err != nil {
|
|
s := mset.srv
|
|
if strings.Contains(err.Error(), "no space left") {
|
|
s.Errorf("JetStream out of space, will be DISABLED")
|
|
s.DisableJetStream()
|
|
} else {
|
|
mset.mu.RLock()
|
|
accName, sname, iName := mset.acc.Name, mset.cfg.Name, si.iname
|
|
mset.mu.RUnlock()
|
|
// Can happen temporarily all the time during normal operations when the sourcing stream is discard new
|
|
// (example use case is for sourcing into a work queue)
|
|
// TODO - Maybe improve sourcing to WQ with limit and new to use flow control rather than re-creating the consumer.
|
|
discardNew := errors.Is(err, ErrMaxMsgs) || errors.Is(err, ErrMaxBytes) || errors.Is(err, ErrMaxMsgsPerSubject)
|
|
|
|
// Log some warning for errors.
|
|
if !discardNew && !errors.Is(err, errLastSeqMismatch) && !errors.Is(err, errMsgIdDuplicate) {
|
|
s.RateLimitWarnf("Error processing inbound source %q for '%s' > '%s': %v",
|
|
iName, accName, sname, err)
|
|
}
|
|
|
|
// Duplicates can be skipped, continue to the next message.
|
|
if errors.Is(err, errMsgIdDuplicate) {
|
|
return true
|
|
}
|
|
|
|
// Do not need to do a full retry that includes finding the last sequence in the stream
|
|
// for that source. Just re-create starting with the seq we couldn't store instead.
|
|
// Especially if we're replicated, we could have inflight proposals that will not be in our store yet.
|
|
mset.mu.Lock()
|
|
si.dseq = odseq
|
|
si.sseq = osseq
|
|
mset.retrySourceConsumerAtSeq(iName, sseq)
|
|
mset.mu.Unlock()
|
|
}
|
|
return false
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// Generate a new (2.10) style source header (stream name, sequence number, source filter, source destination transform).
|
|
func (si *sourceInfo) genSourceHeader(orig, reply string) string {
|
|
var b strings.Builder
|
|
iNameParts := strings.Split(si.iname, " ")
|
|
|
|
b.WriteString(iNameParts[0])
|
|
b.WriteByte(' ')
|
|
// Grab sequence as text here from reply subject.
|
|
var tsa [expectedNumReplyTokensV2]string
|
|
start, tokens := 0, tsa[:0]
|
|
for i := 0; i < len(reply); i++ {
|
|
if reply[i] == btsep {
|
|
tokens, start = append(tokens, reply[start:i]), i+1
|
|
}
|
|
}
|
|
tokens = append(tokens, reply[start:])
|
|
seq := "1" // Default
|
|
if tokens[0] == "$JS" && tokens[1] == "ACK" {
|
|
if len(tokens) == expectedNumReplyTokensV1 {
|
|
seq = tokens[5]
|
|
} else if len(tokens) >= expectedNumReplyTokensV2 {
|
|
seq = tokens[7]
|
|
}
|
|
}
|
|
b.WriteString(seq)
|
|
|
|
b.WriteByte(' ')
|
|
b.WriteString(iNameParts[1])
|
|
b.WriteByte(' ')
|
|
b.WriteString(iNameParts[2])
|
|
b.WriteByte(' ')
|
|
b.WriteString(orig)
|
|
return b.String()
|
|
}
|
|
|
|
// Original version of header that stored ack reply direct.
|
|
func streamAndSeqFromAckReply(reply string) (string, string, uint64) {
|
|
tsa := [expectedNumReplyTokensV2]string{}
|
|
start, tokens := 0, tsa[:0]
|
|
for i := 0; i < len(reply); i++ {
|
|
if reply[i] == btsep {
|
|
tokens, start = append(tokens, reply[start:i]), i+1
|
|
}
|
|
}
|
|
tokens = append(tokens, reply[start:])
|
|
if (len(tokens) != expectedNumReplyTokensV1 && len(tokens) < expectedNumReplyTokensV2) || tokens[0] != "$JS" || tokens[1] != "ACK" {
|
|
return _EMPTY_, _EMPTY_, 0
|
|
}
|
|
offset := 2
|
|
if len(tokens) >= expectedNumReplyTokensV2 {
|
|
offset = 4
|
|
}
|
|
return tokens[offset], _EMPTY_, uint64(parseAckReplyNum(tokens[offset+3]))
|
|
}
|
|
|
|
func consumerFromAckReply(reply string) string {
|
|
tsa := [expectedNumReplyTokensV2]string{}
|
|
start, tokens := 0, tsa[:0]
|
|
for i := 0; i < len(reply); i++ {
|
|
if reply[i] == btsep {
|
|
tokens, start = append(tokens, reply[start:i]), i+1
|
|
}
|
|
}
|
|
tokens = append(tokens, reply[start:])
|
|
if (len(tokens) != expectedNumReplyTokensV1 && len(tokens) < expectedNumReplyTokensV2) || tokens[0] != "$JS" || tokens[1] != "ACK" {
|
|
return _EMPTY_
|
|
}
|
|
offset := 3
|
|
if len(tokens) >= expectedNumReplyTokensV2 {
|
|
offset = 5
|
|
}
|
|
return tokens[offset]
|
|
}
|
|
|
|
// Extract the stream name, the source index name and the message sequence number from the source header.
|
|
// Uses the filter and transform arguments to provide backwards compatibility
|
|
func streamAndSeq(shdr string) (string, string, uint64) {
|
|
if strings.HasPrefix(shdr, jsAckPre) {
|
|
return streamAndSeqFromAckReply(shdr)
|
|
}
|
|
// New version which is stream index name <SPC> sequence
|
|
fields := strings.Split(shdr, " ")
|
|
nFields := len(fields)
|
|
|
|
if nFields != 2 && nFields <= 3 {
|
|
return _EMPTY_, _EMPTY_, 0
|
|
}
|
|
|
|
if nFields >= 4 {
|
|
return fields[0], strings.Join([]string{fields[0], fields[2], fields[3]}, " "), uint64(parseAckReplyNum(fields[1]))
|
|
} else {
|
|
return fields[0], _EMPTY_, uint64(parseAckReplyNum(fields[1]))
|
|
}
|
|
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) setStartingSequenceForSources(iNames map[string]struct{}) {
|
|
var state StreamState
|
|
mset.store.FastState(&state)
|
|
|
|
// Do not reset sseq here so we can remember when purge/expiration happens.
|
|
if state.Msgs == 0 {
|
|
for iName := range iNames {
|
|
si := mset.sources[iName]
|
|
if si == nil {
|
|
continue
|
|
} else {
|
|
si.dseq = 0
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// From the provided list of sources, we build a sublist that contains
|
|
// the interested filters (including transforms). As we figure out the
|
|
// starting sequence for each source, we will eliminate the source from
|
|
// the map and then refresh the sublist, which in turn makes the sublist
|
|
// ideally more specific. This allows LoadPrevMsgsMulti to work most
|
|
// effectively.
|
|
// Because this is a SimpleSublist we can't just remove the entries per
|
|
// source so we have no other option but to rebuild it from scratch, but
|
|
// this is cheap enough to do so not the end of the world.
|
|
var sl *gsl.SimpleSublist
|
|
refreshSublist := func() {
|
|
sl = gsl.NewSimpleSublist()
|
|
for iName := range iNames {
|
|
si := mset.sources[iName]
|
|
if si == nil {
|
|
continue
|
|
}
|
|
if si.sf == _EMPTY_ {
|
|
sl.Insert(fwcs, struct{}{})
|
|
} else {
|
|
sl.Insert(si.sf, struct{}{})
|
|
}
|
|
for _, sf := range si.sfs {
|
|
if sf == _EMPTY_ {
|
|
sl.Insert(fwcs, struct{}{})
|
|
} else {
|
|
sl.Insert(sf, struct{}{})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
refreshSublist()
|
|
|
|
var smv StoreMsg
|
|
for last := state.LastSeq; ; {
|
|
sm, seq, err := mset.store.LoadPrevMsgMulti(sl, last, &smv)
|
|
if err == ErrStoreEOF || err != nil {
|
|
break
|
|
}
|
|
last = seq - 1
|
|
if len(sm.hdr) == 0 {
|
|
continue
|
|
}
|
|
ss := sliceHeader(JSStreamSource, sm.hdr)
|
|
if len(ss) == 0 {
|
|
continue
|
|
}
|
|
|
|
streamName, indexName, sseq := streamAndSeq(bytesToString(ss))
|
|
if _, ok := iNames[indexName]; ok {
|
|
si := mset.sources[indexName]
|
|
si.sseq = sseq
|
|
si.dseq = 0
|
|
delete(iNames, indexName)
|
|
refreshSublist()
|
|
} else if indexName == _EMPTY_ && streamName != _EMPTY_ {
|
|
for iName := range iNames {
|
|
// TODO streamSource is a linear walk, to optimize later
|
|
if si := mset.sources[iName]; si != nil && streamName == si.name ||
|
|
(mset.streamSource(iName).External != nil && streamName == si.name+":"+getHash(mset.streamSource(iName).External.ApiPrefix)) {
|
|
si.sseq = sseq
|
|
si.dseq = 0
|
|
delete(iNames, iName)
|
|
refreshSublist()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
if len(iNames) == 0 {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// Resets the SourceInfo for all the sources
|
|
// lock should be held.
|
|
func (mset *stream) resetSourceInfo() {
|
|
// Reset if needed.
|
|
mset.stopSourceConsumers()
|
|
mset.sources = make(map[string]*sourceInfo)
|
|
|
|
for _, ssi := range mset.cfg.Sources {
|
|
if ssi.iname == _EMPTY_ {
|
|
ssi.setIndexName()
|
|
}
|
|
|
|
var si *sourceInfo
|
|
|
|
if len(ssi.SubjectTransforms) == 0 {
|
|
si = &sourceInfo{name: ssi.Name, iname: ssi.iname, sf: ssi.FilterSubject}
|
|
} else {
|
|
sfs := make([]string, len(ssi.SubjectTransforms))
|
|
trs := make([]*subjectTransform, len(ssi.SubjectTransforms))
|
|
for i, str := range ssi.SubjectTransforms {
|
|
tr, err := NewSubjectTransform(str.Source, str.Destination)
|
|
if err != nil {
|
|
mset.srv.Errorf("Unable to get subject transform for source: %v", err)
|
|
}
|
|
sfs[i] = str.Source
|
|
trs[i] = tr
|
|
}
|
|
si = &sourceInfo{name: ssi.Name, iname: ssi.iname, sfs: sfs, trs: trs}
|
|
}
|
|
mset.sources[ssi.iname] = si
|
|
}
|
|
}
|
|
|
|
// This will do a reverse scan on startup or leader election
|
|
// searching for the starting sequence number.
|
|
// This can be slow in degenerative cases.
|
|
// Lock should be held.
|
|
func (mset *stream) startingSequenceForSources() {
|
|
if len(mset.cfg.Sources) == 0 {
|
|
return
|
|
}
|
|
|
|
// Always reset here.
|
|
mset.resetSourceInfo()
|
|
|
|
var state StreamState
|
|
mset.store.FastState(&state)
|
|
|
|
// Bail if no messages, meaning no context.
|
|
if state.Msgs == 0 {
|
|
return
|
|
}
|
|
|
|
// For short circuiting return.
|
|
expected := len(mset.cfg.Sources)
|
|
seqs := make(map[string]uint64)
|
|
|
|
// Stamp our si seq records on the way out.
|
|
defer func() {
|
|
for sname, seq := range seqs {
|
|
// Ignore if not set.
|
|
if seq == 0 {
|
|
continue
|
|
}
|
|
if si := mset.sources[sname]; si != nil {
|
|
si.sseq = seq
|
|
si.dseq = 0
|
|
}
|
|
}
|
|
}()
|
|
|
|
// Generate a list of sources and, from that, a sublist that contains
|
|
// the interested filters (including transforms). As we figure out the
|
|
// starting sequence for each source, we will eliminate the source from
|
|
// the map and then refresh the sublist, which in turn makes the sublist
|
|
// ideally more specific. This allows LoadPrevMsgsMulti to work most
|
|
// effectively.
|
|
// Because this is a SimpleSublist we can't just remove the entries per
|
|
// source so we have no other option but to rebuild it from scratch, but
|
|
// this is cheap enough to do so not the end of the world.
|
|
sources := map[string]*StreamSource{}
|
|
for _, src := range mset.cfg.Sources {
|
|
sources[src.composeIName()] = src
|
|
}
|
|
var sl *gsl.SimpleSublist
|
|
refreshSublist := func() {
|
|
sl = gsl.NewSimpleSublist()
|
|
for _, src := range sources {
|
|
if src.FilterSubject == _EMPTY_ {
|
|
sl.Insert(fwcs, struct{}{})
|
|
} else {
|
|
sl.Insert(src.FilterSubject, struct{}{})
|
|
}
|
|
for _, tr := range src.SubjectTransforms {
|
|
if tr.Destination == _EMPTY_ {
|
|
sl.Insert(fwcs, struct{}{})
|
|
} else {
|
|
sl.Insert(tr.Destination, struct{}{})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
refreshSublist()
|
|
|
|
update := func(iName string, seq uint64) {
|
|
// Only update active in case we have older ones in here that got configured out.
|
|
if si := mset.sources[iName]; si != nil {
|
|
if _, ok := seqs[iName]; !ok {
|
|
seqs[iName] = seq
|
|
delete(sources, iName)
|
|
refreshSublist()
|
|
}
|
|
}
|
|
}
|
|
|
|
var smv StoreMsg
|
|
for last := state.LastSeq; ; {
|
|
sm, seq, err := mset.store.LoadPrevMsgMulti(sl, last, &smv)
|
|
if err == ErrStoreEOF || err != nil {
|
|
break
|
|
}
|
|
last = seq - 1
|
|
if len(sm.hdr) == 0 {
|
|
continue
|
|
}
|
|
ss := sliceHeader(JSStreamSource, sm.hdr)
|
|
if len(ss) == 0 {
|
|
continue
|
|
}
|
|
|
|
streamName, iName, sseq := streamAndSeq(bytesToString(ss))
|
|
if iName == _EMPTY_ { // Pre-2.10 message header means it's a match for any source using that stream name
|
|
for _, ssi := range mset.cfg.Sources {
|
|
if streamName == ssi.Name || (ssi.External != nil && streamName == ssi.Name+":"+getHash(ssi.External.ApiPrefix)) {
|
|
update(ssi.iname, sseq)
|
|
}
|
|
}
|
|
} else {
|
|
update(iName, sseq)
|
|
}
|
|
if len(seqs) == expected {
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Setup our source consumers.
|
|
// Lock should be held.
|
|
func (mset *stream) setupSourceConsumers() error {
|
|
if mset.outq == nil {
|
|
return errors.New("outq required")
|
|
}
|
|
// Reset if needed.
|
|
for _, si := range mset.sources {
|
|
if si.sub != nil {
|
|
mset.cancelSourceConsumer(si.iname)
|
|
}
|
|
}
|
|
|
|
// If we are no longer the leader, give up
|
|
if !mset.isLeader() {
|
|
return nil
|
|
}
|
|
|
|
mset.startingSequenceForSources()
|
|
|
|
// Setup our consumers at the proper starting position.
|
|
for _, ssi := range mset.cfg.Sources {
|
|
if si := mset.sources[ssi.iname]; si != nil {
|
|
mset.setupSourceConsumer(ssi.iname, si.sseq+1, time.Time{})
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Will create internal subscriptions for the stream.
|
|
// Lock should be held.
|
|
func (mset *stream) subscribeToStream() error {
|
|
if mset.active {
|
|
return nil
|
|
}
|
|
for _, subject := range mset.cfg.Subjects {
|
|
if _, err := mset.subscribeInternal(subject, mset.processInboundJetStreamMsg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// Check if we need to setup mirroring.
|
|
if mset.cfg.Mirror != nil {
|
|
// setup the initial mirror sourceInfo
|
|
mset.mirror = &sourceInfo{name: mset.cfg.Mirror.Name}
|
|
sfs := make([]string, len(mset.cfg.Mirror.SubjectTransforms))
|
|
trs := make([]*subjectTransform, len(mset.cfg.Mirror.SubjectTransforms))
|
|
|
|
for i, tr := range mset.cfg.Mirror.SubjectTransforms {
|
|
// will not fail as already checked before that the transform will work
|
|
subjectTransform, err := NewSubjectTransform(tr.Source, tr.Destination)
|
|
if err != nil {
|
|
mset.srv.Errorf("Unable to get transform for mirror consumer: %v", err)
|
|
}
|
|
|
|
sfs[i] = tr.Source
|
|
trs[i] = subjectTransform
|
|
}
|
|
mset.mirror.sfs = sfs
|
|
mset.mirror.trs = trs
|
|
// delay the actual mirror consumer creation for after a delay
|
|
mset.scheduleSetupMirrorConsumerRetry()
|
|
} else if len(mset.cfg.Sources) > 0 && mset.sourcesConsumerSetup == nil {
|
|
// Setup the initial source infos for the sources
|
|
mset.resetSourceInfo()
|
|
// Delay the actual source consumer(s) creation(s) for after a delay if a replicated stream.
|
|
// If it's an R1, this is done at startup and we will do inline.
|
|
if mset.cfg.Replicas == 1 {
|
|
mset.setupSourceConsumers()
|
|
} else {
|
|
mset.sourcesConsumerSetup = time.AfterFunc(time.Duration(rand.Intn(int(500*time.Millisecond)))+100*time.Millisecond, func() {
|
|
mset.mu.Lock()
|
|
mset.setupSourceConsumers()
|
|
mset.mu.Unlock()
|
|
})
|
|
}
|
|
}
|
|
// Check for direct get access.
|
|
// We spin up followers for clustered streams in monitorStream().
|
|
if mset.cfg.AllowDirect {
|
|
if err := mset.subscribeToDirect(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
mset.active = true
|
|
return nil
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) subscribeToDirect() error {
|
|
// We will make this listen on a queue group by default, which can allow mirrors to participate on opt-in basis.
|
|
if mset.directSub == nil {
|
|
dsubj := fmt.Sprintf(JSDirectMsgGetT, mset.cfg.Name)
|
|
if sub, err := mset.queueSubscribeInternal(dsubj, dgetGroup, mset.processDirectGetRequest); err == nil {
|
|
mset.directSub = sub
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
// Now the one that will have subject appended past stream name.
|
|
if mset.lastBySub == nil {
|
|
dsubj := fmt.Sprintf(JSDirectGetLastBySubjectT, mset.cfg.Name, fwcs)
|
|
// We will make this listen on a queue group by default, which can allow mirrors to participate on opt-in basis.
|
|
if sub, err := mset.queueSubscribeInternal(dsubj, dgetGroup, mset.processDirectGetLastBySubjectRequest); err == nil {
|
|
mset.lastBySub = sub
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) unsubscribeToDirect() {
|
|
if mset.directSub != nil {
|
|
mset.unsubscribe(mset.directSub)
|
|
mset.directSub = nil
|
|
}
|
|
if mset.lastBySub != nil {
|
|
mset.unsubscribe(mset.lastBySub)
|
|
mset.lastBySub = nil
|
|
}
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) subscribeToMirrorDirect() error {
|
|
if mset.cfg.Mirror == nil {
|
|
return nil
|
|
}
|
|
mirrorName := mset.cfg.Mirror.Name
|
|
|
|
// We will make this listen on a queue group by default, which can allow mirrors to participate on opt-in basis.
|
|
if mset.mirrorDirectSub == nil {
|
|
dsubj := fmt.Sprintf(JSDirectMsgGetT, mirrorName)
|
|
// We will make this listen on a queue group by default, which can allow mirrors to participate on opt-in basis.
|
|
if sub, err := mset.queueSubscribeInternal(dsubj, dgetGroup, mset.processDirectGetRequest); err == nil {
|
|
mset.mirrorDirectSub = sub
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
// Now the one that will have subject appended past stream name.
|
|
if mset.mirrorLastBySub == nil {
|
|
dsubj := fmt.Sprintf(JSDirectGetLastBySubjectT, mirrorName, fwcs)
|
|
// We will make this listen on a queue group by default, which can allow mirrors to participate on opt-in basis.
|
|
if sub, err := mset.queueSubscribeInternal(dsubj, dgetGroup, mset.processDirectGetLastBySubjectRequest); err == nil {
|
|
mset.mirrorLastBySub = sub
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) unsubscribeToMirrorDirect() {
|
|
if mset.mirrorDirectSub != nil {
|
|
mset.unsubscribe(mset.mirrorDirectSub)
|
|
mset.mirrorDirectSub = nil
|
|
}
|
|
if mset.mirrorLastBySub != nil {
|
|
mset.unsubscribe(mset.mirrorLastBySub)
|
|
mset.mirrorLastBySub = nil
|
|
}
|
|
}
|
|
|
|
// Stop our source consumers.
|
|
// Lock should be held.
|
|
func (mset *stream) stopSourceConsumers() {
|
|
for _, si := range mset.sources {
|
|
mset.cancelSourceInfo(si)
|
|
}
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) removeInternalConsumer(si *sourceInfo) {
|
|
if si == nil || si.cname == _EMPTY_ {
|
|
return
|
|
}
|
|
si.cname = _EMPTY_
|
|
}
|
|
|
|
// Will unsubscribe from the stream.
|
|
// Lock should be held.
|
|
func (mset *stream) unsubscribeToStream(stopping, shuttingDown bool) error {
|
|
for _, subject := range mset.cfg.Subjects {
|
|
mset.unsubscribeInternal(subject)
|
|
}
|
|
if mset.mirror != nil {
|
|
mset.cancelSourceInfo(mset.mirror)
|
|
mset.mirror = nil
|
|
}
|
|
|
|
if len(mset.sources) > 0 {
|
|
mset.stopSourceConsumers()
|
|
mset.sources = nil
|
|
}
|
|
// Clear batching state.
|
|
mset.deleteAtomicBatches(shuttingDown)
|
|
if stopping || shuttingDown {
|
|
mset.deleteFastBatches()
|
|
}
|
|
if mset.batches != nil {
|
|
mset.batches.mu.Lock()
|
|
reset := len(mset.batches.atomic) == 0 && len(mset.batches.fast) == 0
|
|
mset.batches.mu.Unlock()
|
|
if reset {
|
|
mset.batches = nil
|
|
}
|
|
}
|
|
|
|
if stopping {
|
|
// In case we had a direct get subscriptions.
|
|
mset.unsubscribeToDirect()
|
|
mset.unsubscribeToMirrorDirect()
|
|
}
|
|
|
|
if mset.directLeaderSub == nil {
|
|
// Always unsubscribe the leader sub.
|
|
mset.unsubscribe(mset.directLeaderSub)
|
|
mset.directLeaderSub = nil
|
|
}
|
|
|
|
mset.active = false
|
|
return nil
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) deleteAtomicBatches(shuttingDown bool) {
|
|
if mset.batches != nil {
|
|
mset.batches.mu.Lock()
|
|
for batchId, b := range mset.batches.atomic {
|
|
// If shutting down, do fixup during startup. In-memory batches don't require manual cleanup.
|
|
if shuttingDown {
|
|
b.stopLocked()
|
|
} else {
|
|
b.cleanupLocked(batchId, mset.batches)
|
|
}
|
|
}
|
|
mset.batches.atomic = nil
|
|
mset.batches.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) deleteBatchApplyState() {
|
|
if batch := mset.batchApply; batch != nil {
|
|
// Need to return entries (if any) to the pool.
|
|
for _, bce := range batch.entries {
|
|
bce.ReturnToPool()
|
|
}
|
|
mset.batchApply = nil
|
|
}
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) deleteFastBatches() {
|
|
if mset.batches != nil {
|
|
mset.batches.mu.Lock()
|
|
for batchId, b := range mset.batches.fast {
|
|
b.cleanupLocked(batchId, mset.batches)
|
|
}
|
|
mset.batches.fast = nil
|
|
mset.batches.mu.Unlock()
|
|
}
|
|
}
|
|
|
|
// Lock does NOT need to be held, we set the client on setup and never change it at this point.
|
|
func (mset *stream) subscribeInternal(subject string, cb msgHandler) (*subscription, error) {
|
|
if mset.closed.Load() {
|
|
return nil, errStreamClosed
|
|
}
|
|
if cb == nil {
|
|
return nil, errInvalidMsgHandler
|
|
}
|
|
c := mset.client
|
|
sid := int(mset.sid.Add(1))
|
|
// Now create the subscription
|
|
return c.processSub([]byte(subject), nil, []byte(strconv.Itoa(sid)), cb, false)
|
|
}
|
|
|
|
// Lock does NOT need to be held, we set the client on setup and never change it at this point.
|
|
func (mset *stream) queueSubscribeInternal(subject, group string, cb msgHandler) (*subscription, error) {
|
|
if mset.closed.Load() {
|
|
return nil, errStreamClosed
|
|
}
|
|
if cb == nil {
|
|
return nil, errInvalidMsgHandler
|
|
}
|
|
c := mset.client
|
|
sid := int(mset.sid.Add(1))
|
|
// Now create the subscription
|
|
return c.processSub([]byte(subject), []byte(group), []byte(strconv.Itoa(sid)), cb, false)
|
|
}
|
|
|
|
// This will unsubscribe us from the exact subject given.
|
|
// We do not currently track the subs so do not have the sid.
|
|
// This should be called only on an update.
|
|
// Lock does NOT need to be held, we set the client on setup and never change it at this point.
|
|
func (mset *stream) unsubscribeInternal(subject string) error {
|
|
if mset.closed.Load() {
|
|
return errStreamClosed
|
|
}
|
|
c := mset.client
|
|
var sid []byte
|
|
c.mu.Lock()
|
|
for _, sub := range c.subs {
|
|
if subject == string(sub.subject) {
|
|
sid = sub.sid
|
|
break
|
|
}
|
|
}
|
|
c.mu.Unlock()
|
|
|
|
if sid != nil {
|
|
return c.processUnsub(sid)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (mset *stream) unsubscribe(sub *subscription) {
|
|
if sub == nil || mset.closed.Load() {
|
|
return
|
|
}
|
|
mset.client.processUnsub(sub.sid)
|
|
}
|
|
|
|
func (mset *stream) setupStore(fsCfg *FileStoreConfig) error {
|
|
mset.mu.Lock()
|
|
switch mset.cfg.Storage {
|
|
case MemoryStorage:
|
|
ms, err := newMemStore(&mset.cfg)
|
|
if err != nil {
|
|
mset.mu.Unlock()
|
|
return err
|
|
}
|
|
mset.store = ms
|
|
case FileStorage:
|
|
s := mset.srv
|
|
prf := s.jsKeyGen(s.getOpts().JetStreamKey, mset.acc.Name)
|
|
if prf != nil {
|
|
// We are encrypted here, fill in correct cipher selection.
|
|
fsCfg.Cipher = s.getOpts().JetStreamCipher
|
|
}
|
|
oldprf := s.jsKeyGen(s.getOpts().JetStreamOldKey, mset.acc.Name)
|
|
cfg := *fsCfg
|
|
cfg.srv = s
|
|
fs, err := newFileStoreWithCreated(cfg, mset.cfg, mset.created, prf, oldprf)
|
|
if err != nil {
|
|
mset.mu.Unlock()
|
|
return err
|
|
}
|
|
mset.store = fs
|
|
}
|
|
// This will fire the callback but we do not require the lock since md will be 0 here.
|
|
mset.store.RegisterStorageUpdates(mset.storeUpdates)
|
|
mset.store.RegisterStorageRemoveMsg(func(seq uint64) {
|
|
if mset.IsClustered() {
|
|
if mset.IsLeader() {
|
|
mset.mu.RLock()
|
|
md := streamMsgDelete{Seq: seq, NoErase: true, Stream: mset.cfg.Name}
|
|
mset.node.Propose(encodeMsgDelete(&md))
|
|
mset.mu.RUnlock()
|
|
}
|
|
} else {
|
|
mset.removeMsg(seq)
|
|
}
|
|
})
|
|
mset.store.RegisterProcessJetStreamMsg(func(im *inMsg) {
|
|
if mset.IsClustered() {
|
|
if mset.IsLeader() {
|
|
mset.processClusteredInboundMsg(im.subj, im.rply, im.hdr, im.msg, im.mt, true)
|
|
}
|
|
} else {
|
|
mset.processJetStreamMsg(im.subj, im.rply, im.hdr, im.msg, 0, 0, im.mt, true, true)
|
|
}
|
|
})
|
|
mset.mu.Unlock()
|
|
|
|
return nil
|
|
}
|
|
|
|
// Called for any updates to the underlying stream. We pass through the bytes to the
|
|
// jetstream account. We do local processing for stream pending for consumers, but only
|
|
// for removals.
|
|
// Lock should not be held.
|
|
func (mset *stream) storeUpdates(md, bd int64, seq uint64, subj string) {
|
|
// If we have a single negative update then we will process our consumers for stream pending.
|
|
// Purge and Store handled separately inside individual calls.
|
|
if md == -1 && seq > 0 && subj != _EMPTY_ {
|
|
// We use our consumer list mutex here instead of the main stream lock since it may be held already.
|
|
mset.clsMu.RLock()
|
|
if mset.csl != nil {
|
|
mset.csl.Match(subj, func(o *consumer) {
|
|
o.decStreamPending(seq, subj)
|
|
})
|
|
} else {
|
|
for _, o := range mset.cList {
|
|
o.decStreamPending(seq, subj)
|
|
}
|
|
}
|
|
mset.clsMu.RUnlock()
|
|
} else if md < 0 {
|
|
// Batch decrements we need to force consumers to re-calculate num pending.
|
|
mset.clsMu.RLock()
|
|
for _, o := range mset.cList {
|
|
o.streamNumPendingLocked()
|
|
}
|
|
mset.clsMu.RUnlock()
|
|
}
|
|
|
|
if mset.jsa != nil {
|
|
mset.jsa.updateUsage(mset.tier, mset.stype, bd)
|
|
}
|
|
}
|
|
|
|
// NumMsgIds returns the number of message ids being tracked for duplicate suppression.
|
|
func (mset *stream) numMsgIds() int {
|
|
mset.ddMu.Lock()
|
|
defer mset.ddMu.Unlock()
|
|
return len(mset.ddmap)
|
|
}
|
|
|
|
// checkMsgId will process and check for duplicates.
|
|
// mset.ddMu lock should be held.
|
|
func (mset *stream) checkMsgId(id string) *ddentry {
|
|
if id == _EMPTY_ || len(mset.ddmap) == 0 {
|
|
return nil
|
|
}
|
|
return mset.ddmap[id]
|
|
}
|
|
|
|
// Will purge the entries that are past the window.
|
|
// Should be called from a timer.
|
|
func (mset *stream) purgeMsgIds() {
|
|
now := time.Now().UnixNano()
|
|
mset.cfgMu.RLock()
|
|
tmrNext := mset.cfg.Duplicates
|
|
mset.cfgMu.RUnlock()
|
|
window := int64(tmrNext)
|
|
|
|
mset.ddMu.Lock()
|
|
defer mset.ddMu.Unlock()
|
|
|
|
for i, dde := range mset.ddarr[mset.ddindex:] {
|
|
if now-dde.ts >= window {
|
|
delete(mset.ddmap, dde.id)
|
|
} else {
|
|
mset.ddindex += i
|
|
// Check if we should garbage collect here if we are 1/3 total size.
|
|
if cap(mset.ddarr) > 3*(len(mset.ddarr)-mset.ddindex) {
|
|
mset.ddarr = append([]*ddentry(nil), mset.ddarr[mset.ddindex:]...)
|
|
mset.ddindex = 0
|
|
}
|
|
tmrNext = time.Duration(window - (now - dde.ts))
|
|
break
|
|
}
|
|
}
|
|
if len(mset.ddmap) > 0 {
|
|
// Make sure to not fire too quick
|
|
const minFire = 50 * time.Millisecond
|
|
if tmrNext < minFire {
|
|
tmrNext = minFire
|
|
}
|
|
if mset.ddtmr != nil {
|
|
mset.ddtmr.Reset(tmrNext)
|
|
} else {
|
|
mset.ddtmr = time.AfterFunc(tmrNext, mset.purgeMsgIds)
|
|
}
|
|
} else {
|
|
if mset.ddtmr != nil {
|
|
mset.ddtmr.Stop()
|
|
mset.ddtmr = nil
|
|
}
|
|
mset.ddmap = nil
|
|
mset.ddarr = nil
|
|
mset.ddindex = 0
|
|
}
|
|
}
|
|
|
|
// storeMsgId will store the message id for duplicate detection.
|
|
func (mset *stream) storeMsgId(dde *ddentry) {
|
|
mset.ddMu.Lock()
|
|
defer mset.ddMu.Unlock()
|
|
mset.storeMsgIdLocked(dde)
|
|
}
|
|
|
|
// storeMsgIdLocked will store the message id for duplicate detection.
|
|
// mset.ddMu lock should be held.
|
|
func (mset *stream) storeMsgIdLocked(dde *ddentry) {
|
|
// Zero means disabled.
|
|
if mset.cfg.Duplicates <= 0 {
|
|
return
|
|
}
|
|
|
|
if mset.ddmap == nil {
|
|
mset.ddmap = make(map[string]*ddentry)
|
|
}
|
|
mset.ddmap[dde.id] = dde
|
|
mset.ddarr = append(mset.ddarr, dde)
|
|
if mset.ddtmr == nil {
|
|
mset.ddtmr = time.AfterFunc(mset.cfg.Duplicates, mset.purgeMsgIds)
|
|
}
|
|
}
|
|
|
|
// Fast lookup of msgId.
|
|
func getMsgId(hdr []byte) string {
|
|
return string(getHeader(JSMsgId, hdr))
|
|
}
|
|
|
|
// Fast lookup of expected last msgId.
|
|
func getExpectedLastMsgId(hdr []byte) string {
|
|
return string(getHeader(JSExpectedLastMsgId, hdr))
|
|
}
|
|
|
|
// Fast lookup of expected stream.
|
|
func getExpectedStream(hdr []byte) string {
|
|
return string(getHeader(JSExpectedStream, hdr))
|
|
}
|
|
|
|
// Fast lookup of expected last sequence.
|
|
func getExpectedLastSeq(hdr []byte) (uint64, bool) {
|
|
bseq := sliceHeader(JSExpectedLastSeq, hdr)
|
|
if len(bseq) == 0 {
|
|
return 0, false
|
|
}
|
|
return uint64(parseInt64(bseq)), true
|
|
}
|
|
|
|
// Fast lookup of rollups.
|
|
func getRollup(hdr []byte) string {
|
|
r := getHeader(JSMsgRollup, hdr)
|
|
if len(r) == 0 {
|
|
return _EMPTY_
|
|
}
|
|
return strings.ToLower(string(r))
|
|
}
|
|
|
|
// Fast lookup of expected stream sequence per subject.
|
|
func getExpectedLastSeqPerSubject(hdr []byte) (uint64, bool) {
|
|
bseq := sliceHeader(JSExpectedLastSubjSeq, hdr)
|
|
if len(bseq) == 0 {
|
|
return 0, false
|
|
}
|
|
return uint64(parseInt64(bseq)), true
|
|
}
|
|
|
|
// Fast lookup of expected subject for the expected stream sequence per subject.
|
|
func getExpectedLastSeqPerSubjectForSubject(hdr []byte) string {
|
|
return bytesToString(sliceHeader(JSExpectedLastSubjSeqSubj, hdr))
|
|
}
|
|
|
|
// Fast lookup of the message TTL from headers:
|
|
// - Positive return value: duration in seconds.
|
|
// - Zero return value: no TTL or parse error.
|
|
// - Negative return value: never expires.
|
|
func getMessageTTL(hdr []byte) (int64, error) {
|
|
ttl := getHeader(JSMessageTTL, hdr)
|
|
if len(ttl) == 0 {
|
|
return 0, nil
|
|
}
|
|
return parseMessageTTL(bytesToString(ttl))
|
|
}
|
|
|
|
// - Positive return value: duration in seconds.
|
|
// - Zero return value: no TTL or parse error.
|
|
// - Negative return value: never expires.
|
|
func parseMessageTTL(ttl string) (int64, error) {
|
|
if strings.ToLower(ttl) == "never" {
|
|
return -1, nil
|
|
}
|
|
dur, err := time.ParseDuration(ttl)
|
|
if err == nil {
|
|
if dur < time.Second {
|
|
return 0, NewJSMessageTTLInvalidError()
|
|
}
|
|
return int64(dur.Seconds()), nil
|
|
}
|
|
t := parseInt64(stringToBytes(ttl))
|
|
if t < 0 {
|
|
// This probably means a parse failure, hence why
|
|
// we have a special case "never" for returning -1.
|
|
// Otherwise we can't know if it's a genuine TTL
|
|
// that says never expire or if it's a parse error.
|
|
return 0, NewJSMessageTTLInvalidError()
|
|
}
|
|
return t, nil
|
|
}
|
|
|
|
// Fast lookup of the message Incr from headers.
|
|
// Return includes the value or nil, and success.
|
|
func getMessageIncr(hdr []byte) (*big.Int, bool) {
|
|
incr := sliceHeader(JSMessageIncr, hdr)
|
|
if len(incr) == 0 {
|
|
return nil, true
|
|
}
|
|
var v big.Int
|
|
return v.SetString(bytesToString(incr), 10)
|
|
}
|
|
|
|
// Fast lookup of message schedule.
|
|
func getMessageSchedule(hdr []byte) (time.Time, *ApiError) {
|
|
if len(hdr) == 0 {
|
|
return time.Time{}, nil
|
|
}
|
|
return nextMessageSchedule(hdr, time.Now().UTC().UnixNano())
|
|
}
|
|
|
|
// Fast lookup and calculation of next message schedule.
|
|
func nextMessageSchedule(hdr []byte, ts int64) (time.Time, *ApiError) {
|
|
if len(hdr) == 0 {
|
|
return time.Time{}, nil
|
|
}
|
|
loc, apiErr := loadMessageScheduleLocation(hdr)
|
|
if apiErr != nil {
|
|
return time.Time{}, apiErr
|
|
}
|
|
val := bytesToString(sliceHeader(JSSchedulePattern, hdr))
|
|
schedule, _, ok := parseMsgSchedule(val, loc, ts)
|
|
if !ok {
|
|
return time.Time{}, NewJSMessageSchedulesPatternInvalidError()
|
|
}
|
|
return schedule, nil
|
|
}
|
|
|
|
// loadMessageScheduleLocation returns the *time.Location for the schedule's
|
|
// time zone header. Returns nil loc when the header is absent. A header that
|
|
// is present but empty or names an unknown zone yields a TimeZoneInvalid error.
|
|
func loadMessageScheduleLocation(hdr []byte) (*time.Location, *ApiError) {
|
|
tz := sliceHeader(JSScheduleTimeZone, hdr)
|
|
if tz == nil {
|
|
return nil, nil
|
|
}
|
|
if len(tz) == 0 {
|
|
return nil, NewJSMessageSchedulesTimeZoneInvalidError()
|
|
}
|
|
loc, err := time.LoadLocation(bytesToString(tz))
|
|
if err != nil {
|
|
return nil, NewJSMessageSchedulesTimeZoneInvalidError()
|
|
}
|
|
return loc, nil
|
|
}
|
|
|
|
// Fast lookup of the message schedule TTL from headers.
|
|
// The TTL is confirmed to be valid, but the raw TTL string is returned.
|
|
func getMessageScheduleTTL(hdr []byte) (string, bool) {
|
|
ttl := getHeader(JSScheduleTTL, hdr)
|
|
if len(ttl) == 0 {
|
|
return _EMPTY_, true
|
|
}
|
|
if _, err := parseMessageTTL(bytesToString(ttl)); err != nil {
|
|
return _EMPTY_, false
|
|
}
|
|
return string(ttl), true
|
|
}
|
|
|
|
// Fast lookup of the message schedule rollup from headers.
|
|
func getMessageScheduleRollup(hdr []byte) string {
|
|
return string(sliceHeader(JSScheduleRollup, hdr))
|
|
}
|
|
|
|
// Fast lookup of message schedule target.
|
|
func getMessageScheduleTarget(hdr []byte) string {
|
|
if len(hdr) == 0 {
|
|
return _EMPTY_
|
|
}
|
|
return string(getHeader(JSScheduleTarget, hdr))
|
|
}
|
|
|
|
// Fast lookup of message schedule source.
|
|
func getMessageScheduleSource(hdr []byte) string {
|
|
if len(hdr) == 0 {
|
|
return _EMPTY_
|
|
}
|
|
return string(getHeader(JSScheduleSource, hdr))
|
|
}
|
|
|
|
// Fast lookup of message scheduler.
|
|
func getMessageScheduler(hdr []byte) string {
|
|
if len(hdr) == 0 {
|
|
return _EMPTY_
|
|
}
|
|
return string(getHeader(JSScheduler, hdr))
|
|
}
|
|
|
|
// Fast lookup of batch ID.
|
|
func getBatchId(hdr []byte) string {
|
|
if len(hdr) == 0 {
|
|
return _EMPTY_
|
|
}
|
|
if atomicBatchId := sliceHeader(JSBatchId, hdr); atomicBatchId != nil {
|
|
return string(atomicBatchId)
|
|
}
|
|
return _EMPTY_
|
|
}
|
|
|
|
type FastBatch struct {
|
|
id string
|
|
seq uint64
|
|
flow uint16
|
|
ping bool
|
|
gapOk bool
|
|
commit bool
|
|
commitEob bool
|
|
}
|
|
|
|
const (
|
|
FastBatchSuffix = ".$FI"
|
|
FastBatchGapFail = "fail"
|
|
FastBatchGapOk = "ok"
|
|
)
|
|
|
|
const (
|
|
FastBatchOpStart = iota
|
|
FastBatchOpAppend
|
|
FastBatchOpCommit
|
|
FastBatchOpCommitEob
|
|
FastBatchOpPing
|
|
)
|
|
|
|
var fastBatchPool sync.Pool
|
|
|
|
func getFastBatchFromPool() *FastBatch {
|
|
idx := fastBatchPool.Get()
|
|
if idx != nil {
|
|
return idx.(*FastBatch)
|
|
}
|
|
return new(FastBatch)
|
|
}
|
|
|
|
func (b *FastBatch) returnToPool() {
|
|
if b == nil {
|
|
return
|
|
}
|
|
// Nil out all values.
|
|
*b = FastBatch{}
|
|
fastBatchPool.Put(b)
|
|
}
|
|
|
|
// getFastBatch gets fast batch info from the reply subject in the form:
|
|
// <prefix>.<uuid>.<initial flow>.<gap mode>.<batch seq>.<operation>.$FI
|
|
func getFastBatch(reply string, hdr []byte) (*FastBatch, bool) {
|
|
lreply := len(reply)
|
|
if lreply <= 4 || reply[lreply-4:] != FastBatchSuffix {
|
|
if !isServiceReply(stringToBytes(reply)) {
|
|
return nil, false
|
|
}
|
|
// If account imports/exports are used, the reply might be internal.
|
|
// Check the client header for the original reply subject.
|
|
ci := sliceHeader(ClientInfoHdr, hdr)
|
|
if ci == nil {
|
|
return nil, false
|
|
}
|
|
var cis ClientInfo
|
|
if err := json.Unmarshal(ci, &cis); err != nil || cis.Reply == _EMPTY_ {
|
|
return nil, false
|
|
}
|
|
reply = cis.Reply
|
|
lreply = len(reply)
|
|
if lreply <= 4 || reply[lreply-4:] != FastBatchSuffix {
|
|
return nil, false
|
|
}
|
|
}
|
|
|
|
n := lreply - 4 // Move to just before the dot
|
|
o := strings.LastIndexByte(reply[:n], '.')
|
|
if o == -1 {
|
|
return nil, true
|
|
}
|
|
// Batch operation.
|
|
ops := reply[o+1 : n]
|
|
op := parseInt64(stringToBytes(ops))
|
|
if op < FastBatchOpStart || op > FastBatchOpPing {
|
|
return nil, true
|
|
}
|
|
|
|
b := getFastBatchFromPool()
|
|
b.ping = op == FastBatchOpPing
|
|
b.commitEob = op == FastBatchOpCommitEob
|
|
b.commit = b.commitEob || op == FastBatchOpCommit
|
|
p := o
|
|
|
|
// Batch seq.
|
|
if o = strings.LastIndexByte(reply[:o], '.'); o == -1 {
|
|
return nil, true
|
|
}
|
|
a := parseInt64(stringToBytes(reply[o+1 : p]))
|
|
if a < 1 {
|
|
return nil, true
|
|
}
|
|
b.seq = uint64(a)
|
|
p = o
|
|
if b.seq <= 0 {
|
|
return nil, true
|
|
} else if b.seq == 1 && b.commitEob {
|
|
return nil, true
|
|
}
|
|
if op == FastBatchOpStart && b.seq != 1 {
|
|
return nil, true
|
|
} else if op == FastBatchOpAppend && b.seq <= 1 {
|
|
return nil, true
|
|
}
|
|
|
|
// Gap mode.
|
|
if o = strings.LastIndexByte(reply[:o], '.'); o == -1 {
|
|
return nil, true
|
|
}
|
|
gapMode := reply[o+1 : p]
|
|
if gapMode != FastBatchGapFail && gapMode != FastBatchGapOk {
|
|
return nil, true // Not recognized.
|
|
}
|
|
b.gapOk = gapMode == FastBatchGapOk
|
|
p = o
|
|
|
|
// Ack flow.
|
|
if o = strings.LastIndexByte(reply[:o], '.'); o == -1 {
|
|
return nil, true
|
|
}
|
|
a = parseInt64(stringToBytes(reply[o+1 : p]))
|
|
if a <= 0 {
|
|
a = 10
|
|
} else if a > math.MaxUint16 {
|
|
a = math.MaxUint16
|
|
}
|
|
b.flow = uint16(a)
|
|
p = o
|
|
|
|
// Batch id.
|
|
if o = strings.LastIndexByte(reply[:o], '.'); o == -1 {
|
|
return nil, true
|
|
}
|
|
b.id = reply[o+1 : p]
|
|
return b, false
|
|
}
|
|
|
|
// Fast lookup of batch sequence.
|
|
func getBatchSequence(hdr []byte) (uint64, bool) {
|
|
bseq := sliceHeader(JSBatchSeq, hdr)
|
|
if len(bseq) == 0 {
|
|
return 0, false
|
|
}
|
|
return uint64(parseInt64(bseq)), true
|
|
}
|
|
|
|
// Signal if we are clustered. Will acquire rlock.
|
|
func (mset *stream) IsClustered() bool {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
return mset.isClustered()
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) isClustered() bool {
|
|
return mset.node != nil
|
|
}
|
|
|
|
// Used if we have to queue things internally to avoid the route/gw path.
|
|
type inMsg struct {
|
|
subj string
|
|
rply string
|
|
hdr []byte
|
|
msg []byte
|
|
si *sourceInfo
|
|
mt *msgTrace
|
|
seq uint64 // seq that can be optionally used for sorting out-of-band.
|
|
}
|
|
|
|
var inMsgPool = sync.Pool{
|
|
New: func() any {
|
|
return &inMsg{}
|
|
},
|
|
}
|
|
|
|
func (im *inMsg) returnToPool() {
|
|
im.subj, im.rply, im.hdr, im.msg, im.si, im.mt = _EMPTY_, _EMPTY_, nil, nil, nil, nil
|
|
inMsgPool.Put(im)
|
|
}
|
|
|
|
func (mset *stream) queueInbound(ib *ipQueue[*inMsg], subj, rply string, hdr, msg []byte, si *sourceInfo, mt *msgTrace) {
|
|
im := inMsgPool.Get().(*inMsg)
|
|
im.subj, im.rply, im.hdr, im.msg, im.si, im.mt = subj, rply, hdr, msg, si, mt
|
|
if _, err := ib.push(im); err != nil {
|
|
im.returnToPool()
|
|
streamName := mset.cfg.Name
|
|
mset.srv.RateLimitWarnf("Dropping messages due to excessive stream ingest rate on '%s' > '%s': %s", mset.acc.Name, streamName, err)
|
|
if rply != _EMPTY_ {
|
|
hdr := []byte("NATS/1.0 429 Too Many Requests\r\n\r\n")
|
|
b, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: streamName}, Error: NewJSStreamTooManyRequestsError()})
|
|
mset.outq.send(newJSPubMsg(rply, _EMPTY_, _EMPTY_, hdr, b, nil, 0))
|
|
}
|
|
}
|
|
}
|
|
|
|
var dgPool = sync.Pool{
|
|
New: func() any {
|
|
return &directGetReq{}
|
|
},
|
|
}
|
|
|
|
// For when we need to not inline the request.
|
|
type directGetReq struct {
|
|
// Copy of this is correct for this.
|
|
req JSApiMsgGetRequest
|
|
reply string
|
|
}
|
|
|
|
// processDirectGetRequest handles direct get request for stream messages.
|
|
func (mset *stream) processDirectGetRequest(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
|
|
if len(reply) == 0 {
|
|
return
|
|
}
|
|
hdr, msg := c.msgParts(rmsg)
|
|
if errorOnRequiredApiLevel(hdr) {
|
|
hdr := []byte("NATS/1.0 412 Required Api Level\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
if len(msg) == 0 {
|
|
hdr := []byte("NATS/1.0 408 Empty Request\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
var req JSApiMsgGetRequest
|
|
err := json.Unmarshal(msg, &req)
|
|
if err != nil {
|
|
hdr := []byte("NATS/1.0 408 Malformed Request\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
// Check if nothing set.
|
|
if req.Seq == 0 && req.LastFor == _EMPTY_ && req.NextFor == _EMPTY_ && len(req.MultiLastFor) == 0 && req.StartTime == nil {
|
|
hdr := []byte("NATS/1.0 408 Empty Request\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
// Check we don't have conflicting options set.
|
|
// We do not allow batch mode for lastFor requests.
|
|
if (req.Seq > 0 && req.LastFor != _EMPTY_) ||
|
|
(req.Seq > 0 && req.StartTime != nil) ||
|
|
(req.StartTime != nil && req.LastFor != _EMPTY_) ||
|
|
(req.LastFor != _EMPTY_ && req.NextFor != _EMPTY_) ||
|
|
(req.LastFor != _EMPTY_ && req.Batch > 0) ||
|
|
(req.LastFor != _EMPTY_ && len(req.MultiLastFor) > 0) ||
|
|
(req.NextFor != _EMPTY_ && len(req.MultiLastFor) > 0) ||
|
|
(req.UpToSeq > 0 && req.UpToTime != nil) {
|
|
hdr := []byte("NATS/1.0 408 Bad Request\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
|
|
inlineOk := c.kind != ROUTER && c.kind != GATEWAY && c.kind != LEAF
|
|
if !inlineOk {
|
|
dg := dgPool.Get().(*directGetReq)
|
|
dg.req, dg.reply = req, reply
|
|
mset.gets.push(dg)
|
|
} else {
|
|
mset.getDirectRequest(&req, reply)
|
|
}
|
|
}
|
|
|
|
// This is for direct get by last subject which is part of the subject itself.
|
|
func (mset *stream) processDirectGetLastBySubjectRequest(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
|
|
if len(reply) == 0 {
|
|
return
|
|
}
|
|
hdr, msg := c.msgParts(rmsg)
|
|
if errorOnRequiredApiLevel(hdr) {
|
|
hdr := []byte("NATS/1.0 412 Required Api Level\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
|
|
var req JSApiMsgGetRequest
|
|
if len(msg) > 0 {
|
|
err := json.Unmarshal(msg, &req)
|
|
if err != nil {
|
|
hdr := []byte("NATS/1.0 408 Malformed Request\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
}
|
|
|
|
// Extract the key.
|
|
var key string
|
|
for i, n := 0, 0; i < len(subject); i++ {
|
|
if subject[i] == btsep {
|
|
if n == 4 {
|
|
if start := i + 1; start < len(subject) {
|
|
key = subject[i+1:]
|
|
}
|
|
break
|
|
}
|
|
n++
|
|
}
|
|
}
|
|
if len(key) == 0 {
|
|
hdr := []byte("NATS/1.0 408 Bad Request\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
|
|
// Error if request fields are provided that don't make sense for this endpoint.
|
|
switch {
|
|
case req.LastFor != _EMPTY_, req.NextFor != _EMPTY_,
|
|
len(req.MultiLastFor) > 0, req.Seq != 0, req.Batch != 0,
|
|
req.StartTime != nil, req.UpToSeq != 0, req.UpToTime != nil:
|
|
hdr := []byte("NATS/1.0 408 Bad Request\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
default:
|
|
req.LastFor = key
|
|
}
|
|
|
|
inlineOk := c.kind != ROUTER && c.kind != GATEWAY && c.kind != LEAF
|
|
if !inlineOk {
|
|
dg := dgPool.Get().(*directGetReq)
|
|
dg.req, dg.reply = req, reply
|
|
mset.gets.push(dg)
|
|
} else {
|
|
mset.getDirectRequest(&req, reply)
|
|
}
|
|
}
|
|
|
|
// For direct get batch and multi requests.
|
|
const (
|
|
dg = "NATS/1.0\r\nNats-Stream: %s\r\nNats-Subject: %s\r\nNats-Sequence: %d\r\nNats-Time-Stamp: %s\r\n\r\n"
|
|
dgb = "NATS/1.0\r\nNats-Stream: %s\r\nNats-Subject: %s\r\nNats-Sequence: %d\r\nNats-Time-Stamp: %s\r\nNats-Num-Pending: %d\r\nNats-Last-Sequence: %d\r\n\r\n"
|
|
eob = "NATS/1.0 204 EOB\r\nNats-Num-Pending: %d\r\nNats-Last-Sequence: %d\r\n\r\n"
|
|
eobm = "NATS/1.0 204 EOB\r\nNats-Num-Pending: %d\r\nNats-Last-Sequence: %d\r\nNats-UpTo-Sequence: %d\r\n\r\n"
|
|
)
|
|
|
|
// Handle a multi request.
|
|
func (mset *stream) getDirectMulti(req *JSApiMsgGetRequest, reply string) {
|
|
// TODO(dlc) - Make configurable?
|
|
const maxAllowedResponses = 1024
|
|
|
|
// Ensure this read request is isolated and doesn't interleave with writes.
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
|
|
// Grab store and name.
|
|
store, name, s := mset.store, mset.cfg.Name, mset.srv
|
|
|
|
// Grab MaxBytes
|
|
mb := req.MaxBytes
|
|
if mb == 0 && s != nil {
|
|
// Fill in with the server's MaxPending.
|
|
mb = int(s.opts.MaxPending)
|
|
}
|
|
|
|
upToSeq := req.UpToSeq
|
|
// If we have UpToTime set get the proper sequence.
|
|
if req.UpToTime != nil {
|
|
upToSeq = store.GetSeqFromTime((*req.UpToTime).UTC())
|
|
// Avoid selecting a first sequence that will take us to before the stream first
|
|
// sequence, otherwise we can return messages after the supplied UpToTime.
|
|
if upToSeq <= mset.state().FirstSeq {
|
|
hdr := []byte("NATS/1.0 404 No Results\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
// We need to back off one since this is used to determine start sequence normally,
|
|
// whereas here we want it to be the ceiling.
|
|
upToSeq--
|
|
}
|
|
// If not set, set to the last sequence and remember that for EOB.
|
|
if upToSeq == 0 {
|
|
var state StreamState
|
|
mset.store.FastState(&state)
|
|
upToSeq = state.LastSeq
|
|
}
|
|
|
|
seqs, err := store.MultiLastSeqs(req.MultiLastFor, upToSeq, maxAllowedResponses)
|
|
if err != nil {
|
|
var hdr []byte
|
|
if err == ErrTooManyResults {
|
|
hdr = []byte("NATS/1.0 413 Too Many Results\r\n\r\n")
|
|
} else {
|
|
hdr = []byte(fmt.Sprintf("NATS/1.0 500 %v\r\n\r\n", err))
|
|
}
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
if len(seqs) == 0 {
|
|
hdr := []byte("NATS/1.0 404 No Results\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
|
|
np, lseq, sentBytes, sent := uint64(len(seqs)), uint64(0), 0, 0
|
|
for _, seq := range seqs {
|
|
if seq < req.Seq {
|
|
if np > 0 {
|
|
np--
|
|
}
|
|
continue
|
|
}
|
|
var svp StoreMsg
|
|
sm, err := store.LoadMsg(seq, &svp)
|
|
if err != nil {
|
|
hdr := []byte("NATS/1.0 404 Message Not Found\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
|
|
hdr := sm.hdr
|
|
ts := time.Unix(0, sm.ts).UTC()
|
|
|
|
// Decrement num pending. This is an optimization, and we do not continue to look it up for these operations.
|
|
if np > 0 {
|
|
np--
|
|
}
|
|
if len(hdr) == 0 {
|
|
hdr = fmt.Appendf(nil, dgb, name, sm.subj, sm.seq, ts.Format(time.RFC3339Nano), np, lseq)
|
|
} else {
|
|
hdr = copyBytes(hdr)
|
|
hdr = genHeader(hdr, JSStream, name)
|
|
hdr = genHeader(hdr, JSSubject, sm.subj)
|
|
hdr = genHeader(hdr, JSSequence, strconv.FormatUint(sm.seq, 10))
|
|
hdr = genHeader(hdr, JSTimeStamp, ts.Format(time.RFC3339Nano))
|
|
hdr = genHeader(hdr, JSNumPending, strconv.FormatUint(np, 10))
|
|
hdr = genHeader(hdr, JSLastSequence, strconv.FormatUint(lseq, 10))
|
|
}
|
|
// Track our lseq
|
|
lseq = sm.seq
|
|
// Send out our message.
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, sm.msg, nil, 0))
|
|
// Check if we have exceeded max bytes.
|
|
sentBytes += len(sm.subj) + len(sm.hdr) + len(sm.msg)
|
|
if sentBytes >= mb {
|
|
break
|
|
}
|
|
sent++
|
|
if req.Batch > 0 && sent >= req.Batch {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Send out EOB
|
|
hdr := fmt.Appendf(nil, eobm, np, lseq, upToSeq)
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
}
|
|
|
|
// Do actual work on a direct msg request.
|
|
// This could be called in a Go routine if we are inline for a non-client connection.
|
|
func (mset *stream) getDirectRequest(req *JSApiMsgGetRequest, reply string) {
|
|
// Handle multi in separate function.
|
|
if len(req.MultiLastFor) > 0 {
|
|
mset.getDirectMulti(req, reply)
|
|
return
|
|
}
|
|
|
|
// Ensure this read request is isolated and doesn't interleave with writes.
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
|
|
store, name, s := mset.store, mset.cfg.Name, mset.srv
|
|
|
|
var seq uint64
|
|
// Lookup start seq if AsOfTime is set.
|
|
if req.StartTime != nil {
|
|
seq = store.GetSeqFromTime(*req.StartTime)
|
|
} else {
|
|
seq = req.Seq
|
|
}
|
|
|
|
wc := subjectHasWildcard(req.NextFor)
|
|
// For tracking num pending if we are batch.
|
|
var np, lseq, validThrough uint64
|
|
var isBatchRequest bool
|
|
batch := req.Batch
|
|
if batch == 0 {
|
|
batch = 1
|
|
} else {
|
|
// This is a batch request, capture initial numPending.
|
|
isBatchRequest = true
|
|
var err error
|
|
if np, validThrough, err = store.NumPending(seq, req.NextFor, false); err != nil {
|
|
return
|
|
}
|
|
}
|
|
|
|
// Grab MaxBytes
|
|
mb := req.MaxBytes
|
|
if mb == 0 && s != nil {
|
|
// Fill in with the server's MaxPending.
|
|
mb = int(s.opts.MaxPending)
|
|
}
|
|
// Track what we have sent.
|
|
var sentBytes int
|
|
|
|
// Loop over batch, which defaults to 1.
|
|
for i := 0; i < batch; i++ {
|
|
var (
|
|
svp StoreMsg
|
|
sm *StoreMsg
|
|
err error
|
|
)
|
|
if seq > 0 && req.NextFor == _EMPTY_ {
|
|
// Only do direct lookup for a non batch.
|
|
if i == 0 && !isBatchRequest {
|
|
sm, err = store.LoadMsg(seq, &svp)
|
|
} else {
|
|
// We want to use load next with fwcs to step over deleted msgs.
|
|
sm, seq, err = store.LoadNextMsg(fwcs, true, seq, &svp)
|
|
}
|
|
// Bump for next loop if applicable.
|
|
seq++
|
|
} else if req.NextFor != _EMPTY_ {
|
|
sm, seq, err = store.LoadNextMsg(req.NextFor, wc, seq, &svp)
|
|
seq++
|
|
} else {
|
|
// Batch is not applicable here, this is checked before we get here.
|
|
sm, err = store.LoadLastMsg(req.LastFor, &svp)
|
|
}
|
|
if err != nil {
|
|
// For batches, if we stop early we want to do EOB logic below.
|
|
if batch > 1 && i > 0 {
|
|
break
|
|
}
|
|
hdr := []byte("NATS/1.0 404 Message Not Found\r\n\r\n")
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
return
|
|
}
|
|
|
|
ts := time.Unix(0, sm.ts).UTC()
|
|
var hdr []byte
|
|
if !req.NoHeaders {
|
|
hdr = sm.hdr
|
|
if isBatchRequest {
|
|
// Decrement num pending. This is an optimization, and we do not continue to look it up for these operations.
|
|
if np > 0 {
|
|
np--
|
|
}
|
|
if len(hdr) == 0 {
|
|
hdr = fmt.Appendf(nil, dgb, name, sm.subj, sm.seq, ts.Format(time.RFC3339Nano), np, lseq)
|
|
} else {
|
|
hdr = copyBytes(hdr)
|
|
hdr = genHeader(hdr, JSStream, name)
|
|
hdr = genHeader(hdr, JSSubject, sm.subj)
|
|
hdr = genHeader(hdr, JSSequence, strconv.FormatUint(sm.seq, 10))
|
|
hdr = genHeader(hdr, JSTimeStamp, ts.Format(time.RFC3339Nano))
|
|
hdr = genHeader(hdr, JSNumPending, strconv.FormatUint(np, 10))
|
|
hdr = genHeader(hdr, JSLastSequence, strconv.FormatUint(lseq, 10))
|
|
}
|
|
} else {
|
|
if len(hdr) == 0 {
|
|
hdr = fmt.Appendf(nil, dg, name, sm.subj, sm.seq, ts.Format(time.RFC3339Nano))
|
|
} else {
|
|
hdr = copyBytes(hdr)
|
|
hdr = genHeader(hdr, JSStream, name)
|
|
hdr = genHeader(hdr, JSSubject, sm.subj)
|
|
hdr = genHeader(hdr, JSSequence, strconv.FormatUint(sm.seq, 10))
|
|
hdr = genHeader(hdr, JSTimeStamp, ts.Format(time.RFC3339Nano))
|
|
}
|
|
}
|
|
}
|
|
// Track our lseq
|
|
lseq = sm.seq
|
|
// Send out our message.
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, sm.msg, nil, 0))
|
|
// Check if we have exceeded max bytes.
|
|
sentBytes += len(sm.subj) + len(sm.hdr) + len(sm.msg)
|
|
if sentBytes >= mb {
|
|
break
|
|
}
|
|
}
|
|
|
|
// If batch was requested send EOB.
|
|
if isBatchRequest {
|
|
// Update if the stream's last sequence has moved past our validThrough.
|
|
if mset.lseq > validThrough {
|
|
var err error
|
|
if np, _, err = store.NumPending(seq, req.NextFor, false); err != nil {
|
|
return
|
|
}
|
|
}
|
|
hdr := fmt.Appendf(nil, eob, np, lseq)
|
|
mset.outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, hdr, nil, nil, 0))
|
|
}
|
|
}
|
|
|
|
// processInboundJetStreamMsg handles processing messages bound for a stream.
|
|
func (mset *stream) processInboundJetStreamMsg(_ *subscription, c *client, _ *Account, subject, reply string, rmsg []byte) {
|
|
hdr, msg := c.msgParts(copyBytes(rmsg)) // Need to copy.
|
|
hdr = removeHeaderStatusIfPresent(hdr)
|
|
if mt, traceOnly := c.isMsgTraceEnabled(); mt != nil {
|
|
// If message is delivered, we need to disable the message trace headers
|
|
// to prevent a trace event to be generated when a stored message
|
|
// is delivered to a consumer and routed.
|
|
if !traceOnly {
|
|
hdr = setHeader(MsgTraceDest, MsgTraceDestDisabled, hdr)
|
|
}
|
|
// This will add the jetstream event while in the client read loop.
|
|
// Since the event will be updated in a different go routine, the
|
|
// tracing object will have a separate reference to the JS trace
|
|
// object.
|
|
mt.addJetStreamEvent(mset.name())
|
|
}
|
|
mset.queueInbound(mset.msgs, subject, reply, hdr, msg, nil, c.pa.trace)
|
|
}
|
|
|
|
var (
|
|
errLastSeqMismatch = errors.New("last sequence mismatch")
|
|
errMsgIdDuplicate = errors.New("msgid is duplicate")
|
|
errStreamClosed = errors.New("stream closed")
|
|
errInvalidMsgHandler = errors.New("undefined message handler")
|
|
errStreamMismatch = errors.New("expected stream does not match")
|
|
errMsgTTLDisabled = errors.New("message TTL disabled")
|
|
)
|
|
|
|
// processJetStreamMsg is where we try to actually process the stream msg.
|
|
func (mset *stream) processJetStreamMsg(subject, reply string, hdr, msg []byte, lseq uint64, ts int64, mt *msgTrace, sourced bool, needLock bool) error {
|
|
return mset.processJetStreamMsgWithBatch(subject, reply, hdr, msg, lseq, ts, mt, sourced, needLock, nil)
|
|
}
|
|
|
|
func (mset *stream) processJetStreamMsgWithBatch(subject, reply string, hdr, msg []byte, lseq uint64, ts int64, mt *msgTrace, sourced bool, needLock bool, fastBatch *FastBatch) (retErr error) {
|
|
if mt != nil {
|
|
// Only the leader/standalone will have mt!=nil. On exit, send the
|
|
// message trace event.
|
|
defer func() {
|
|
mt.sendEventFromJetStream(retErr)
|
|
}()
|
|
}
|
|
|
|
if mset == nil || mset.closed.Load() {
|
|
return errStreamClosed
|
|
}
|
|
|
|
// Hold lock while storing the message, and potentially purging as part of a rollup.
|
|
// If we're writing an atomic batch of multiple messages, the lock is already held.
|
|
if needLock {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
}
|
|
|
|
s, store := mset.srv, mset.store
|
|
|
|
traceOnly := mt.traceOnly()
|
|
bumpCLFS := func() {
|
|
// Do not bump if tracing and not doing message delivery.
|
|
if traceOnly {
|
|
return
|
|
}
|
|
mset.clMu.Lock()
|
|
mset.clfs++
|
|
mset.clMu.Unlock()
|
|
}
|
|
|
|
// Apply the input subject transform if any
|
|
if mset.itr != nil {
|
|
ts, err := mset.itr.Match(subject)
|
|
if err == nil {
|
|
// no filtering: if the subject doesn't map the source of the transform, don't change it
|
|
subject = ts
|
|
}
|
|
}
|
|
|
|
var accName string
|
|
if mset.acc != nil {
|
|
accName = mset.acc.Name
|
|
}
|
|
|
|
js, jsa, doAck := mset.js, mset.jsa, !mset.cfg.NoAck
|
|
name, stype := mset.cfg.Name, mset.cfg.Storage
|
|
maxMsgSize := int(mset.cfg.MaxMsgSize)
|
|
numConsumers := len(mset.consumers)
|
|
interestRetention := mset.cfg.Retention == InterestPolicy
|
|
allowMsgCounter, allowMsgSchedules := mset.cfg.AllowMsgCounter, mset.cfg.AllowMsgSchedules
|
|
allowRollupPurge := mset.cfg.AllowRollup && !mset.cfg.DenyPurge
|
|
// Snapshot if we are the leader and if we can respond.
|
|
isLeader, isSealed := mset.isLeaderNodeState(), mset.cfg.Sealed
|
|
isClustered, isMirror := mset.isClustered(), mset.cfg.Mirror != nil
|
|
canConsistencyCheck := !isClustered || traceOnly
|
|
canRespond := doAck && len(reply) > 0 && isLeader
|
|
outq := mset.outq
|
|
|
|
var resp = &JSPubAckResponse{}
|
|
|
|
var (
|
|
batchId string
|
|
batchSeq uint64
|
|
)
|
|
// Populate batch details.
|
|
if fastBatch != nil {
|
|
// For R1 we can reuse without regenerating.
|
|
batchId, batchSeq = fastBatch.id, fastBatch.seq
|
|
// Disable consistency checking if this was already done
|
|
// earlier as part of the batch consistency check.
|
|
canConsistencyCheck = traceOnly
|
|
} else if fastBatch, _ = getFastBatch(reply, hdr); fastBatch != nil {
|
|
defer fastBatch.returnToPool()
|
|
batchId, batchSeq = fastBatch.id, fastBatch.seq
|
|
// Disable consistency checking if this was already done
|
|
// earlier as part of the batch consistency check.
|
|
canConsistencyCheck = traceOnly
|
|
}
|
|
if len(hdr) > 0 && batchId == _EMPTY_ {
|
|
if batchId = getBatchId(hdr); batchId != _EMPTY_ {
|
|
batchSeq, _ = getBatchSequence(hdr)
|
|
// Disable consistency checking if this was already done
|
|
// earlier as part of the batch consistency check.
|
|
canConsistencyCheck = traceOnly
|
|
}
|
|
}
|
|
|
|
// Bail here if sealed.
|
|
if canConsistencyCheck && isSealed {
|
|
if canRespond && outq != nil {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = ApiErrors[JSStreamSealedErr]
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return ApiErrors[JSStreamSealedErr]
|
|
}
|
|
|
|
var buf [256]byte
|
|
pubAck := append(buf[:0], mset.pubAck...)
|
|
|
|
// If this is a non-clustered msg and we are not considered active, meaning no active subscription, do not process.
|
|
if lseq == 0 && ts == 0 && !mset.active {
|
|
return nil
|
|
}
|
|
|
|
// For clustering the lower layers will pass our expected lseq. If it is present check for that here.
|
|
var clfs uint64
|
|
if lseq > 0 {
|
|
clfs = mset.getCLFS()
|
|
if lseq != (mset.lseq + clfs) {
|
|
isMisMatch := true
|
|
// We may be able to recover here if we have no state whatsoever, or we are a mirror.
|
|
// See if we have to adjust our starting sequence.
|
|
if mset.lseq == 0 || isMirror {
|
|
var state StreamState
|
|
mset.store.FastState(&state)
|
|
if state.FirstSeq == 0 {
|
|
if _, err := mset.store.Compact(lseq + 1); err != nil {
|
|
return err
|
|
}
|
|
mset.lseq = lseq
|
|
isMisMatch = false
|
|
}
|
|
}
|
|
// Really is a mismatch.
|
|
if isMisMatch {
|
|
if canRespond && outq != nil {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = ApiErrors[JSStreamSequenceNotMatchErr]
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return errLastSeqMismatch
|
|
}
|
|
}
|
|
}
|
|
|
|
// If we have received this message across an account we may have request information attached.
|
|
// For now remove. TODO(dlc) - Should this be opt-in or opt-out?
|
|
if len(hdr) > 0 {
|
|
hdr = removeHeaderIfPresent(hdr, ClientInfoHdr)
|
|
}
|
|
|
|
// Process additional msg headers if still present.
|
|
var msgId string
|
|
var incr *big.Int
|
|
var rollupSub, rollupAll bool
|
|
|
|
if len(hdr) > 0 {
|
|
// Certain checks have already been performed if in clustered mode, so only check if not.
|
|
// Note, for cluster mode but with message tracing (without message delivery), we need
|
|
// to do this check here since it was not done in processClusteredInboundMsg().
|
|
if canConsistencyCheck {
|
|
// 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()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return 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()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
} else if len(msg) > 0 {
|
|
apiErr := NewJSMessageIncrPayloadError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return 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()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
}
|
|
}
|
|
}
|
|
|
|
// Expected stream.
|
|
if sname := getExpectedStream(hdr); sname != _EMPTY_ && sname != name {
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSStreamNotMatchError()
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return errStreamMismatch
|
|
}
|
|
|
|
// TTL'd messages are rejected entirely if TTLs are not enabled on the stream.
|
|
// Shouldn't happen in clustered mode since we should have already caught this
|
|
// in processClusteredInboundMsg, but needed here for non-clustered etc.
|
|
if ttl, _ := getMessageTTL(hdr); !sourced && ttl != 0 && !mset.cfg.AllowMsgTTL {
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSMessageTTLDisabledError()
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return errMsgTTLDisabled
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// TODO(dlc) - We could make a new store func that does this all in one.
|
|
var smv StoreMsg
|
|
var fseq uint64
|
|
sm, err := store.LoadLastMsg(seqSubj, &smv)
|
|
if sm != nil {
|
|
fseq = sm.seq
|
|
}
|
|
if err == ErrStoreMsgNotFound && seq == 0 {
|
|
fseq, err = 0, nil
|
|
}
|
|
if err != nil || fseq != seq {
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSStreamWrongLastSequenceError(fseq)
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return fmt.Errorf("last sequence by subject mismatch: %d vs %d", seq, fseq)
|
|
}
|
|
} else if getExpectedLastSeqPerSubjectForSubject(hdr) != _EMPTY_ {
|
|
apiErr := NewJSStreamExpectedLastSeqPerSubjectInvalidError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
}
|
|
|
|
// Expected last sequence.
|
|
if seq, exists := getExpectedLastSeq(hdr); exists && seq != mset.lseq {
|
|
mlseq := mset.lseq
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSStreamWrongLastSequenceError(mlseq)
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return fmt.Errorf("last sequence mismatch: %d vs %d", seq, mlseq)
|
|
}
|
|
|
|
// 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()
|
|
}
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
} else if !schedule.IsZero() {
|
|
if !allowMsgSchedules {
|
|
apiErr := NewJSMessageSchedulesDisabledError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
} else if scheduleTtl, ok := getMessageScheduleTTL(hdr); !ok {
|
|
apiErr := NewJSMessageSchedulesTTLInvalidError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
} else if scheduleRollup := getMessageScheduleRollup(hdr); scheduleRollup != _EMPTY_ && scheduleRollup != JSMsgRollupSubject {
|
|
apiErr := NewJSMessageSchedulesRollupInvalidError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
} else if scheduleTtl != _EMPTY_ && !mset.cfg.AllowMsgTTL {
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSMessageTTLDisabledError()
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return errMsgTTLDisabled
|
|
} else if scheduleTarget := getMessageScheduleTarget(hdr); scheduleTarget == _EMPTY_ ||
|
|
!IsValidPublishSubject(scheduleTarget) || scheduleTarget == subject {
|
|
apiErr := NewJSMessageSchedulesTargetInvalidError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
} else if scheduleSource := getMessageScheduleSource(hdr); scheduleSource != _EMPTY_ &&
|
|
(scheduleSource == scheduleTarget || scheduleSource == subject || !IsValidPublishSubject(scheduleSource)) {
|
|
apiErr := NewJSMessageSchedulesSourceInvalidError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
} else {
|
|
match := slices.ContainsFunc(mset.cfg.Subjects, func(subj string) bool {
|
|
return SubjectsCollide(subj, scheduleTarget)
|
|
})
|
|
if !match {
|
|
apiErr := NewJSMessageSchedulesTargetInvalidError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
}
|
|
if scheduleSource != _EMPTY_ {
|
|
match = slices.ContainsFunc(mset.cfg.Subjects, func(subj string) bool {
|
|
return SubjectsCollide(subj, scheduleSource)
|
|
})
|
|
if !match {
|
|
apiErr := NewJSMessageSchedulesSourceInvalidError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
}
|
|
}
|
|
|
|
// 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()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return 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()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return 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()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
} else if !allowMsgSchedules {
|
|
apiErr := NewJSMessageSchedulesDisabledError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
}
|
|
} else if !sourced && len(sliceHeader(JSScheduler, hdr)) > 0 {
|
|
// Clients may only use Nats-Scheduler alongside Nats-Schedule-Next.
|
|
apiErr := NewJSMessageSchedulesSchedulerInvalidError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
}
|
|
}
|
|
|
|
// Dedupe detection. This is done at the cluster level for dedupe detection above the
|
|
// lower layers. But we still need to pull out the msgId.
|
|
if msgId = getMsgId(hdr); msgId != _EMPTY_ {
|
|
// Do real check only if not clustered or traceOnly flag is set.
|
|
// If we're mirroring we can't deduplicate on our own.
|
|
if canConsistencyCheck && !isMirror {
|
|
var seq uint64
|
|
mset.ddMu.Lock()
|
|
dde := mset.checkMsgId(msgId)
|
|
if dde != nil {
|
|
seq = dde.seq
|
|
}
|
|
mset.ddMu.Unlock()
|
|
if seq > 0 {
|
|
if canRespond {
|
|
response := append(pubAck, strconv.FormatUint(seq, 10)...)
|
|
response = append(response, ",\"duplicate\": true}"...)
|
|
outq.sendMsg(reply, response)
|
|
}
|
|
return errMsgIdDuplicate
|
|
}
|
|
}
|
|
}
|
|
|
|
// Expected last msgId.
|
|
if lmsgId := getExpectedLastMsgId(hdr); lmsgId != _EMPTY_ {
|
|
if lmsgId != mset.lmsgId {
|
|
last := mset.lmsgId
|
|
bumpCLFS()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSStreamWrongLastMsgIDError(last)
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return fmt.Errorf("last msgid mismatch: %q vs %q", lmsgId, last)
|
|
}
|
|
}
|
|
// Check for any rollups.
|
|
if rollup := getRollup(hdr); rollup != _EMPTY_ {
|
|
if canConsistencyCheck && !allowRollupPurge && !sourced {
|
|
err := errors.New("rollup not permitted")
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSStreamRollupFailedError(err)
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return err
|
|
}
|
|
switch rollup {
|
|
case JSMsgRollupSubject:
|
|
rollupSub = true
|
|
case JSMsgRollupAll:
|
|
rollupAll = true
|
|
default:
|
|
if canConsistencyCheck {
|
|
err := fmt.Errorf("rollup value invalid: %q", rollup)
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSStreamRollupFailedError(err)
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if canConsistencyCheck && incr == nil && allowMsgCounter {
|
|
apiErr := NewJSMessageIncrMissingError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
}
|
|
|
|
// Response Ack.
|
|
var (
|
|
response []byte
|
|
seq uint64
|
|
err error
|
|
)
|
|
|
|
// 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 canConsistencyCheck && incr != nil && allowMsgCounter {
|
|
var initial big.Int
|
|
var sources CounterSources
|
|
var smv StoreMsg
|
|
sm, err := 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()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return apiErr
|
|
}
|
|
if ncs := sliceHeader(JSMessageCounterSources, sm.hdr); len(ncs) > 0 {
|
|
if err := json.Unmarshal(ncs, &sources); err != nil {
|
|
apiErr := NewJSMessageCounterBrokenError()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return 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()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = apiErr
|
|
b, _ := json.Marshal(resp)
|
|
outq.sendMsg(reply, b)
|
|
}
|
|
return 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 {
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSMessageCounterBrokenError()
|
|
response, _ = json.Marshal(resp)
|
|
outq.sendMsg(reply, response)
|
|
}
|
|
return err
|
|
}
|
|
hdr = setHeader(JSMessageCounterSources, string(nhdr), hdr)
|
|
}
|
|
|
|
// Check to see if we are over the max msg size.
|
|
// Subtract to prevent against overflows.
|
|
maxPayload := int64(mset.srv.getOpts().MaxPayload)
|
|
hdrLen, msgLen := int64(len(hdr)), int64(len(msg))
|
|
if hdrLen > maxPayload || msgLen > maxPayload-hdrLen {
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSStreamMessageExceedsMaximumError()
|
|
response, _ = json.Marshal(resp)
|
|
outq.sendMsg(reply, response)
|
|
}
|
|
return ErrMaxPayload
|
|
}
|
|
}
|
|
|
|
// Check to see if we are over the max msg size.
|
|
// Subtract to prevent against overflows.
|
|
if canConsistencyCheck && maxMsgSize >= 0 && (len(hdr) > maxMsgSize || len(msg) > maxMsgSize-len(hdr)) {
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSStreamMessageExceedsMaximumError()
|
|
response, _ = json.Marshal(resp)
|
|
outq.sendMsg(reply, response)
|
|
}
|
|
return ErrMaxPayload
|
|
}
|
|
|
|
if canConsistencyCheck && len(hdr) > math.MaxUint16 {
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSStreamHeaderExceedsMaximumError()
|
|
response, _ = json.Marshal(resp)
|
|
outq.sendMsg(reply, response)
|
|
}
|
|
return ErrMaxPayload
|
|
}
|
|
|
|
// Check to see if we have exceeded our limits.
|
|
// Don't error and log/stepdown if we're tracing when clustered.
|
|
if !isClustered && js.limitsExceeded(stype) {
|
|
s.resourcesExceededError(stype)
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSInsufficientResourcesError()
|
|
response, _ = json.Marshal(resp)
|
|
outq.sendMsg(reply, response)
|
|
}
|
|
// Stepdown regardless.
|
|
if node := mset.node; node != nil {
|
|
node.StepDown()
|
|
}
|
|
return NewJSInsufficientResourcesError()
|
|
}
|
|
|
|
var noInterest bool
|
|
|
|
// If we are interest based retention and have no consumers then we can skip.
|
|
if interestRetention {
|
|
mset.clsMu.RLock()
|
|
noInterest = numConsumers == 0 || mset.csl == nil || !mset.csl.HasInterest(subject)
|
|
mset.clsMu.RUnlock()
|
|
}
|
|
|
|
// Grab timestamp if not already set.
|
|
if ts == 0 && lseq > 0 {
|
|
ts = time.Now().UnixNano()
|
|
}
|
|
|
|
mt.updateJetStreamEvent(subject, noInterest)
|
|
if traceOnly {
|
|
return nil
|
|
}
|
|
|
|
// Skip msg here.
|
|
if noInterest {
|
|
mset.lseq, _ = store.SkipMsg(0)
|
|
mset.lmsgId = msgId
|
|
// If we have a msgId make sure to save.
|
|
if msgId != _EMPTY_ {
|
|
mset.storeMsgId(&ddentry{msgId, mset.lseq, ts})
|
|
}
|
|
if allowRollupPurge {
|
|
if err = mset.processJetStreamMsgWithRollup(subject, rollupSub, rollupAll, hdr, 0); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
// If using fast batch publish, we occasionally send flow control messages.
|
|
// And, we need to ensure a PubAck is sent if the commit happens through EOB.
|
|
if fastBatch != nil {
|
|
if mset.batches == nil {
|
|
mset.batches = &batching{}
|
|
}
|
|
mset.batches.mu.Lock()
|
|
// Check full leader state so we only send the client an update once we're caught up.
|
|
commit := mset.batches.fastBatchRegisterSequences(mset, reply, mset.lseq, mset.isLeader(), fastBatch)
|
|
mset.batches.mu.Unlock()
|
|
if !commit {
|
|
reply = _EMPTY_
|
|
canRespond = false
|
|
}
|
|
}
|
|
if canRespond {
|
|
response = append(pubAck, strconv.FormatUint(mset.lseq, 10)...)
|
|
if batchId != _EMPTY_ {
|
|
response = append(response, fmt.Sprintf(",\"batch\":%q,\"count\":%d}", batchId, batchSeq)...)
|
|
} else {
|
|
response = append(response, '}')
|
|
}
|
|
outq.sendMsg(reply, response)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Republish state if needed.
|
|
var tsubj string
|
|
var tlseq uint64
|
|
var thdrsOnly bool
|
|
if mset.tr != nil {
|
|
tsubj, _ = mset.tr.Match(subject)
|
|
if mset.cfg.RePublish != nil {
|
|
thdrsOnly = mset.cfg.RePublish.HeadersOnly
|
|
}
|
|
}
|
|
republish := tsubj != _EMPTY_ && isLeader
|
|
|
|
// If we are republishing grab last sequence for this exact subject. Aids in gap detection for lightweight clients.
|
|
if republish {
|
|
var smv StoreMsg
|
|
if sm, _ := store.LoadLastMsg(subject, &smv); sm != nil {
|
|
tlseq = sm.seq
|
|
}
|
|
}
|
|
|
|
// If clustered this was already checked and we do not want to check here and possibly introduce skew.
|
|
// Don't error and log if we're tracing when clustered.
|
|
if !isClustered {
|
|
if exceeded, err := jsa.wouldExceedLimits(stype, mset.tier, mset.cfg.Replicas, subject, hdr, msg); exceeded {
|
|
if err == nil {
|
|
err = NewJSAccountResourcesExceededError()
|
|
}
|
|
s.RateLimitWarnf("JetStream %s resource limits exceeded for account: %q", strings.ToLower(stype.String()), accName)
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = err
|
|
response, _ = json.Marshal(resp)
|
|
outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0))
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Find the message TTL if any.
|
|
ttl, err := getMessageTTL(hdr)
|
|
if err != nil {
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSMessageTTLInvalidError()
|
|
response, _ = json.Marshal(resp)
|
|
outq.send(newJSPubMsg(reply, _EMPTY_, _EMPTY_, nil, response, nil, 0))
|
|
}
|
|
return err
|
|
}
|
|
|
|
// If subject delete markers are used, ensure message TTL is that at minimum.
|
|
// Otherwise, subject delete markers could be missed if one already exists for this subject.
|
|
// MaxMsgsPer=1 is an exception, because we'll only ever have one message.
|
|
if ttl > 0 && mset.cfg.SubjectDeleteMarkerTTL > 0 && mset.cfg.MaxMsgsPer != 1 {
|
|
if minTtl := int64(mset.cfg.SubjectDeleteMarkerTTL.Seconds()); ttl < minTtl {
|
|
ttl = minTtl
|
|
hdr = removeHeaderIfPresent(hdr, JSMessageTTL)
|
|
hdr = genHeader(hdr, JSMessageTTL, strconv.FormatInt(ttl, 10))
|
|
}
|
|
}
|
|
|
|
// Store actual msg.
|
|
if lseq == 0 && ts == 0 {
|
|
seq, ts, err = store.StoreMsg(subject, hdr, msg, ttl)
|
|
} else {
|
|
// Make sure to take into account any message assignments that we had to skip (clfs).
|
|
seq = lseq + 1 - clfs
|
|
err = store.StoreRawMsg(subject, hdr, msg, seq, ts, ttl, canConsistencyCheck)
|
|
}
|
|
|
|
if err != nil {
|
|
if isPermissionError(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.
|
|
go mset.srv.DisableJetStream()
|
|
mset.srv.Warnf("Filesystem permission denied while writing msg, disabling JetStream: %v", err)
|
|
return err
|
|
}
|
|
|
|
switch err {
|
|
case ErrMaxMsgs, ErrMaxBytes, ErrMaxMsgsPerSubject, ErrMsgTooLarge:
|
|
s.RateLimitDebugf("JetStream failed to store a msg on stream '%s > %s': %v", accName, name, err)
|
|
case ErrStoreClosed:
|
|
default:
|
|
// We don't want to respond back to the user, and definitely not up CLFS either.
|
|
// This was likely an IO issue, so only log and return the error. This will stop
|
|
// the stream if it was replicated.
|
|
s.RateLimitErrorf("JetStream failed to store a msg on stream '%s > %s': %v", accName, name, err)
|
|
mset.setWriteErrLocked(err)
|
|
return err
|
|
}
|
|
|
|
// If we did not succeed increment clfs in case we are clustered.
|
|
bumpCLFS()
|
|
if canRespond {
|
|
resp.PubAck = &PubAck{Stream: name}
|
|
resp.Error = NewJSStreamStoreFailedError(err, Unless(err))
|
|
response, _ = json.Marshal(resp)
|
|
outq.sendMsg(reply, response)
|
|
}
|
|
return err
|
|
}
|
|
|
|
// Check for preAcks and the need to clear it.
|
|
if mset.hasAllPreAcks(seq, subject) {
|
|
mset.clearAllPreAcks(seq)
|
|
// If we're clustered and the stream leader, we can now propose deleting this message.
|
|
// We still store it below, so we remain properly synchronized with our followers.
|
|
// If this proposal fails, we retry out-of-band.
|
|
if isClustered && isLeader {
|
|
md := streamMsgDelete{Seq: seq, NoErase: true, Stream: mset.cfg.Name}
|
|
_ = mset.node.Propose(encodeMsgDelete(&md))
|
|
}
|
|
}
|
|
|
|
// If here we succeeded in storing the message.
|
|
mset.lmsgId = msgId
|
|
mset.lseq = seq
|
|
|
|
// If we have a msgId make sure to save.
|
|
// This will replace our estimate from the cluster layer if we are clustered.
|
|
if msgId != _EMPTY_ {
|
|
mset.ddMu.Lock()
|
|
if isClustered && isLeader && mset.ddmap != nil {
|
|
if dde := mset.ddmap[msgId]; dde != nil {
|
|
dde.seq, dde.ts = seq, ts
|
|
} else {
|
|
mset.storeMsgIdLocked(&ddentry{msgId, seq, ts})
|
|
}
|
|
} else {
|
|
// R1 or not leader..
|
|
mset.storeMsgIdLocked(&ddentry{msgId, seq, ts})
|
|
}
|
|
mset.ddMu.Unlock()
|
|
}
|
|
|
|
// No errors, this is the normal path.
|
|
if allowRollupPurge {
|
|
if err = mset.processJetStreamMsgWithRollup(subject, rollupSub, rollupAll, hdr, 1); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Check for republish.
|
|
if republish {
|
|
const ht = "NATS/1.0\r\nNats-Stream: %s\r\nNats-Subject: %s\r\nNats-Sequence: %d\r\nNats-Time-Stamp: %s\r\nNats-Last-Sequence: %d\r\n\r\n"
|
|
const htho = "NATS/1.0\r\nNats-Stream: %s\r\nNats-Subject: %s\r\nNats-Sequence: %d\r\nNats-Time-Stamp: %s\r\nNats-Last-Sequence: %d\r\nNats-Msg-Size: %d\r\n\r\n"
|
|
// When adding to existing headers, will use the fmt.Append version so this skips the headers from above.
|
|
const hoff = 10
|
|
|
|
tsStr := time.Unix(0, ts).UTC().Format(time.RFC3339Nano)
|
|
var rpMsg []byte
|
|
if len(hdr) == 0 {
|
|
if !thdrsOnly {
|
|
hdr = fmt.Appendf(nil, ht, name, subject, seq, tsStr, tlseq)
|
|
rpMsg = copyBytes(msg)
|
|
} else {
|
|
hdr = fmt.Appendf(nil, htho, name, subject, seq, tsStr, tlseq, len(msg))
|
|
}
|
|
} else {
|
|
// use hdr[:end:end] to make sure as we add we copy the original hdr.
|
|
end := len(hdr) - LEN_CR_LF
|
|
if !thdrsOnly {
|
|
hdr = fmt.Appendf(hdr[:end:end], ht[hoff:], name, subject, seq, tsStr, tlseq)
|
|
rpMsg = copyBytes(msg)
|
|
} else {
|
|
hdr = fmt.Appendf(hdr[:end:end], htho[hoff:], name, subject, seq, tsStr, tlseq, len(msg))
|
|
}
|
|
}
|
|
outq.send(newJSPubMsg(tsubj, _EMPTY_, _EMPTY_, hdr, rpMsg, nil, seq))
|
|
}
|
|
|
|
// If using fast batch publish, we occasionally send flow control messages.
|
|
// And, we need to ensure a PubAck is sent if the commit happens through EOB.
|
|
if fastBatch != nil {
|
|
if mset.batches == nil {
|
|
mset.batches = &batching{}
|
|
}
|
|
mset.batches.mu.Lock()
|
|
// Check full leader state so we only send the client an update once we're caught up.
|
|
commit := mset.batches.fastBatchRegisterSequences(mset, reply, mset.lseq, mset.isLeader(), fastBatch)
|
|
mset.batches.mu.Unlock()
|
|
if !commit {
|
|
reply = _EMPTY_
|
|
canRespond = false
|
|
}
|
|
}
|
|
|
|
// Send response here.
|
|
if canRespond {
|
|
response = append(pubAck, strconv.FormatUint(seq, 10)...)
|
|
if batchId != _EMPTY_ {
|
|
response = append(response, fmt.Sprintf(",\"batch\":%q,\"count\":%d}", batchId, batchSeq)...)
|
|
} else if allowMsgCounter {
|
|
var counter CounterValue
|
|
json.Unmarshal(msg, &counter)
|
|
response = append(response, fmt.Sprintf(",\"val\":%q}", counter.Value)...)
|
|
} else {
|
|
response = append(response, '}')
|
|
}
|
|
outq.sendMsg(reply, response)
|
|
}
|
|
|
|
// Signal consumers for new messages.
|
|
if numConsumers > 0 {
|
|
mset.sigq.push(newCMsg(subject, seq))
|
|
select {
|
|
case mset.sch <- struct{}{}:
|
|
default:
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) processJetStreamMsgWithRollup(subject string, rollupSub, rollupAll bool, hdr []byte, keep uint64) error {
|
|
if rollupSub {
|
|
if _, err := mset.purgeLocked(&JSApiStreamPurgeRequest{Subject: subject, Keep: keep}, false); err != nil {
|
|
return err
|
|
}
|
|
} else if rollupAll {
|
|
if _, err := mset.purgeLocked(&JSApiStreamPurgeRequest{Keep: keep}, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if scheduleNext := sliceHeader(JSScheduleNext, hdr); len(scheduleNext) > 0 && bytesToString(scheduleNext) == JSScheduleNextPurge {
|
|
// Purge the message schedule.
|
|
scheduler := getMessageScheduler(hdr)
|
|
if scheduler != _EMPTY_ {
|
|
if _, err := mset.purgeLocked(&JSApiStreamPurgeRequest{Subject: scheduler}, false); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// processJetStreamAtomicBatchMsg processes a JetStream message that's part of an atomic batch publish.
|
|
// Handles constraints around the batch, storing messages, doing consistency checks, and performing the commit.
|
|
func (mset *stream) processJetStreamAtomicBatchMsg(batchId, subject, reply string, hdr, msg []byte, mt *msgTrace) (retErr error) {
|
|
mset.mu.RLock()
|
|
canRespond := !mset.cfg.NoAck && len(reply) > 0
|
|
name, stype := mset.cfg.Name, mset.cfg.Storage
|
|
discard, discardNewPer, maxMsgs, maxMsgsPer, maxBytes := mset.cfg.Discard, mset.cfg.DiscardNewPer, mset.cfg.MaxMsgs, mset.cfg.MaxMsgsPer, mset.cfg.MaxBytes
|
|
s, js, jsa, r, tierName, outq, node := mset.srv, mset.js, mset.jsa, mset.cfg.Replicas, mset.tier, mset.outq, mset.node
|
|
maxMsgSize, lseq := int(mset.cfg.MaxMsgSize), mset.lseq
|
|
isLeader, isClustered, isSealed, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules, allowAtomicPublish := mset.isLeader(), mset.isClustered(), mset.cfg.Sealed, mset.cfg.AllowRollup, mset.cfg.DenyPurge, mset.cfg.AllowMsgTTL, mset.cfg.AllowMsgCounter, mset.cfg.AllowMsgSchedules, mset.cfg.AllowAtomicPublish
|
|
mset.mu.RUnlock()
|
|
|
|
// If message tracing (with message delivery), we will need to send the
|
|
// event on exit in case there was an error (if message was not proposed).
|
|
// Otherwise, the event will be sent from processJetStreamMsg when
|
|
// invoked by the leader (from applyStreamEntries).
|
|
if mt != nil {
|
|
defer func() {
|
|
if retErr != nil {
|
|
mt.sendEventFromJetStream(retErr)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Check that we are the leader. This can be false if we have scaled up from an R1 that had inbound queued messages.
|
|
if !isLeader {
|
|
return NewJSClusterNotLeaderError()
|
|
}
|
|
|
|
respondError := func(apiErr *ApiError) error {
|
|
if canRespond {
|
|
buf, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: apiErr})
|
|
outq.sendMsg(reply, buf)
|
|
}
|
|
return apiErr
|
|
}
|
|
|
|
// Bail here if sealed.
|
|
if isSealed {
|
|
return respondError(NewJSStreamSealedError())
|
|
}
|
|
|
|
// Check here pre-emptively if we have exceeded this server limits.
|
|
if js.limitsExceeded(stype) {
|
|
s.resourcesExceededError(stype)
|
|
// Stepdown regardless.
|
|
if node := mset.raftNode(); node != nil {
|
|
node.StepDown()
|
|
}
|
|
return respondError(NewJSInsufficientResourcesError())
|
|
}
|
|
|
|
// Check here pre-emptively if we have exceeded our account limits.
|
|
if exceeded, err := jsa.wouldExceedLimits(stype, tierName, r, subject, hdr, msg); exceeded {
|
|
if err == nil {
|
|
err = NewJSAccountResourcesExceededError()
|
|
}
|
|
s.RateLimitWarnf("JetStream account limits exceeded for '%s': %s", jsa.acc().GetName(), err.Error())
|
|
return respondError(err)
|
|
}
|
|
|
|
// Check msgSize if we have a limit set there. Again this works if it goes through but better to be pre-emptive.
|
|
// Subtract to prevent against overflows.
|
|
if maxMsgSize >= 0 && (len(hdr) > maxMsgSize || len(msg) > maxMsgSize-len(hdr)) {
|
|
err := fmt.Errorf("JetStream message size exceeds limits for '%s > %s'", jsa.acc().Name, mset.cfg.Name)
|
|
s.RateLimitWarnf("%s", err.Error())
|
|
_ = respondError(NewJSStreamMessageExceedsMaximumError())
|
|
return err
|
|
}
|
|
|
|
if !allowAtomicPublish {
|
|
return respondError(NewJSAtomicPublishDisabledError())
|
|
}
|
|
|
|
// Batch ID is too long.
|
|
if len(batchId) > 64 {
|
|
return respondError(NewJSAtomicPublishInvalidBatchIDError())
|
|
}
|
|
|
|
batchSeq, exists := getBatchSequence(hdr)
|
|
if !exists {
|
|
return respondError(NewJSAtomicPublishMissingSeqError())
|
|
}
|
|
|
|
jsa.mu.RLock()
|
|
storeDir := jsa.storeDir
|
|
jsa.mu.RUnlock()
|
|
|
|
mset.mu.Lock()
|
|
if mset.batches == nil {
|
|
mset.batches = &batching{}
|
|
}
|
|
batches := mset.batches
|
|
// Acquire the batches lock.
|
|
// Can't release the stream lock now, we need to keep holding it while we hold the batches lock.
|
|
// Re-acquiring the stream lock with the batches lock already held would be a lock inversion.
|
|
batches.mu.Lock()
|
|
|
|
respondIncompleteBatch := func() error {
|
|
return respondError(NewJSAtomicPublishIncompleteBatchError())
|
|
}
|
|
|
|
// Get batch.
|
|
b, ok := batches.atomic[batchId]
|
|
if !ok {
|
|
if batchSeq != 1 {
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
maxBatchSize := streamMaxAtomicBatchSize
|
|
opts := s.getOpts()
|
|
if opts.JetStreamLimits.MaxBatchSize > 0 {
|
|
maxBatchSize = opts.JetStreamLimits.MaxBatchSize
|
|
}
|
|
if batchSeq > uint64(maxBatchSize) {
|
|
err := NewJSAtomicPublishTooLargeBatchError(maxBatchSize)
|
|
return respondError(err)
|
|
}
|
|
return respondIncompleteBatch()
|
|
}
|
|
|
|
// Limits.
|
|
maxInflightPerStream := streamMaxAtomicBatchInflightPerStream
|
|
maxInflightTotal := streamMaxAtomicBatchInflightTotal
|
|
opts := s.getOpts()
|
|
if opts.JetStreamLimits.MaxBatchInflightPerStream > 0 {
|
|
maxInflightPerStream = opts.JetStreamLimits.MaxBatchInflightPerStream
|
|
}
|
|
if opts.JetStreamLimits.MaxBatchInflightTotal > 0 {
|
|
maxInflightTotal = opts.JetStreamLimits.MaxBatchInflightTotal
|
|
}
|
|
|
|
// Confirm we can facilitate an additional batch.
|
|
if len(batches.atomic)+1 > maxInflightPerStream {
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
return respondError(NewJSAtomicPublishTooManyInflightError())
|
|
}
|
|
|
|
// Confirm we'll not exceed the server limit.
|
|
if globalInflightAtomicBatches.Add(1) > int64(maxInflightTotal) {
|
|
globalInflightAtomicBatches.Add(-1)
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
return respondError(NewJSAtomicPublishTooManyInflightError())
|
|
}
|
|
|
|
var err error
|
|
b, err = batches.newAtomicBatch(mset, batchId, r, stype, storeDir, name)
|
|
if err != nil {
|
|
globalInflightAtomicBatches.Add(-1)
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
return respondIncompleteBatch()
|
|
}
|
|
if batches.atomic == nil {
|
|
batches.atomic = make(map[string]*atomicBatch, 1)
|
|
}
|
|
batches.atomic[batchId] = b
|
|
}
|
|
|
|
var commit, commitEob bool
|
|
if c := sliceHeader(JSBatchCommit, hdr); c != nil {
|
|
commitEob = bytes.Equal(c, []byte("eob"))
|
|
// Reject the batch if the commit is not recognized.
|
|
if !commitEob && !bytes.Equal(c, []byte("1")) {
|
|
b.cleanupLocked(batchId, batches)
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
err := NewJSAtomicPublishInvalidBatchCommitError()
|
|
return respondError(err)
|
|
}
|
|
commit = true
|
|
}
|
|
|
|
// The required API level can have the batch be rejected. But the header is always removed.
|
|
if len(sliceHeader(JSRequiredApiLevel, hdr)) != 0 {
|
|
if errorOnRequiredApiLevel(hdr) {
|
|
b.cleanupLocked(batchId, batches)
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
mset.sendStreamBatchAbandonedAdvisory(batchId, BatchRequirementsNotMet)
|
|
err := NewJSRequiredApiLevelError()
|
|
return respondError(err)
|
|
}
|
|
hdr = removeHeaderIfPresent(hdr, JSRequiredApiLevel)
|
|
}
|
|
|
|
// If cleanup has already happened, we can't continue.
|
|
cleanup := !b.resetCleanupTimer(mset)
|
|
|
|
// Detect gaps.
|
|
b.lseq++
|
|
if b.lseq != batchSeq || cleanup || (batchSeq == 1 && commitEob) {
|
|
b.cleanupLocked(batchId, batches)
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
mset.sendStreamBatchAbandonedAdvisory(batchId, BatchIncomplete)
|
|
return respondIncompleteBatch()
|
|
}
|
|
|
|
// Confirm the batch doesn't exceed the allowed size.
|
|
maxSize := streamMaxAtomicBatchSize
|
|
if maxBatchSize := s.getOpts().JetStreamLimits.MaxBatchSize; maxBatchSize > 0 {
|
|
maxSize = maxBatchSize
|
|
}
|
|
if batchSeq > uint64(maxSize) {
|
|
b.cleanupLocked(batchId, batches)
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
mset.sendStreamBatchAbandonedAdvisory(batchId, BatchLarge)
|
|
err := NewJSAtomicPublishTooLargeBatchError(maxSize)
|
|
return respondError(err)
|
|
}
|
|
|
|
// Persist, but optimize if we're committing because we already know last.
|
|
// If the underlying store is file-based we need to persist everything to survive hard kills, because we're R1.
|
|
if !commit || b.store.Type() == FileStorage {
|
|
seq, _, err := b.store.StoreMsg(subject, hdr, msg, 0)
|
|
if err != nil || seq != batchSeq {
|
|
b.cleanupLocked(batchId, batches)
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
mset.sendStreamBatchAbandonedAdvisory(batchId, BatchIncomplete)
|
|
return respondIncompleteBatch()
|
|
}
|
|
}
|
|
if !commit {
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
// Send empty ack to let them know we've persisted the data prior to commit.
|
|
if canRespond {
|
|
outq.sendMsg(reply, nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Ensure the batch is prepared for the commit and will not be cleaned up while committing.
|
|
if abandonReason := b.readyForCommit(); abandonReason != nil {
|
|
// Don't do cleanup, this is already done.
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
mset.sendStreamBatchAbandonedAdvisory(batchId, *abandonReason)
|
|
return respondIncompleteBatch()
|
|
}
|
|
|
|
// Proceed with proposing the batch.
|
|
ts := time.Now().UnixNano()
|
|
|
|
// Need to hold this lock even if we're not clustered, because we'll
|
|
// be accessing state that requires this lock (even if it's empty).
|
|
mset.clMu.Lock()
|
|
|
|
// If not clustered, set up the last sequence.
|
|
if !isClustered {
|
|
mset.clseq, mset.clfs = mset.lseq, 0
|
|
}
|
|
|
|
// We only use mset.clseq for clustering and in case we run ahead of actual commits.
|
|
// Check if we need to set initial value here
|
|
if isClustered && (mset.clseq == 0 || mset.clseq < lseq+mset.clfs) {
|
|
lseq = recalculateClusteredSeq(mset, false)
|
|
}
|
|
|
|
oclseq := mset.clseq
|
|
rollback := func() {
|
|
if isClustered {
|
|
// Only need to move the clustered sequence back if the batch fails to commit.
|
|
// Other changes were staged but not applied, so this is the only thing we need to do.
|
|
mset.clseq = oclseq
|
|
}
|
|
mset.clMu.Unlock()
|
|
}
|
|
|
|
errorOnUnsupported := func(header string) *ApiError {
|
|
apiErr := NewJSAtomicPublishUnsupportedHeaderBatchError(header)
|
|
rollback()
|
|
b.cleanupLocked(batchId, batches)
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
_ = respondError(apiErr)
|
|
return apiErr
|
|
}
|
|
|
|
var (
|
|
entries []*Entry
|
|
bsubj string
|
|
bhdr []byte
|
|
bmsg []byte
|
|
apiErr *ApiError
|
|
err error
|
|
smv StoreMsg
|
|
sm *StoreMsg
|
|
sz int
|
|
)
|
|
|
|
// If the commit ends with an "End Of Batch" message, we don't store this.
|
|
if commitEob {
|
|
batchSeq--
|
|
}
|
|
|
|
diff := &batchStagedDiff{}
|
|
for seq := uint64(1); seq <= batchSeq; seq++ {
|
|
if seq == batchSeq && !commitEob && b.store.Type() != FileStorage {
|
|
bsubj, bhdr, bmsg = subject, hdr, msg
|
|
} else if sm, err = b.store.LoadMsg(seq, &smv); sm != nil && err == nil {
|
|
bsubj, bhdr, bmsg = sm.subj, sm.hdr, sm.msg
|
|
} else {
|
|
rollback()
|
|
b.cleanupLocked(batchId, batches)
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
return respondIncompleteBatch()
|
|
}
|
|
|
|
// Apply the input subject transform if any
|
|
csubj := bsubj
|
|
if mset.itr != nil {
|
|
ts, err := mset.itr.Match(csubj)
|
|
if err == nil {
|
|
// no filtering: if the subject doesn't map the source of the transform, don't change it
|
|
csubj = ts
|
|
}
|
|
}
|
|
|
|
// Reject unsupported headers.
|
|
if getExpectedLastMsgId(bhdr) != _EMPTY_ {
|
|
return errorOnUnsupported(JSExpectedLastMsgId)
|
|
}
|
|
|
|
if bhdr, bmsg, _, apiErr, err = checkMsgHeadersPreClusteredProposal(diff, mset, csubj, bsubj, bhdr, bmsg, false, name, jsa, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules, discard, discardNewPer, maxMsgSize, maxMsgs, maxMsgsPer, maxBytes); err != nil {
|
|
rollback()
|
|
b.cleanupLocked(batchId, batches)
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
_ = respondError(apiErr)
|
|
return err
|
|
}
|
|
|
|
if isClustered {
|
|
var _reply string
|
|
isCommit := seq == batchSeq
|
|
if isCommit {
|
|
_reply = reply
|
|
// If committed by EOB, the last message must get the normal commit header.
|
|
if commitEob {
|
|
bhdr = genHeader(bhdr, JSBatchCommit, "1")
|
|
}
|
|
}
|
|
esm := encodeStreamMsgAllowCompressAndBatch(bsubj, _reply, bhdr, bmsg, mset.clseq, ts, false, batchId, seq, isCommit)
|
|
entries = append(entries, newEntry(EntryNormal, esm))
|
|
sz += len(esm)
|
|
}
|
|
mset.clseq++
|
|
}
|
|
|
|
// Commit batch.
|
|
if !isClustered {
|
|
// Reset, we only used this to do the batching checks.
|
|
mset.clseq, mset.clfs = 0, 0
|
|
mset.clMu.Unlock()
|
|
|
|
// Ensure the whole batch is fully isolated, and reads
|
|
// can only happen after the full batch is committed.
|
|
// We keep holding the stream lock.
|
|
for seq := uint64(1); seq <= batchSeq; seq++ {
|
|
if seq == batchSeq && !commitEob && b.store.Type() != FileStorage {
|
|
bsubj, bhdr, bmsg = subject, hdr, msg
|
|
} else if sm, err = b.store.LoadMsg(seq, &smv); sm != nil && err == nil {
|
|
bsubj, bhdr, bmsg = sm.subj, sm.hdr, sm.msg
|
|
} else {
|
|
// Should not happen, we've already checked this message existed while doing consistency checks.
|
|
// We'll just exit here, the batch is already inconsistent without the message at this sequence.
|
|
// No use in trying to still store the rest.
|
|
break
|
|
}
|
|
var _reply string
|
|
if seq == batchSeq {
|
|
_reply = reply
|
|
// If committed by EOB, the last message must get the normal commit header.
|
|
if commitEob {
|
|
bhdr = genHeader(bhdr, JSBatchCommit, "1")
|
|
}
|
|
}
|
|
_ = mset.processJetStreamMsg(bsubj, _reply, bhdr, bmsg, 0, 0, mt, false, false)
|
|
}
|
|
mset.mu.Unlock()
|
|
} else {
|
|
mset.mu.Unlock()
|
|
// Do a single multi proposal. This ensures we get to push all entries to the proposal queue in-order
|
|
// and not interleaved with other proposals.
|
|
if err = node.ProposeMulti(entries); err == nil {
|
|
diff.commit(mset)
|
|
mset.trackReplicationTraffic(node, sz, r)
|
|
|
|
// 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)
|
|
s.RateLimitWarnf("%s", lerr.Error())
|
|
}
|
|
} else {
|
|
mset.clseq = oclseq
|
|
}
|
|
mset.clMu.Unlock()
|
|
}
|
|
b.cleanupLocked(batchId, batches)
|
|
batches.mu.Unlock()
|
|
return err
|
|
}
|
|
|
|
// processJetStreamFastBatchMsg processes a JetStream message that's part of an atomic batch publish.
|
|
// Handles constraints around the batch, storing messages, doing consistency checks, and performing the commit.
|
|
func (mset *stream) processJetStreamFastBatchMsg(batch *FastBatch, subject, reply string, hdr, msg []byte, mt *msgTrace) (retErr error) {
|
|
mset.mu.RLock()
|
|
canRespond := !mset.cfg.NoAck && len(reply) > 0
|
|
name, stype := mset.cfg.Name, mset.cfg.Storage
|
|
discard, discardNewPer, maxMsgs, maxMsgsPer, maxBytes := mset.cfg.Discard, mset.cfg.DiscardNewPer, mset.cfg.MaxMsgs, mset.cfg.MaxMsgsPer, mset.cfg.MaxBytes
|
|
s, js, jsa, st, r, tierName, outq, node := mset.srv, mset.js, mset.jsa, mset.cfg.Storage, mset.cfg.Replicas, mset.tier, mset.outq, mset.node
|
|
maxMsgSize, lseq := int(mset.cfg.MaxMsgSize), mset.lseq
|
|
isLeader, isClustered, isSealed, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules, allowBatchPublish := mset.isLeader(), mset.isClustered(), mset.cfg.Sealed, mset.cfg.AllowRollup, mset.cfg.DenyPurge, mset.cfg.AllowMsgTTL, mset.cfg.AllowMsgCounter, mset.cfg.AllowMsgSchedules, mset.cfg.AllowBatchPublish
|
|
|
|
// Apply the input subject transform if any
|
|
csubject := subject
|
|
if mset.itr != nil {
|
|
ts, err := mset.itr.Match(csubject)
|
|
if err == nil {
|
|
// no filtering: if the subject doesn't map the source of the transform, don't change it
|
|
csubject = ts
|
|
}
|
|
}
|
|
mset.mu.RUnlock()
|
|
|
|
// If message tracing (with message delivery), we will need to send the
|
|
// event on exit in case there was an error (if message was not proposed).
|
|
// Otherwise, the event will be sent from processJetStreamMsg when
|
|
// invoked by the leader (from applyStreamEntries).
|
|
if mt != nil {
|
|
defer func() {
|
|
if retErr != nil {
|
|
mt.sendEventFromJetStream(retErr)
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Check that we are the leader. This can be false if we have scaled up from an R1 that had inbound queued messages.
|
|
if !isLeader {
|
|
return NewJSClusterNotLeaderError()
|
|
}
|
|
|
|
respondError := func(apiErr *ApiError) error {
|
|
if canRespond {
|
|
buf, _ := json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: apiErr})
|
|
outq.sendMsg(reply, buf)
|
|
}
|
|
return apiErr
|
|
}
|
|
|
|
// Bail here if sealed.
|
|
if isSealed {
|
|
return respondError(NewJSStreamSealedError())
|
|
}
|
|
|
|
// Check here pre-emptively if we have exceeded this server limits.
|
|
if js.limitsExceeded(stype) {
|
|
s.resourcesExceededError(stype)
|
|
// Stepdown regardless.
|
|
if node := mset.raftNode(); node != nil {
|
|
node.StepDown()
|
|
}
|
|
return respondError(NewJSInsufficientResourcesError())
|
|
}
|
|
|
|
// Check here pre-emptively if we have exceeded our account limits.
|
|
if exceeded, err := jsa.wouldExceedLimits(st, tierName, r, csubject, hdr, msg); exceeded {
|
|
if err == nil {
|
|
err = NewJSAccountResourcesExceededError()
|
|
}
|
|
s.RateLimitWarnf("JetStream account limits exceeded for '%s': %s", jsa.acc().GetName(), err.Error())
|
|
return respondError(err)
|
|
}
|
|
|
|
// Check msgSize if we have a limit set there. Again this works if it goes through but better to be pre-emptive.
|
|
// Subtract to prevent against overflows.
|
|
if maxMsgSize >= 0 && (len(hdr) > maxMsgSize || len(msg) > maxMsgSize-len(hdr)) {
|
|
err := fmt.Errorf("JetStream message size exceeds limits for '%s > %s'", jsa.acc().Name, mset.cfg.Name)
|
|
s.RateLimitWarnf("%s", err.Error())
|
|
_ = respondError(NewJSStreamMessageExceedsMaximumError())
|
|
return err
|
|
}
|
|
|
|
if !allowBatchPublish {
|
|
return respondError(NewJSBatchPublishDisabledError())
|
|
}
|
|
|
|
if batch == nil {
|
|
return respondError(NewJSBatchPublishInvalidPatternError())
|
|
}
|
|
|
|
// Batch ID is too long.
|
|
if len(batch.id) > 64 {
|
|
return respondError(NewJSBatchPublishInvalidBatchIDError())
|
|
}
|
|
|
|
mset.mu.Lock()
|
|
if mset.batches == nil {
|
|
mset.batches = &batching{}
|
|
}
|
|
batches := mset.batches
|
|
// Acquire the batches lock.
|
|
// Can't release the stream lock now, we need to keep holding it while we hold the batches lock.
|
|
// Re-acquiring the stream lock with the batches lock already held would be a lock inversion.
|
|
batches.mu.Lock()
|
|
|
|
// Get batch.
|
|
b, ok := batches.fast[batch.id]
|
|
if !ok {
|
|
if batch.seq != 1 {
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
return respondError(NewJSBatchPublishUnknownBatchIDError())
|
|
}
|
|
|
|
// Limits.
|
|
maxInflightPerStream := streamMaxFastBatchInflightPerStream
|
|
maxInflightTotal := streamMaxFastBatchInflightTotal
|
|
opts := s.getOpts()
|
|
if opts.JetStreamLimits.MaxBatchInflightPerStream > 0 {
|
|
maxInflightPerStream = opts.JetStreamLimits.MaxBatchInflightPerStream
|
|
}
|
|
if opts.JetStreamLimits.MaxBatchInflightTotal > 0 {
|
|
maxInflightTotal = opts.JetStreamLimits.MaxBatchInflightTotal
|
|
}
|
|
|
|
// Confirm we can facilitate an additional batch.
|
|
if len(batches.fast)+1 > maxInflightPerStream {
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
return respondError(NewJSBatchPublishTooManyInflightError())
|
|
}
|
|
|
|
// Confirm we'll not exceed the server limit.
|
|
if globalInflightFastBatches.Add(1) > int64(maxInflightTotal) {
|
|
globalInflightFastBatches.Add(-1)
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
return respondError(NewJSBatchPublishTooManyInflightError())
|
|
}
|
|
|
|
// 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)
|
|
}
|
|
|
|
// The required API level can have the batch be rejected. But the header is always removed.
|
|
if len(sliceHeader(JSRequiredApiLevel, hdr)) != 0 {
|
|
if errorOnRequiredApiLevel(hdr) {
|
|
b.cleanupLocked(batch.id, batches)
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
return respondError(NewJSRequiredApiLevelError())
|
|
}
|
|
hdr = removeHeaderIfPresent(hdr, JSRequiredApiLevel)
|
|
}
|
|
|
|
// Fast publishing resets the cleanup timer.
|
|
// If cleanup has already happened, we can't continue.
|
|
cleanup := !b.resetCleanupTimer(mset)
|
|
|
|
// A ping operation confirms we've received a minimum amount of data and resends ack messages.
|
|
if batch.ping {
|
|
sendFlowControl := true
|
|
// Detect a gap or if the batch was cleaned up in the meantime.
|
|
if batch.seq > b.lseq || cleanup {
|
|
// If a gap is detected, we always report about it.
|
|
buf, _ := BatchFlowGap{ExpectedLastSequence: b.lseq + 1, CurrentSequence: batch.seq + 1}.MarshalJSON()
|
|
outq.sendMsg(reply, buf)
|
|
// If the gap is okay, we can continue without rejecting.
|
|
if b.gapOk && !cleanup {
|
|
b.lseq = batch.seq
|
|
if b.pending == 0 {
|
|
b.pseq = b.lseq
|
|
}
|
|
sendFlowControl = !b.checkFlowControl(mset, reply, batches)
|
|
} else if cleanup = batches.fastBatchCommit(b, batch.id, mset, reply); cleanup {
|
|
b.cleanupLocked(batch.id, batches)
|
|
sendFlowControl = false
|
|
}
|
|
}
|
|
if sendFlowControl {
|
|
b.sendFlowControl(b.fseq, mset, reply)
|
|
}
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// If the batch is committing, due to an error, we can't add more messages.
|
|
// We simply skip, since the client will be waiting for the PubAck.
|
|
if b.commit {
|
|
// MUST NOT clean up, that will happen when the commit completes.
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// Detect gaps.
|
|
b.lseq++
|
|
if b.lseq != batch.seq || cleanup {
|
|
// If a forward gap is detected, we always report about it.
|
|
if batch.seq > b.lseq {
|
|
buf, _ := BatchFlowGap{ExpectedLastSequence: b.lseq, CurrentSequence: batch.seq}.MarshalJSON()
|
|
outq.sendMsg(reply, buf)
|
|
}
|
|
// If the forward gap is okay, we can continue without rejecting.
|
|
if b.gapOk && !cleanup && batch.seq > b.lseq {
|
|
b.lseq = batch.seq
|
|
} else {
|
|
// We've reached either a backward gap, or were cleaned up already, or it's gap-fail mode.
|
|
// Revert, since we incremented for the gap check.
|
|
b.lseq--
|
|
if cleanup = batches.fastBatchCommit(b, batch.id, mset, reply); cleanup {
|
|
b.cleanupLocked(batch.id, batches)
|
|
}
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
return nil
|
|
}
|
|
}
|
|
|
|
if batch.commit {
|
|
if batch.commitEob {
|
|
// Revert, since we incremented for the gap check.
|
|
b.lseq--
|
|
// If there is none pending, correct the persisted sequence as we need to commit below.
|
|
if b.pending == 0 {
|
|
b.pseq = b.lseq
|
|
}
|
|
}
|
|
// We'll try to immediately send a PubAck if we can.
|
|
// Only possible if EOB is used and the last message was already persisted
|
|
// Otherwise, this sets up the commit for the last message we're about to propose.
|
|
cleanup = batches.fastBatchCommit(b, batch.id, mset, reply)
|
|
if batch.commitEob {
|
|
if cleanup {
|
|
b.cleanupLocked(batch.id, batches)
|
|
}
|
|
batches.mu.Unlock()
|
|
mset.mu.Unlock()
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// The first message in the batch responds with the settings used for flow control.
|
|
// If committing immediately, we only send the PubAck.
|
|
if batch.seq == 1 && canRespond && !batch.commit {
|
|
buf, _ := BatchFlowAck{Sequence: 0, Messages: b.ackMessages}.MarshalJSON()
|
|
outq.sendMsg(reply, buf)
|
|
}
|
|
|
|
// Proceed with proposing this message.
|
|
|
|
// We only use mset.clseq for clustering and in case we run ahead of actual commits.
|
|
// Check if we need to set initial value here
|
|
mset.clMu.Lock()
|
|
if mset.clseq == 0 || mset.clseq < lseq+mset.clfs {
|
|
lseq = recalculateClusteredSeq(mset, false)
|
|
}
|
|
// We can now unlock, since we've potentially recalculated the clustered seq above.
|
|
mset.mu.Unlock()
|
|
|
|
var (
|
|
dseq uint64
|
|
apiErr *ApiError
|
|
err error
|
|
)
|
|
diff := &batchStagedDiff{}
|
|
if hdr, msg, dseq, apiErr, err = checkMsgHeadersPreClusteredProposal(diff, mset, csubject, subject, hdr, msg, false, name, jsa, allowRollup, denyPurge, allowTTL, allowMsgCounter, allowMsgSchedules, discard, discardNewPer, maxMsgSize, maxMsgs, maxMsgsPer, maxBytes); err != nil {
|
|
mset.clMu.Unlock()
|
|
|
|
// If the message is a duplicate, and we have no pending messages, we should check if we need to
|
|
// send the flow control message here.
|
|
if err == errMsgIdDuplicate {
|
|
if b.pending == 0 {
|
|
b.pseq = batch.seq
|
|
b.checkFlowControl(mset, reply, batches)
|
|
}
|
|
if !batch.commit {
|
|
// Otherwise, just skip.
|
|
batches.mu.Unlock()
|
|
return err
|
|
}
|
|
}
|
|
|
|
// If a batch immediately errors, we send the same response as we would a normal publish.
|
|
if !batch.gapOk && b.lseq == 1 {
|
|
var response []byte
|
|
if err == errMsgIdDuplicate && dseq > 0 {
|
|
var buf [256]byte
|
|
response = append(buf[:0], mset.pubAck...)
|
|
response = append(response, strconv.FormatUint(dseq, 10)...)
|
|
response = append(response, fmt.Sprintf(",\"duplicate\": true,\"batch\":%q,\"count\":%d}", batch.id, batch.seq)...)
|
|
} else {
|
|
response, _ = json.Marshal(&JSPubAckResponse{PubAck: &PubAck{Stream: name}, Error: apiErr})
|
|
}
|
|
b.cleanupLocked(batch.id, batches)
|
|
batches.mu.Unlock()
|
|
outq.sendMsg(reply, response)
|
|
return err
|
|
}
|
|
|
|
// We always return the error to the client, unless it's a duplicate.
|
|
if err != errMsgIdDuplicate {
|
|
buf, _ := BatchFlowErr{Sequence: batch.seq, Error: apiErr}.MarshalJSON()
|
|
outq.sendMsg(reply, buf)
|
|
}
|
|
|
|
// If gaps are okay, we just allow them to continue.
|
|
if batch.gapOk {
|
|
batches.mu.Unlock()
|
|
return err
|
|
}
|
|
|
|
// Revert the last sequence, we might be able to immediately return the PubAck as part of the commit.
|
|
// Otherwise, the batch is cleaned up automatically later.
|
|
if err != errMsgIdDuplicate {
|
|
b.lseq--
|
|
}
|
|
if cleanup = batches.fastBatchCommit(b, batch.id, mset, reply); cleanup {
|
|
b.cleanupLocked(batch.id, batches)
|
|
}
|
|
batches.mu.Unlock()
|
|
return err
|
|
}
|
|
b.pending++
|
|
batches.mu.Unlock()
|
|
if !isClustered {
|
|
mset.clMu.Unlock()
|
|
return mset.processJetStreamMsgWithBatch(subject, reply, hdr, msg, 0, 0, mt, false, true, batch)
|
|
}
|
|
err = commitSingleMsg(diff, mset, subject, reply, hdr, msg, name, jsa, mt, node, r, lseq)
|
|
mset.clMu.Unlock()
|
|
return err
|
|
}
|
|
|
|
// Used to signal inbound message to registered consumers.
|
|
type cMsg struct {
|
|
seq uint64
|
|
subj string
|
|
}
|
|
|
|
// Pool to recycle consumer bound msgs.
|
|
var cMsgPool sync.Pool
|
|
|
|
// Used to queue up consumer bound msgs for signaling.
|
|
func newCMsg(subj string, seq uint64) *cMsg {
|
|
var m *cMsg
|
|
cm := cMsgPool.Get()
|
|
if cm != nil {
|
|
m = cm.(*cMsg)
|
|
} else {
|
|
m = new(cMsg)
|
|
}
|
|
m.subj, m.seq = subj, seq
|
|
|
|
return m
|
|
}
|
|
|
|
func (m *cMsg) returnToPool() {
|
|
if m == nil {
|
|
return
|
|
}
|
|
m.subj, m.seq = _EMPTY_, 0
|
|
cMsgPool.Put(m)
|
|
}
|
|
|
|
// Go routine to signal consumers.
|
|
// Offloaded from stream msg processing.
|
|
func (mset *stream) signalConsumersLoop() {
|
|
mset.mu.RLock()
|
|
s, qch, sch, msgs := mset.srv, mset.qch, mset.sch, mset.sigq
|
|
mset.mu.RUnlock()
|
|
|
|
for {
|
|
select {
|
|
case <-s.quitCh:
|
|
return
|
|
case <-qch:
|
|
return
|
|
case <-sch:
|
|
cms := msgs.pop()
|
|
for _, m := range cms {
|
|
seq, subj := m.seq, m.subj
|
|
m.returnToPool()
|
|
// Signal all appropriate consumers.
|
|
mset.signalConsumers(subj, seq)
|
|
}
|
|
msgs.recycle(&cms)
|
|
}
|
|
}
|
|
}
|
|
|
|
// This will update and signal all consumers that match.
|
|
func (mset *stream) signalConsumers(subj string, seq uint64) {
|
|
mset.clsMu.RLock()
|
|
defer mset.clsMu.RUnlock()
|
|
csl := mset.csl
|
|
if csl == nil {
|
|
return
|
|
}
|
|
csl.Match(subj, func(o *consumer) {
|
|
o.processStreamSignal(seq)
|
|
})
|
|
}
|
|
|
|
// Internal message for use by jetstream subsystem.
|
|
type jsPubMsg struct {
|
|
dsubj string // Subject to send to, e.g. _INBOX.xxx
|
|
reply string
|
|
StoreMsg
|
|
o *consumer
|
|
}
|
|
|
|
var jsPubMsgPool = sync.Pool{
|
|
New: func() any {
|
|
return &jsPubMsg{}
|
|
},
|
|
}
|
|
|
|
func newJSPubMsg(dsubj, subj, reply string, hdr, msg []byte, o *consumer, seq uint64) *jsPubMsg {
|
|
m := getJSPubMsgFromPool()
|
|
if m.buf == nil {
|
|
m.buf = make([]byte, 0, len(hdr)+len(msg))
|
|
}
|
|
buf := append(m.buf[:0], hdr...)
|
|
buf = append(buf, msg...)
|
|
hdr = buf[:len(hdr):len(hdr)]
|
|
msg = buf[len(hdr):]
|
|
// When getting something from a pool it is critical that all fields are
|
|
// initialized. Doing this way guarantees that if someone adds a field to
|
|
// the structure, the compiler will fail the build if this line is not updated.
|
|
(*m) = jsPubMsg{dsubj, reply, StoreMsg{subj, hdr, msg, buf, seq, 0}, o}
|
|
return m
|
|
}
|
|
|
|
// Gets a jsPubMsg from the pool.
|
|
func getJSPubMsgFromPool() *jsPubMsg {
|
|
return jsPubMsgPool.Get().(*jsPubMsg)
|
|
}
|
|
|
|
func (pm *jsPubMsg) returnToPool() {
|
|
if pm == nil {
|
|
return
|
|
}
|
|
pm.subj, pm.dsubj, pm.reply, pm.hdr, pm.msg, pm.o = _EMPTY_, _EMPTY_, _EMPTY_, nil, nil, nil
|
|
if len(pm.buf) > 0 {
|
|
pm.buf = pm.buf[:0]
|
|
}
|
|
jsPubMsgPool.Put(pm)
|
|
}
|
|
|
|
func (pm *jsPubMsg) size() int {
|
|
if pm == nil {
|
|
return 0
|
|
}
|
|
return len(pm.dsubj) + len(pm.reply) + len(pm.hdr) + len(pm.msg)
|
|
}
|
|
|
|
// Queue of *jsPubMsg for sending internal system messages.
|
|
type jsOutQ struct {
|
|
*ipQueue[*jsPubMsg]
|
|
}
|
|
|
|
func (q *jsOutQ) sendMsg(subj string, msg []byte) {
|
|
if q != nil {
|
|
q.send(newJSPubMsg(subj, _EMPTY_, _EMPTY_, nil, msg, nil, 0))
|
|
}
|
|
}
|
|
|
|
func (q *jsOutQ) send(msg *jsPubMsg) {
|
|
if q == nil || msg == nil {
|
|
return
|
|
}
|
|
q.push(msg)
|
|
}
|
|
|
|
func (q *jsOutQ) unregister() {
|
|
if q == nil {
|
|
return
|
|
}
|
|
q.ipQueue.unregister()
|
|
}
|
|
|
|
// StoredMsg is for raw access to messages in a stream.
|
|
type StoredMsg struct {
|
|
Subject string `json:"subject"`
|
|
Sequence uint64 `json:"seq"`
|
|
Header []byte `json:"hdrs,omitempty"`
|
|
Data []byte `json:"data,omitempty"`
|
|
Time time.Time `json:"time"`
|
|
}
|
|
|
|
// This is similar to system semantics but did not want to overload the single system sendq,
|
|
// or require system account when doing simple setup with jetstream.
|
|
func (mset *stream) setupSendCapabilities() {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
if mset.outq != nil {
|
|
return
|
|
}
|
|
qname := fmt.Sprintf("[ACC:%s] stream '%s' sendQ", mset.acc.Name, mset.cfg.Name)
|
|
mset.outq = &jsOutQ{newIPQueue[*jsPubMsg](mset.srv, qname)}
|
|
go mset.internalLoop()
|
|
}
|
|
|
|
// Returns the associated account name.
|
|
func (mset *stream) accName() string {
|
|
if mset == nil {
|
|
return _EMPTY_
|
|
}
|
|
mset.mu.RLock()
|
|
acc := mset.acc
|
|
mset.mu.RUnlock()
|
|
return acc.Name
|
|
}
|
|
|
|
// Name returns the stream name.
|
|
func (mset *stream) name() string {
|
|
return mset.nameLocked(true)
|
|
}
|
|
|
|
func (mset *stream) nameLocked(needLock bool) string {
|
|
if mset == nil {
|
|
return _EMPTY_
|
|
}
|
|
if needLock {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
}
|
|
return mset.cfg.Name
|
|
}
|
|
|
|
func (mset *stream) internalLoop() {
|
|
mset.mu.RLock()
|
|
setGoRoutineLabels(pprofLabels{
|
|
"account": mset.acc.Name,
|
|
"stream": mset.cfg.Name,
|
|
})
|
|
s := mset.srv
|
|
c := s.createInternalJetStreamClient()
|
|
c.registerWithAccount(mset.acc)
|
|
defer c.closeConnection(ClientClosed)
|
|
outq, qch, msgs, gets := mset.outq, mset.qch, mset.msgs, mset.gets
|
|
|
|
// For the ack msgs queue for interest retention.
|
|
var (
|
|
amch chan struct{}
|
|
ackq *ipQueue[uint64]
|
|
)
|
|
if mset.ackq != nil {
|
|
ackq, amch = mset.ackq, mset.ackq.ch
|
|
}
|
|
mset.mu.RUnlock()
|
|
|
|
// Raw scratch buffer.
|
|
// This should be rarely used now so can be smaller.
|
|
var _r [1024]byte
|
|
|
|
// To optimize for not converting a string to a []byte slice.
|
|
var (
|
|
subj [256]byte
|
|
dsubj [256]byte
|
|
rply [256]byte
|
|
szb [10]byte
|
|
hdb [10]byte
|
|
)
|
|
|
|
for {
|
|
select {
|
|
case <-outq.ch:
|
|
pms := outq.pop()
|
|
for _, pm := range pms {
|
|
c.pa.subject = append(dsubj[:0], pm.dsubj...)
|
|
c.pa.deliver = append(subj[:0], pm.subj...)
|
|
c.pa.size = len(pm.msg) + len(pm.hdr)
|
|
c.pa.szb = append(szb[:0], strconv.Itoa(c.pa.size)...)
|
|
if len(pm.reply) > 0 {
|
|
c.pa.reply = append(rply[:0], pm.reply...)
|
|
} else {
|
|
c.pa.reply = nil
|
|
}
|
|
|
|
// If we have an underlying buf that is the wire contents for hdr + msg, else construct on the fly.
|
|
var msg []byte
|
|
if len(pm.buf) > 0 {
|
|
msg = pm.buf
|
|
} else {
|
|
if len(pm.hdr) > 0 {
|
|
msg = pm.hdr
|
|
if len(pm.msg) > 0 {
|
|
msg = _r[:0]
|
|
msg = append(msg, pm.hdr...)
|
|
msg = append(msg, pm.msg...)
|
|
}
|
|
} else if len(pm.msg) > 0 {
|
|
// We own this now from a low level buffer perspective so can use directly here.
|
|
msg = pm.msg
|
|
}
|
|
}
|
|
|
|
if len(pm.hdr) > 0 {
|
|
c.pa.hdr = len(pm.hdr)
|
|
c.pa.hdb = []byte(strconv.Itoa(c.pa.hdr))
|
|
c.pa.hdb = append(hdb[:0], strconv.Itoa(c.pa.hdr)...)
|
|
} else {
|
|
c.pa.hdr = -1
|
|
c.pa.hdb = nil
|
|
}
|
|
|
|
msg = append(msg, _CRLF_...)
|
|
|
|
didDeliver, _ := c.processInboundClientMsg(msg)
|
|
c.pa.szb, c.pa.subject, c.pa.deliver = nil, nil, nil
|
|
|
|
// Check to see if this is a delivery for a consumer and
|
|
// we failed to deliver the message. If so alert the consumer.
|
|
if pm.o != nil && pm.seq > 0 && !didDeliver {
|
|
pm.o.didNotDeliver(pm.seq, pm.dsubj)
|
|
}
|
|
pm.returnToPool()
|
|
}
|
|
// TODO: Move in the for-loop?
|
|
c.flushClients(0)
|
|
outq.recycle(&pms)
|
|
case <-msgs.ch:
|
|
// This can possibly change now so needs to be checked here.
|
|
isClustered := mset.IsClustered()
|
|
ims := msgs.pop()
|
|
for _, im := range ims {
|
|
// If we are clustered we need to propose this message to the underlying raft group.
|
|
if batch, err := getFastBatch(im.rply, im.hdr); batch != nil || err {
|
|
mset.processJetStreamFastBatchMsg(batch, im.subj, im.rply, im.hdr, im.msg, im.mt)
|
|
batch.returnToPool()
|
|
} else if batchId := getBatchId(im.hdr); batchId != _EMPTY_ {
|
|
mset.processJetStreamAtomicBatchMsg(batchId, im.subj, im.rply, im.hdr, im.msg, im.mt)
|
|
} else if isClustered {
|
|
mset.processClusteredInboundMsg(im.subj, im.rply, im.hdr, im.msg, im.mt, false)
|
|
} else {
|
|
mset.processJetStreamMsg(im.subj, im.rply, im.hdr, im.msg, 0, 0, im.mt, false, true)
|
|
}
|
|
im.returnToPool()
|
|
}
|
|
msgs.recycle(&ims)
|
|
case <-gets.ch:
|
|
dgs := gets.pop()
|
|
for _, dg := range dgs {
|
|
mset.getDirectRequest(&dg.req, dg.reply)
|
|
dgPool.Put(dg)
|
|
}
|
|
gets.recycle(&dgs)
|
|
|
|
case <-amch:
|
|
seqs := ackq.pop()
|
|
for _, seq := range seqs {
|
|
mset.ackMsg(nil, seq)
|
|
}
|
|
ackq.recycle(&seqs)
|
|
case <-qch:
|
|
return
|
|
case <-s.quitCh:
|
|
return
|
|
}
|
|
}
|
|
}
|
|
|
|
// Used to break consumers out of their monitorConsumer go routines.
|
|
func (mset *stream) resetAndWaitOnConsumers() {
|
|
mset.mu.RLock()
|
|
consumers := make([]*consumer, 0, len(mset.consumers))
|
|
for _, o := range mset.consumers {
|
|
consumers = append(consumers, o)
|
|
}
|
|
mset.mu.RUnlock()
|
|
|
|
for _, o := range consumers {
|
|
if node := o.raftNode(); node != nil {
|
|
node.StepDown()
|
|
node.Stop()
|
|
}
|
|
if o.isMonitorRunning() {
|
|
o.signalMonitorQuit()
|
|
o.monitorWg.Wait()
|
|
}
|
|
}
|
|
}
|
|
|
|
// Internal function to delete a stream.
|
|
func (mset *stream) delete() error {
|
|
if mset == nil {
|
|
return nil
|
|
}
|
|
return mset.stop(true, true)
|
|
}
|
|
|
|
// Internal function to stop or delete the stream.
|
|
func (mset *stream) stop(deleteFlag, advisory bool) error {
|
|
mset.mu.RLock()
|
|
js, jsa, name, offlineReason := mset.js, mset.jsa, mset.cfg.Name, mset.offlineReason
|
|
mset.mu.RUnlock()
|
|
|
|
if jsa == nil {
|
|
return NewJSNotEnabledForAccountError()
|
|
}
|
|
|
|
isShuttingDown := js.isShuttingDown()
|
|
|
|
// Remove from our account map first.
|
|
jsa.mu.Lock()
|
|
// Preserve in the account if it's marked offline, to have it remain queryable.
|
|
if deleteFlag || offlineReason == _EMPTY_ {
|
|
delete(jsa.streams, name)
|
|
}
|
|
accName := jsa.account.Name
|
|
jsa.mu.Unlock()
|
|
|
|
// Kick monitor and collect consumers first.
|
|
mset.mu.Lock()
|
|
|
|
// Mark closed.
|
|
mset.closed.Store(true)
|
|
|
|
// Both flags set mean a delete where we are the stream leader.
|
|
// Try to clean up any consumers used for sourcing (if one wasn't provided to us).
|
|
if deleteFlag && advisory {
|
|
if mset.cfg.Mirror != nil && mset.cfg.Mirror.Consumer == nil {
|
|
mset.tryDeleteMirrorConsumer(mset.cfg.Mirror)
|
|
}
|
|
for _, s := range mset.cfg.Sources {
|
|
if s.Consumer == nil {
|
|
id := mset.createSourcingConsumerHash(s, mset.cfg.Sources)
|
|
mset.tryDeleteSourceConsumer(id, s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Signal to the monitor loop.
|
|
// Can't use qch here.
|
|
if mset.mqch != nil {
|
|
close(mset.mqch)
|
|
mset.mqch = nil
|
|
}
|
|
|
|
// Stop responding to sync requests.
|
|
mset.stopClusterSubs()
|
|
// Unsubscribe from direct stream.
|
|
mset.unsubscribeToStream(true, isShuttingDown)
|
|
|
|
// Our info sub if we spun it up.
|
|
if mset.infoSub != nil {
|
|
mset.srv.sysUnsubscribe(mset.infoSub)
|
|
mset.infoSub = nil
|
|
}
|
|
|
|
// Clean up consumers.
|
|
var obs []*consumer
|
|
for _, o := range mset.consumers {
|
|
obs = append(obs, o)
|
|
}
|
|
// Preserve the consumers if it's marked offline, to have them remain queryable.
|
|
if deleteFlag || offlineReason == _EMPTY_ {
|
|
mset.clsMu.Lock()
|
|
mset.consumers, mset.cList, mset.csl = nil, nil, nil
|
|
mset.clsMu.Unlock()
|
|
}
|
|
|
|
// Check if we are a mirror.
|
|
if mset.mirror != nil && mset.mirror.sub != nil {
|
|
mset.unsubscribe(mset.mirror.sub)
|
|
mset.mirror.sub = nil
|
|
mset.removeInternalConsumer(mset.mirror)
|
|
}
|
|
// Now check for sources.
|
|
if len(mset.sources) > 0 {
|
|
for _, si := range mset.sources {
|
|
mset.cancelSourceConsumer(si.iname)
|
|
}
|
|
}
|
|
mset.mu.Unlock()
|
|
|
|
for _, o := range obs {
|
|
if !o.isClosed() {
|
|
// Third flag says do not broadcast a signal.
|
|
// TODO(dlc) - If we have an err here we don't want to stop
|
|
// but should we log?
|
|
o.stopWithFlags(deleteFlag, deleteFlag, false, advisory)
|
|
if !isShuttingDown {
|
|
o.signalMonitorQuit()
|
|
o.monitorWg.Wait()
|
|
}
|
|
}
|
|
}
|
|
|
|
mset.mu.Lock()
|
|
// Send stream delete advisory after the consumers.
|
|
if deleteFlag && advisory {
|
|
mset.sendDeleteAdvisoryLocked()
|
|
}
|
|
|
|
// Quit channel, do this after sending the delete advisory
|
|
if mset.qch != nil {
|
|
close(mset.qch)
|
|
mset.qch = nil
|
|
}
|
|
|
|
// Cluster cleanup
|
|
var sa *streamAssignment
|
|
if n := mset.node; n != nil {
|
|
if deleteFlag {
|
|
n.Delete()
|
|
sa = mset.sa
|
|
} else if !isShuttingDown {
|
|
// Stop Raft, unless JetStream is already shutting down, in which case they'll be stopped separately.
|
|
n.Stop()
|
|
}
|
|
}
|
|
|
|
// Cleanup duplicate timer if running.
|
|
mset.ddMu.Lock()
|
|
if mset.ddtmr != nil {
|
|
mset.ddtmr.Stop()
|
|
mset.ddtmr = nil
|
|
mset.ddmap = nil
|
|
mset.ddarr = nil
|
|
mset.ddindex = 0
|
|
}
|
|
mset.ddMu.Unlock()
|
|
|
|
sysc := mset.sysc
|
|
mset.sysc = nil
|
|
|
|
if deleteFlag {
|
|
// Unregistering ipQueues do not prevent them from push/pop
|
|
// just will remove them from the central monitoring map
|
|
mset.msgs.unregister()
|
|
mset.ackq.unregister()
|
|
mset.outq.unregister()
|
|
mset.sigq.unregister()
|
|
mset.smsgs.unregister()
|
|
}
|
|
|
|
// Snapshot store.
|
|
store := mset.store
|
|
c := mset.client
|
|
|
|
// Clustered cleanup.
|
|
mset.mu.Unlock()
|
|
|
|
// Check if the stream assignment has the group node specified.
|
|
// We need this cleared for if the stream gets reassigned here.
|
|
if sa != nil {
|
|
js.mu.Lock()
|
|
if sa.Group != nil {
|
|
sa.Group.node = nil
|
|
}
|
|
js.mu.Unlock()
|
|
}
|
|
|
|
if c != nil {
|
|
c.closeConnection(ClientClosed)
|
|
}
|
|
|
|
if sysc != nil {
|
|
sysc.closeConnection(ClientClosed)
|
|
}
|
|
|
|
if deleteFlag {
|
|
// cleanup directories after the stream
|
|
accDir := filepath.Join(js.config.StoreDir, accName)
|
|
if store != nil {
|
|
// Ignore errors.
|
|
store.Delete(false)
|
|
} else {
|
|
streamDir := filepath.Join(accDir, streamsDir)
|
|
os.RemoveAll(filepath.Join(streamDir, name))
|
|
}
|
|
// Release any resources.
|
|
js.releaseStreamResources(&mset.cfg)
|
|
// Do cleanup in separate go routine similar to how fs will use purge here..
|
|
go func() {
|
|
// no op if not empty
|
|
os.Remove(filepath.Join(accDir, streamsDir))
|
|
os.Remove(accDir)
|
|
}()
|
|
} else if store != nil {
|
|
// Ignore errors.
|
|
store.Stop()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (mset *stream) getMsg(seq uint64) (*StoredMsg, error) {
|
|
var smv StoreMsg
|
|
sm, err := mset.store.LoadMsg(seq, &smv)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// This only used in tests directly so no need to pool etc.
|
|
return &StoredMsg{
|
|
Subject: sm.subj,
|
|
Sequence: sm.seq,
|
|
Header: sm.hdr,
|
|
Data: sm.msg,
|
|
Time: time.Unix(0, sm.ts).UTC(),
|
|
}, nil
|
|
}
|
|
|
|
// getConsumers will return a copy of all the current consumers for this stream.
|
|
func (mset *stream) getConsumers() []*consumer {
|
|
mset.clsMu.RLock()
|
|
defer mset.clsMu.RUnlock()
|
|
return append([]*consumer(nil), mset.cList...)
|
|
}
|
|
|
|
// numLimitableConsumers returns the number of consumers that are not direct/sourcing consumers.
|
|
// Used to limit the number of consumers for MaxConsumers limits or WQ exclusivity.
|
|
// Lock should be held for this one.
|
|
func (mset *stream) numLimitableConsumers() int {
|
|
return len(mset.consumers) - mset.sourcingConsumers
|
|
}
|
|
|
|
// This returns all consumers that are not DIRECT.
|
|
func (mset *stream) getPublicConsumers() []*consumer {
|
|
mset.clsMu.RLock()
|
|
defer mset.clsMu.RUnlock()
|
|
|
|
var obs []*consumer
|
|
for _, o := range mset.cList {
|
|
if !o.cfg.Direct {
|
|
obs = append(obs, o)
|
|
}
|
|
}
|
|
return obs
|
|
}
|
|
|
|
// This returns all consumers that are DIRECT.
|
|
func (mset *stream) getDirectConsumers() []*consumer {
|
|
mset.clsMu.RLock()
|
|
defer mset.clsMu.RUnlock()
|
|
|
|
var obs []*consumer
|
|
for _, o := range mset.cList {
|
|
if o.cfg.Direct {
|
|
obs = append(obs, o)
|
|
}
|
|
}
|
|
return obs
|
|
}
|
|
|
|
// 2 minutes plus up to 30s jitter.
|
|
const (
|
|
defaultCheckInterestStateT = 2 * time.Minute
|
|
defaultCheckInterestStateJ = 30
|
|
)
|
|
|
|
var (
|
|
checkInterestStateT = defaultCheckInterestStateT // Interval
|
|
checkInterestStateJ = defaultCheckInterestStateJ // Jitter (secs)
|
|
)
|
|
|
|
// Will check for interest retention and make sure messages
|
|
// that have been acked are processed and removed.
|
|
// This will check the ack floors of all consumers, and adjust our first sequence accordingly.
|
|
func (mset *stream) checkInterestState() {
|
|
if mset == nil || !mset.isInterestRetention() {
|
|
// If we are limits based nothing to do.
|
|
return
|
|
}
|
|
|
|
// Ensure only one of these runs at the same time.
|
|
if !mset.cisrun.CompareAndSwap(false, true) {
|
|
return
|
|
}
|
|
defer mset.cisrun.Store(false)
|
|
|
|
var ss StreamState
|
|
mset.store.FastState(&ss)
|
|
|
|
asflr := uint64(math.MaxUint64)
|
|
for _, o := range mset.getConsumers() {
|
|
o.checkStateForInterestStream(&ss)
|
|
o.mu.RLock()
|
|
chkflr := o.chkflr
|
|
o.mu.RUnlock()
|
|
asflr = min(asflr, chkflr)
|
|
}
|
|
|
|
mset.cfgMu.RLock()
|
|
rp := mset.cfg.Retention
|
|
mset.cfgMu.RUnlock()
|
|
// Remove as many messages from the "head" of the stream if there's no interest anymore.
|
|
if rp == InterestPolicy && asflr != math.MaxUint64 {
|
|
mset.store.Compact(asflr)
|
|
}
|
|
}
|
|
|
|
func (mset *stream) isInterestRetention() bool {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
return mset.cfg.Retention != LimitsPolicy
|
|
}
|
|
|
|
// NumConsumers reports on number of active consumers for this stream.
|
|
func (mset *stream) numConsumers() int {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
return len(mset.consumers)
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) setConsumer(o *consumer) {
|
|
mset.consumers[o.name] = o
|
|
if len(o.subjf) > 0 {
|
|
mset.numFilter++
|
|
}
|
|
if o.cfg.Direct || o.cfg.Sourcing {
|
|
mset.sourcingConsumers++
|
|
}
|
|
// Now update consumers list as well
|
|
mset.clsMu.Lock()
|
|
mset.cList = append(mset.cList, o)
|
|
if mset.csl == nil {
|
|
mset.csl = gsl.NewSublist[*consumer]()
|
|
}
|
|
for _, sub := range o.signalSubs() {
|
|
mset.csl.Insert(sub, o)
|
|
}
|
|
mset.clsMu.Unlock()
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) removeConsumer(o *consumer) {
|
|
if o.cfg.FilterSubject != _EMPTY_ && mset.numFilter > 0 {
|
|
mset.numFilter--
|
|
}
|
|
if (o.cfg.Direct || o.cfg.Sourcing) && mset.sourcingConsumers > 0 {
|
|
mset.sourcingConsumers--
|
|
}
|
|
if mset.consumers != nil {
|
|
delete(mset.consumers, o.name)
|
|
// Now update consumers list as well
|
|
mset.clsMu.Lock()
|
|
for i, ol := range mset.cList {
|
|
if ol == o {
|
|
mset.cList = append(mset.cList[:i], mset.cList[i+1:]...)
|
|
break
|
|
}
|
|
}
|
|
// Always remove from the leader sublist.
|
|
if mset.csl != nil {
|
|
for _, sub := range o.signalSubs() {
|
|
mset.csl.Remove(sub, o)
|
|
}
|
|
}
|
|
mset.clsMu.Unlock()
|
|
}
|
|
}
|
|
|
|
// swapSigSubs will update signal Subs for a new subject filter.
|
|
// consumer lock should not be held.
|
|
func (mset *stream) swapSigSubs(o *consumer, newFilters []string) {
|
|
mset.clsMu.Lock()
|
|
o.mu.Lock()
|
|
|
|
if o.closed || o.mset == nil {
|
|
o.mu.Unlock()
|
|
mset.clsMu.Unlock()
|
|
return
|
|
}
|
|
|
|
if o.sigSubs != nil {
|
|
if mset.csl != nil {
|
|
for _, sub := range o.sigSubs {
|
|
mset.csl.Remove(sub, o)
|
|
}
|
|
}
|
|
o.sigSubs = nil
|
|
}
|
|
|
|
if mset.csl == nil {
|
|
mset.csl = gsl.NewSublist[*consumer]()
|
|
}
|
|
// If no filters are present, add fwcs to sublist for that consumer.
|
|
if newFilters == nil {
|
|
mset.csl.Insert(fwcs, o)
|
|
o.sigSubs = append(o.sigSubs, fwcs)
|
|
} else {
|
|
// If there are filters, add their subjects to sublist.
|
|
for _, filter := range newFilters {
|
|
mset.csl.Insert(filter, o)
|
|
o.sigSubs = append(o.sigSubs, filter)
|
|
}
|
|
}
|
|
o.mu.Unlock()
|
|
mset.clsMu.Unlock()
|
|
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
|
|
if mset.numFilter > 0 && len(o.subjf) > 0 {
|
|
mset.numFilter--
|
|
}
|
|
if len(newFilters) > 0 {
|
|
mset.numFilter++
|
|
}
|
|
}
|
|
|
|
// lookupConsumer will retrieve a consumer by name.
|
|
func (mset *stream) lookupConsumer(name string) *consumer {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
return mset.consumers[name]
|
|
}
|
|
|
|
func (mset *stream) numDirectConsumers() (num int) {
|
|
mset.clsMu.RLock()
|
|
defer mset.clsMu.RUnlock()
|
|
|
|
// Consumers that are direct are not recorded at the store level.
|
|
for _, o := range mset.cList {
|
|
o.mu.RLock()
|
|
if o.cfg.Direct {
|
|
num++
|
|
}
|
|
o.mu.RUnlock()
|
|
}
|
|
return num
|
|
}
|
|
|
|
// State will return the current state for this stream.
|
|
func (mset *stream) state() StreamState {
|
|
return mset.stateWithDetail(false)
|
|
}
|
|
|
|
func (mset *stream) stateWithDetail(details bool) StreamState {
|
|
// mset.store does not change once set, so ok to reference here directly.
|
|
// We do this elsewhere as well.
|
|
store := mset.store
|
|
if store == nil {
|
|
return StreamState{}
|
|
}
|
|
|
|
// Currently rely on store for details.
|
|
if details {
|
|
return store.State()
|
|
}
|
|
// Here we do the fast version.
|
|
var state StreamState
|
|
store.FastState(&state)
|
|
return state
|
|
}
|
|
|
|
func (mset *stream) Store() StreamStore {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
return mset.store
|
|
}
|
|
|
|
// Determines if the new proposed partition is unique amongst all public consumers.
|
|
// Lock should be held.
|
|
func (mset *stream) partitionUnique(name string, partitions []string) bool {
|
|
for _, partition := range partitions {
|
|
for n, o := range mset.consumers {
|
|
// Skip the consumer being checked.
|
|
if n == name {
|
|
continue
|
|
}
|
|
o.mu.RLock()
|
|
// Ignore direct/sourcing consumers.
|
|
if o.cfg.Direct || o.cfg.Sourcing {
|
|
o.mu.RUnlock()
|
|
continue
|
|
}
|
|
if o.subjf == nil {
|
|
o.mu.RUnlock()
|
|
return false
|
|
}
|
|
for _, filter := range o.subjf {
|
|
if SubjectsCollide(partition, filter.subject) {
|
|
o.mu.RUnlock()
|
|
return false
|
|
}
|
|
}
|
|
o.mu.RUnlock()
|
|
}
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Lock should be held.
|
|
func (mset *stream) potentialFilteredConsumers() bool {
|
|
numSubjects := len(mset.cfg.Subjects)
|
|
if len(mset.consumers) == 0 || numSubjects == 0 {
|
|
return false
|
|
}
|
|
if numSubjects > 1 || subjectHasWildcard(mset.cfg.Subjects[0]) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Check if there is no interest in this sequence number across our consumers.
|
|
// The consumer passed is optional if we are processing the ack for that consumer.
|
|
// Write lock should be held.
|
|
func (mset *stream) noInterest(seq uint64, obs *consumer) bool {
|
|
return !mset.checkForInterest(seq, obs)
|
|
}
|
|
|
|
// Check if there is no interest in this sequence number and subject across our consumers.
|
|
// The consumer passed is optional if we are processing the ack for that consumer.
|
|
// Write lock should be held.
|
|
func (mset *stream) noInterestWithSubject(seq uint64, subj string, obs *consumer) bool {
|
|
return !mset.checkForInterestWithSubject(seq, subj, obs)
|
|
}
|
|
|
|
// Write lock should be held here for the stream to avoid race conditions on state.
|
|
func (mset *stream) checkForInterest(seq uint64, obs *consumer) bool {
|
|
var subj string
|
|
if mset.potentialFilteredConsumers() {
|
|
pmsg := getJSPubMsgFromPool()
|
|
defer pmsg.returnToPool()
|
|
sm, err := mset.store.LoadMsg(seq, &pmsg.StoreMsg)
|
|
if err != nil {
|
|
if err == ErrStoreEOF {
|
|
// Register this as a preAck.
|
|
mset.registerPreAck(obs, seq)
|
|
return true
|
|
}
|
|
mset.clearAllPreAcks(seq)
|
|
return false
|
|
}
|
|
subj = sm.subj
|
|
}
|
|
return mset.checkForInterestWithSubject(seq, subj, obs)
|
|
}
|
|
|
|
// Checks for interest given a sequence and subject.
|
|
func (mset *stream) checkForInterestWithSubject(seq uint64, subj string, obs *consumer) bool {
|
|
for _, o := range mset.consumers {
|
|
// If this is us or we have a registered preAck for this consumer continue inspecting.
|
|
if o == obs || mset.hasPreAck(o, seq) {
|
|
continue
|
|
}
|
|
// Check if we need an ack.
|
|
if o.needAck(seq, subj) {
|
|
return true
|
|
}
|
|
}
|
|
mset.clearAllPreAcks(seq)
|
|
return false
|
|
}
|
|
|
|
// Check if we have a pre-registered ack for this sequence.
|
|
// Write lock should be held.
|
|
func (mset *stream) hasPreAck(o *consumer, seq uint64) bool {
|
|
if o == nil || len(mset.preAcks) == 0 {
|
|
return false
|
|
}
|
|
consumers := mset.preAcks[seq]
|
|
if len(consumers) == 0 {
|
|
return false
|
|
}
|
|
_, found := consumers[o]
|
|
return found
|
|
}
|
|
|
|
// Check if we have all consumers pre-acked for this sequence and subject.
|
|
// Write lock should be held.
|
|
func (mset *stream) hasAllPreAcks(seq uint64, subj string) bool {
|
|
if len(mset.preAcks) == 0 || len(mset.preAcks[seq]) == 0 {
|
|
return false
|
|
}
|
|
// Since these can be filtered and mutually exclusive,
|
|
// if we have some preAcks we need to check all interest here.
|
|
return mset.noInterestWithSubject(seq, subj, nil)
|
|
}
|
|
|
|
// Check if we have all consumers pre-acked.
|
|
// Write lock should be held.
|
|
func (mset *stream) clearAllPreAcks(seq uint64) {
|
|
delete(mset.preAcks, seq)
|
|
}
|
|
|
|
// Clear all preAcks below floor.
|
|
// Write lock should be held.
|
|
func (mset *stream) clearAllPreAcksBelowFloor(floor uint64) {
|
|
for seq := range mset.preAcks {
|
|
if seq < floor {
|
|
delete(mset.preAcks, seq)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Clear all preAcks in [first, last]. Iterates the preAcks map, not the
|
|
// range, so callers can pass very wide ranges cheaply.
|
|
// Write lock should be held.
|
|
func (mset *stream) clearAllPreAcksInRange(first, last uint64) {
|
|
for seq := range mset.preAcks {
|
|
if seq >= first && seq <= last {
|
|
delete(mset.preAcks, seq)
|
|
}
|
|
}
|
|
}
|
|
|
|
// This will register an ack for a consumer if it arrives before the actual message.
|
|
func (mset *stream) registerPreAckLock(o *consumer, seq uint64) {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
mset.registerPreAck(o, seq)
|
|
}
|
|
|
|
// This will register an ack for a consumer if it arrives before
|
|
// the actual message.
|
|
// Write lock should be held.
|
|
func (mset *stream) registerPreAck(o *consumer, seq uint64) {
|
|
if o == nil {
|
|
return
|
|
}
|
|
if mset.preAcks == nil {
|
|
mset.preAcks = make(map[uint64]map[*consumer]struct{})
|
|
}
|
|
if mset.preAcks[seq] == nil {
|
|
mset.preAcks[seq] = make(map[*consumer]struct{})
|
|
}
|
|
mset.preAcks[seq][o] = struct{}{}
|
|
}
|
|
|
|
// This will clear an ack for a consumer.
|
|
// Write lock should be held.
|
|
func (mset *stream) clearPreAck(o *consumer, seq uint64) {
|
|
if o == nil || len(mset.preAcks) == 0 {
|
|
return
|
|
}
|
|
if consumers := mset.preAcks[seq]; len(consumers) > 0 {
|
|
delete(consumers, o)
|
|
if len(consumers) == 0 {
|
|
delete(mset.preAcks, seq)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ackMsg is called into from a consumer when we have a WorkQueue or Interest Retention Policy.
|
|
// Returns whether the message at seq was removed as a result of the ACK.
|
|
// (Or should be removed in the case of clustered streams, since it requires a message delete proposal)
|
|
func (mset *stream) ackMsg(o *consumer, seq uint64) bool {
|
|
if seq == 0 {
|
|
return false
|
|
}
|
|
|
|
// Don't make this RLock(). We need to have only 1 running at a time to gauge interest across all consumers.
|
|
mset.mu.Lock()
|
|
if mset.closed.Load() || mset.cfg.Retention == LimitsPolicy {
|
|
mset.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
store := mset.store
|
|
var state StreamState
|
|
store.FastState(&state)
|
|
|
|
// If this has arrived before we have processed the message itself.
|
|
if seq > state.LastSeq {
|
|
mset.registerPreAck(o, seq)
|
|
mset.mu.Unlock()
|
|
// We have not removed the message, but should still signal so we could retry later
|
|
// since we potentially need to remove it then.
|
|
return true
|
|
}
|
|
|
|
// Always clear pre-ack if here.
|
|
mset.clearPreAck(o, seq)
|
|
|
|
// Make sure this sequence is not below our first sequence.
|
|
if seq < state.FirstSeq {
|
|
mset.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
// If there's no interest left on this message for all consumers, we can remove it.
|
|
shouldRemove := mset.noInterest(seq, nil)
|
|
|
|
// If nothing else to do.
|
|
if !shouldRemove {
|
|
mset.mu.Unlock()
|
|
return false
|
|
}
|
|
|
|
if !mset.isClustered() {
|
|
mset.mu.Unlock()
|
|
// If we are here we should attempt to remove.
|
|
if _, err := store.RemoveMsg(seq); err == ErrStoreEOF {
|
|
// This should not happen, but being pedantic.
|
|
mset.registerPreAckLock(o, seq)
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Only propose message deletion to the stream if we're the leader, otherwise followers would also propose.
|
|
// We must be the stream leader, since we are the only one that can guarantee message ordering and ack handling.
|
|
// Either we've stored the message, and we know for sure all consumers have acked the message.
|
|
// Or, we've not stored the message yet (rare), and all consumers have registered as pre-acks,
|
|
// then we do the message delete proposal after we've stored the message instead.
|
|
// Except for a Direct AckNone consumer, as that has a nil consumer here, we still forward the delete proposal.
|
|
if o != nil && !mset.isLeader() {
|
|
// Currently, interest-based streams can race on "no interest" because consumer creates/updates go over
|
|
// the meta layer and published messages go over the stream layer. Some servers could then either store
|
|
// or not store some initial set of messages that gained new interest. To get the stream back in sync,
|
|
// we allow moving the first sequence up.
|
|
// TODO(mvv): later on only the stream leader should determine "no interest"
|
|
interestRaiseFirst := mset.cfg.Retention == InterestPolicy && seq == state.FirstSeq
|
|
mset.mu.Unlock()
|
|
if interestRaiseFirst {
|
|
if _, err := store.RemoveMsg(seq); err == ErrStoreEOF {
|
|
// This should not happen, but being pedantic.
|
|
mset.registerPreAckLock(o, seq)
|
|
}
|
|
}
|
|
// Must still mark as removal if follower. If we become leader later, we must be able to retry the proposal.
|
|
return true
|
|
}
|
|
|
|
md := streamMsgDelete{Seq: seq, NoErase: true, Stream: mset.cfg.Name}
|
|
// Directly proposes if stream leader, otherwise forwards it.
|
|
mset.node.ForwardProposal(encodeMsgDelete(&md))
|
|
mset.mu.Unlock()
|
|
return true
|
|
}
|
|
|
|
// Snapshot creates a snapshot for the stream and possibly consumers.
|
|
func (mset *stream) snapshot(deadline time.Duration, checkMsgs, includeConsumers bool) (*SnapshotResult, error) {
|
|
if mset.closed.Load() {
|
|
return nil, errStreamClosed
|
|
}
|
|
store := mset.store
|
|
return store.Snapshot(deadline, checkMsgs, includeConsumers)
|
|
}
|
|
|
|
const snapsDir = "__snapshots__"
|
|
|
|
// RestoreStream will restore a stream from a snapshot.
|
|
func (a *Account) RestoreStream(ncfg *StreamConfig, r io.Reader) (*stream, error) {
|
|
if ncfg == nil {
|
|
return nil, errors.New("nil config on stream restore")
|
|
}
|
|
|
|
s, jsa, err := a.checkForJetStream()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
js := jsa.js
|
|
if js == nil {
|
|
return nil, NewJSNotEnabledForAccountError()
|
|
}
|
|
|
|
cfg, apiErr := s.checkStreamCfg(ncfg, a, false)
|
|
if apiErr != nil {
|
|
return nil, apiErr
|
|
}
|
|
|
|
sd := filepath.Join(jsa.storeDir, snapsDir)
|
|
if _, err := os.Stat(sd); os.IsNotExist(err) {
|
|
if err := os.MkdirAll(sd, defaultDirPerms); err != nil {
|
|
return nil, fmt.Errorf("could not create snapshots directory - %v", err)
|
|
}
|
|
}
|
|
sdir, err := os.MkdirTemp(sd, "snap-")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := os.Stat(sdir); os.IsNotExist(err) {
|
|
if err := os.MkdirAll(sdir, defaultDirPerms); err != nil {
|
|
return nil, fmt.Errorf("could not create snapshots directory - %v", err)
|
|
}
|
|
}
|
|
defer os.RemoveAll(sdir)
|
|
|
|
logAndReturnError := func() error {
|
|
a.mu.RLock()
|
|
err := fmt.Errorf("unexpected content (account=%s)", a.Name)
|
|
if a.srv != nil {
|
|
a.srv.Errorf("Stream restore failed due to %v", err)
|
|
}
|
|
a.mu.RUnlock()
|
|
return err
|
|
}
|
|
sdirCheck := filepath.Clean(sdir) + string(os.PathSeparator)
|
|
|
|
_, isClustered := jsa.jetStreamAndClustered()
|
|
jsa.usageMu.RLock()
|
|
selected, tier, hasTier := jsa.selectLimits(cfg.Replicas)
|
|
jsa.usageMu.RUnlock()
|
|
reserved := int64(0)
|
|
if hasTier {
|
|
if isClustered {
|
|
js.mu.RLock()
|
|
_, reserved = js.tieredStreamAndReservationCount(a.Name, tier, &cfg)
|
|
js.mu.RUnlock()
|
|
} else {
|
|
reserved = jsa.tieredReservation(tier, &cfg)
|
|
}
|
|
}
|
|
|
|
var bc int64
|
|
tr := tar.NewReader(s2.NewReader(r))
|
|
for {
|
|
hdr, err := tr.Next()
|
|
if err == io.EOF {
|
|
break // End of snapshot
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if hdr.Typeflag != tar.TypeReg {
|
|
return nil, logAndReturnError()
|
|
}
|
|
bc += hdr.Size
|
|
js.mu.RLock()
|
|
err = js.checkAllLimits(&selected, &cfg, reserved, bc)
|
|
js.mu.RUnlock()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
fpath := filepath.Join(sdir, filepath.Clean(hdr.Name))
|
|
if !strings.HasPrefix(fpath, sdirCheck) {
|
|
return nil, logAndReturnError()
|
|
}
|
|
os.MkdirAll(filepath.Dir(fpath), defaultDirPerms)
|
|
fd, err := os.OpenFile(fpath, os.O_CREATE|os.O_RDWR, 0600)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = io.Copy(fd, tr)
|
|
fd.Close()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Check metadata.
|
|
// The cfg passed in will be the new identity for the stream.
|
|
var fcfg FileStreamInfo
|
|
b, err := os.ReadFile(filepath.Join(sdir, JetStreamMetaFile))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := json.Unmarshal(b, &fcfg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Check to make sure names match.
|
|
if fcfg.Name != cfg.Name {
|
|
return nil, errors.New("stream names do not match")
|
|
}
|
|
|
|
// See if this stream already exists.
|
|
if _, err := a.lookupStream(cfg.Name); err == nil {
|
|
return nil, NewJSStreamNameExistRestoreFailedError()
|
|
}
|
|
// Move into the correct place here.
|
|
ndir := filepath.Join(jsa.storeDir, streamsDir, cfg.Name)
|
|
// Remove old one if for some reason it is still here.
|
|
if _, err := os.Stat(ndir); err == nil {
|
|
os.RemoveAll(ndir)
|
|
}
|
|
// Make sure our destination streams directory exists.
|
|
if err := os.MkdirAll(filepath.Join(jsa.storeDir, streamsDir), defaultDirPerms); err != nil {
|
|
return nil, err
|
|
}
|
|
// Move into new location.
|
|
if err := os.Rename(sdir, ndir); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
mset, err := a.addStream(&cfg)
|
|
if err != nil {
|
|
// Make sure to clean up after ourselves here.
|
|
os.RemoveAll(ndir)
|
|
return nil, err
|
|
}
|
|
if !fcfg.Created.IsZero() {
|
|
mset.setCreatedTime(fcfg.Created)
|
|
}
|
|
|
|
// Make sure we do an update if the configs have changed.
|
|
if !reflect.DeepEqual(fcfg.StreamConfig, cfg) {
|
|
if err := mset.update(&cfg); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
// Now do consumers.
|
|
odir := filepath.Join(ndir, consumerDir)
|
|
ofis, _ := os.ReadDir(odir)
|
|
for _, ofi := range ofis {
|
|
metafile := filepath.Join(odir, ofi.Name(), JetStreamMetaFile)
|
|
metasum := filepath.Join(odir, ofi.Name(), JetStreamMetaFileSum)
|
|
if _, err := os.Stat(metafile); os.IsNotExist(err) {
|
|
mset.stop(true, false)
|
|
return nil, fmt.Errorf("error restoring consumer [%q]: %v", ofi.Name(), err)
|
|
}
|
|
buf, err := os.ReadFile(metafile)
|
|
if err != nil {
|
|
mset.stop(true, false)
|
|
return nil, fmt.Errorf("error restoring consumer [%q]: %v", ofi.Name(), err)
|
|
}
|
|
if _, err := os.Stat(metasum); os.IsNotExist(err) {
|
|
mset.stop(true, false)
|
|
return nil, fmt.Errorf("error restoring consumer [%q]: %v", ofi.Name(), err)
|
|
}
|
|
var cfg FileConsumerInfo
|
|
if err := json.Unmarshal(buf, &cfg); err != nil {
|
|
mset.stop(true, false)
|
|
return nil, fmt.Errorf("error restoring consumer [%q]: %v", ofi.Name(), err)
|
|
}
|
|
isEphemeral := !isDurableConsumer(&cfg.ConsumerConfig)
|
|
if isEphemeral {
|
|
// This is an ephermal consumer and this could fail on restart until
|
|
// the consumer can reconnect. We will create it as a durable and switch it.
|
|
cfg.ConsumerConfig.Durable = ofi.Name()
|
|
}
|
|
obs, err := mset.addConsumer(&cfg.ConsumerConfig)
|
|
if err != nil {
|
|
mset.stop(true, false)
|
|
return nil, fmt.Errorf("error restoring consumer [%q]: %v", ofi.Name(), err)
|
|
}
|
|
if isEphemeral {
|
|
obs.switchToEphemeral()
|
|
}
|
|
if !cfg.Created.IsZero() {
|
|
obs.setCreatedTime(cfg.Created)
|
|
}
|
|
obs.mu.Lock()
|
|
err = obs.readStoredState()
|
|
obs.mu.Unlock()
|
|
if err != nil {
|
|
mset.stop(true, false)
|
|
return nil, fmt.Errorf("error restoring consumer [%q]: %v", ofi.Name(), err)
|
|
}
|
|
}
|
|
return mset, nil
|
|
}
|
|
|
|
// This is to check for dangling messages on interest retention streams. Only called on account enable.
|
|
// Issue https://github.com/nats-io/nats-server/issues/3612
|
|
func (mset *stream) checkForOrphanMsgs() {
|
|
mset.mu.RLock()
|
|
consumers := make([]*consumer, 0, len(mset.consumers))
|
|
for _, o := range mset.consumers {
|
|
consumers = append(consumers, o)
|
|
}
|
|
accName, stream := mset.acc.Name, mset.cfg.Name
|
|
|
|
var ss StreamState
|
|
mset.store.FastState(&ss)
|
|
mset.mu.RUnlock()
|
|
|
|
for _, o := range consumers {
|
|
if err := o.checkStateForInterestStream(&ss); err == errAckFloorHigherThanLastSeq {
|
|
o.mu.RLock()
|
|
s, consumer := o.srv, o.name
|
|
state, _ := o.store.State()
|
|
asflr := state.AckFloor.Stream
|
|
o.mu.RUnlock()
|
|
// Warn about stream state vs our ack floor.
|
|
s.RateLimitWarnf("Detected consumer '%s > %s > %s' ack floor %d is ahead of stream's last sequence %d",
|
|
accName, stream, consumer, asflr, ss.LastSeq)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Check on startup to make sure that consumers replication matches us.
|
|
// Interest retention requires replication matches.
|
|
func (mset *stream) checkConsumerReplication() {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
|
|
if mset.cfg.Retention != InterestPolicy {
|
|
return
|
|
}
|
|
|
|
s, acc := mset.srv, mset.acc
|
|
for _, o := range mset.consumers {
|
|
o.mu.RLock()
|
|
// Consumer replicas 0 can be a legit config for the replicas and we will inherit from the stream
|
|
// when this is the case.
|
|
if mset.cfg.Replicas != o.cfg.Replicas && o.cfg.Replicas != 0 {
|
|
s.Errorf("consumer '%s > %s > %s' MUST match replication (%d vs %d) of stream with interest policy",
|
|
acc, mset.cfg.Name, o.cfg.Name, mset.cfg.Replicas, o.cfg.Replicas)
|
|
}
|
|
o.mu.RUnlock()
|
|
}
|
|
}
|
|
|
|
// Will check if we are running in the monitor already and if not set the appropriate flag.
|
|
func (mset *stream) checkInMonitor() bool {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
|
|
if mset.inMonitor {
|
|
return true
|
|
}
|
|
mset.inMonitor = true
|
|
return false
|
|
}
|
|
|
|
// Clear us being in the monitor routine.
|
|
func (mset *stream) clearMonitorRunning() {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
mset.inMonitor = false
|
|
|
|
// We've stopped running the monitor goroutine, now also do additional cleanup.
|
|
mset.deleteBatchApplyState()
|
|
}
|
|
|
|
// Check if our monitor is running.
|
|
func (mset *stream) isMonitorRunning() bool {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
return mset.inMonitor
|
|
}
|
|
|
|
// setWriteErr stores the write error in the stream.
|
|
func (mset *stream) setWriteErr(err error) {
|
|
mset.mu.Lock()
|
|
defer mset.mu.Unlock()
|
|
mset.setWriteErrLocked(err)
|
|
}
|
|
|
|
func (mset *stream) setWriteErrLocked(err error) {
|
|
if mset.werr != nil {
|
|
return
|
|
}
|
|
// Ignore non-write errors.
|
|
if err == ErrStoreClosed {
|
|
return
|
|
}
|
|
mset.srv.Errorf("JetStream stream '%s > %s' critical write error: %v", mset.acc.Name, mset.cfg.Name, err)
|
|
mset.werr = err
|
|
assert.Unreachable("Stream encountered write error", map[string]any{
|
|
"account": mset.acc.Name,
|
|
"stream": mset.cfg.Name,
|
|
"err": err,
|
|
})
|
|
|
|
// If stream is replicated, put it in observer mode to make sure another server can pick it up.
|
|
if node := mset.node; node != nil {
|
|
node.StepDown()
|
|
node.SetObserver(true)
|
|
}
|
|
}
|
|
|
|
// getWriteErr returns the write error stored in the stream (if any).
|
|
func (mset *stream) getWriteErr() error {
|
|
mset.mu.RLock()
|
|
defer mset.mu.RUnlock()
|
|
return mset.werr
|
|
}
|
|
|
|
// Adjust accounting for sent messages as part of replication.
|
|
func (mset *stream) trackReplicationTraffic(node RaftNode, sz int, r int) {
|
|
// If we are using the system account for NRG, add in the extra sent msgs and bytes to our account
|
|
// so that the end user / account owner has visibility.
|
|
if node.IsSystemAccount() && mset.acc != nil && r > 1 {
|
|
outMsgs := int64(r - 1)
|
|
outBytes := int64(sz * (r - 1))
|
|
|
|
mset.acc.stats.Lock()
|
|
mset.acc.stats.outMsgs += outMsgs
|
|
mset.acc.stats.outBytes += outBytes
|
|
mset.acc.stats.rt.outMsgs += outMsgs
|
|
mset.acc.stats.rt.outBytes += outBytes
|
|
mset.acc.stats.Unlock()
|
|
}
|
|
}
|